2 * ui-grid - v3.1.1 - 2016-02-09
3 * Copyright (c) 2016 ; License: MIT
8 angular.module('ui.grid.i18n', []);
9 angular.module('ui.grid', ['ui.grid.i18n']);
13 angular.module('ui.grid').constant('uiGridConstants', {
14 LOG_DEBUG_MESSAGES: true,
15 LOG_WARN_MESSAGES: true,
16 LOG_ERROR_MESSAGES: true,
17 CUSTOM_FILTERS: /CUSTOM_FILTERS/g,
18 COL_FIELD: /COL_FIELD/g,
19 MODEL_COL_FIELD: /MODEL_COL_FIELD/g,
20 TOOLTIP: /title=\"TOOLTIP\"/g,
21 DISPLAY_CELL_TEMPLATE: /DISPLAY_CELL_TEMPLATE/g,
22 TEMPLATE_REGEXP: /<.+>/,
23 FUNC_REGEXP: /(\([^)]*\))?$/,
26 BRACKET_REGEXP: /^(.*)((?:\s*\[\s*\d+\s*\]\s*)|(?:\s*\[\s*"(?:[^"\\]|\\.)*"\s*\]\s*)|(?:\s*\[\s*'(?:[^'\\]|\\.)*'\s*\]\s*))(.*)$/,
27 COL_CLASS_PREFIX: 'ui-grid-col',
29 GRID_SCROLL: 'uiGridScroll',
30 COLUMN_MENU_SHOWN: 'uiGridColMenuShown',
31 ITEM_DRAGGING: 'uiGridItemDragStart', // For any item being dragged
32 COLUMN_HEADER_CLICK: 'uiGridColumnHeaderClick'
34 // copied from http://www.lsauer.com/2011/08/javascript-keymap-keycodes-in-json.html
83 GREATER_THAN_OR_EQUAL: 64,
85 LESS_THAN_OR_EQUAL: 256,
99 // TODO(c0bra): Create full list of these somehow. NOTE: do any allow a space before or after them?
100 CURRENCY_SYMBOLS: ['ƒ', '$', '£', '$', '¤', '¥', '៛', '₩', '₱', '฿', '₫'],
126 angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUtil', 'uiGridConstants', function ($compile, $parse, gridUtil, uiGridConstants) {
131 compile: function() {
133 pre: function($scope, $elm, $attrs, uiGridCtrl) {
134 function compileTemplate() {
135 var compiledElementFn = $scope.col.compiledElementFn;
137 compiledElementFn($scope, function(clonedElement, scope) {
138 $elm.append(clonedElement);
142 // If the grid controller is present, use it to get the compiled cell template function
143 if (uiGridCtrl && $scope.col.compiledElementFn) {
146 // No controller, compile the element manually (for unit tests)
148 if ( uiGridCtrl && !$scope.col.compiledElementFn ){
149 // gridUtil.logError('Render has been called before precompile. Please log a ui-grid issue');
151 $scope.col.getCompiledElementFn()
152 .then(function (compiledElementFn) {
153 compiledElementFn($scope, function(clonedElement, scope) {
154 $elm.append(clonedElement);
159 var html = $scope.col.cellTemplate
160 .replace(uiGridConstants.MODEL_COL_FIELD, 'row.entity.' + gridUtil.preEval($scope.col.field))
161 .replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
163 var cellElement = $compile(html)($scope);
164 $elm.append(cellElement);
168 post: function($scope, $elm, $attrs, uiGridCtrl) {
169 var initColClass = $scope.col.getColClass(false);
170 $elm.addClass(initColClass);
173 var updateClass = function( grid ){
176 contents.removeClass( classAdded );
180 if (angular.isFunction($scope.col.cellClass)) {
181 classAdded = $scope.col.cellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
184 classAdded = $scope.col.cellClass;
186 contents.addClass(classAdded);
189 if ($scope.col.cellClass) {
193 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
194 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN, uiGridConstants.dataChange.EDIT]);
196 // watch the col and row to see if they change - which would indicate that we've scrolled or sorted or otherwise
197 // changed the row/col that this cell relates to, and we need to re-evaluate cell classes and maybe other things
198 var cellChangeFunction = function( n, o ){
200 if ( classAdded || $scope.col.cellClass ){
204 // See if the column's internal class has changed
205 var newColClass = $scope.col.getColClass(false);
206 if (newColClass !== initColClass) {
207 $elm.removeClass(initColClass);
208 $elm.addClass(newColClass);
209 initColClass = newColClass;
214 // TODO(c0bra): Turn this into a deep array watch
215 /* shouldn't be needed any more given track by col.name
216 var colWatchDereg = $scope.$watch( 'col', cellChangeFunction );
218 var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction );
221 var deregisterFunction = function() {
227 $scope.$on( '$destroy', deregisterFunction );
228 $elm.on( '$destroy', deregisterFunction );
240 angular.module('ui.grid')
241 .service('uiGridColumnMenuService', [ 'i18nService', 'uiGridConstants', 'gridUtil',
242 function ( i18nService, uiGridConstants, gridUtil ) {
245 * @name ui.grid.service:uiGridColumnMenuService
247 * @description Services for working with column menus, factored out
248 * to make the code easier to understand
254 * @methodOf ui.grid.service:uiGridColumnMenuService
256 * @description Sets defaults, puts a reference to the $scope on
257 * the uiGridController
258 * @param {$scope} $scope the $scope from the uiGridColumnMenu
259 * @param {controller} uiGridCtrl the uiGridController for the grid
263 initialize: function( $scope, uiGridCtrl ){
264 $scope.grid = uiGridCtrl.grid;
266 // Store a reference to this link/controller in the main uiGrid controller
267 // to allow showMenu later
268 uiGridCtrl.columnMenuScope = $scope;
270 // Save whether we're shown or not so the columns can check
271 $scope.menuShown = false;
277 * @methodOf ui.grid.service:uiGridColumnMenuService
278 * @name setColMenuItemWatch
279 * @description Setup a watch on $scope.col.menuItems, and update
280 * menuItems based on this. $scope.col needs to be set by the column
281 * before calling the menu.
282 * @param {$scope} $scope the $scope from the uiGridColumnMenu
283 * @param {controller} uiGridCtrl the uiGridController for the grid
287 setColMenuItemWatch: function ( $scope ){
288 var deregFunction = $scope.$watch('col.menuItems', function (n) {
289 if (typeof(n) !== 'undefined' && n && angular.isArray(n)) {
290 n.forEach(function (item) {
291 if (typeof(item.context) === 'undefined' || !item.context) {
294 item.context.col = $scope.col;
297 $scope.menuItems = $scope.defaultMenuItems.concat(n);
300 $scope.menuItems = $scope.defaultMenuItems;
304 $scope.$on( '$destroy', deregFunction );
310 * @name enableSorting
311 * @propertyOf ui.grid.class:GridOptions.columnDef
312 * @description (optional) True by default. When enabled, this setting adds sort
313 * widgets to the column header, allowing sorting of the data in the individual column.
317 * @methodOf ui.grid.service:uiGridColumnMenuService
319 * @description determines whether this column is sortable
320 * @param {$scope} $scope the $scope from the uiGridColumnMenu
323 sortable: function( $scope ) {
324 if ( $scope.grid.options.enableSorting && typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.enableSorting) {
334 * @methodOf ui.grid.service:uiGridColumnMenuService
336 * @description determines whether the requested sort direction is current active, to
337 * allow highlighting in the menu
338 * @param {$scope} $scope the $scope from the uiGridColumnMenu
339 * @param {string} direction the direction that we'd have selected for us to be active
342 isActiveSort: function( $scope, direction ){
343 return (typeof($scope.col) !== 'undefined' && typeof($scope.col.sort) !== 'undefined' &&
344 typeof($scope.col.sort.direction) !== 'undefined' && $scope.col.sort.direction === direction);
350 * @methodOf ui.grid.service:uiGridColumnMenuService
351 * @name suppressRemoveSort
352 * @description determines whether we should suppress the removeSort option
353 * @param {$scope} $scope the $scope from the uiGridColumnMenu
356 suppressRemoveSort: function( $scope ) {
357 if ($scope.col && $scope.col.suppressRemoveSort) {
369 * @propertyOf ui.grid.class:GridOptions.columnDef
370 * @description (optional) True by default. When set to false, this setting prevents a user from hiding the column
371 * using the column menu or the grid menu.
375 * @methodOf ui.grid.service:uiGridColumnMenuService
377 * @description determines whether a column can be hidden, by checking the enableHiding columnDef option
378 * @param {$scope} $scope the $scope from the uiGridColumnMenu
381 hideable: function( $scope ) {
382 if (typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.colDef && $scope.col.colDef.enableHiding === false ) {
393 * @methodOf ui.grid.service:uiGridColumnMenuService
394 * @name getDefaultMenuItems
395 * @description returns the default menu items for a column menu
396 * @param {$scope} $scope the $scope from the uiGridColumnMenu
399 getDefaultMenuItems: function( $scope ){
402 title: i18nService.getSafeText('sort.ascending'),
403 icon: 'ui-grid-icon-sort-alt-up',
404 action: function($event) {
405 $event.stopPropagation();
406 $scope.sortColumn($event, uiGridConstants.ASC);
409 return service.sortable( $scope );
412 return service.isActiveSort( $scope, uiGridConstants.ASC);
416 title: i18nService.getSafeText('sort.descending'),
417 icon: 'ui-grid-icon-sort-alt-down',
418 action: function($event) {
419 $event.stopPropagation();
420 $scope.sortColumn($event, uiGridConstants.DESC);
423 return service.sortable( $scope );
426 return service.isActiveSort( $scope, uiGridConstants.DESC);
430 title: i18nService.getSafeText('sort.remove'),
431 icon: 'ui-grid-icon-cancel',
432 action: function ($event) {
433 $event.stopPropagation();
434 $scope.unsortColumn();
437 return service.sortable( $scope ) &&
438 typeof($scope.col) !== 'undefined' && (typeof($scope.col.sort) !== 'undefined' &&
439 typeof($scope.col.sort.direction) !== 'undefined') && $scope.col.sort.direction !== null &&
440 !service.suppressRemoveSort( $scope );
444 title: i18nService.getSafeText('column.hide'),
445 icon: 'ui-grid-icon-cancel',
447 return service.hideable( $scope );
449 action: function ($event) {
450 $event.stopPropagation();
460 * @methodOf ui.grid.service:uiGridColumnMenuService
461 * @name getColumnElementPosition
462 * @description gets the position information needed to place the column
463 * menu below the column header
464 * @param {$scope} $scope the $scope from the uiGridColumnMenu
465 * @param {GridCol} column the column we want to position below
466 * @param {element} $columnElement the column element we want to position below
467 * @returns {hash} containing left, top, offset, height, width
470 getColumnElementPosition: function( $scope, column, $columnElement ){
471 var positionData = {};
472 positionData.left = $columnElement[0].offsetLeft;
473 positionData.top = $columnElement[0].offsetTop;
474 positionData.parentLeft = $columnElement[0].offsetParent.offsetLeft;
476 // Get the grid scrollLeft
477 positionData.offset = 0;
478 if (column.grid.options.offsetLeft) {
479 positionData.offset = column.grid.options.offsetLeft;
482 positionData.height = gridUtil.elementHeight($columnElement, true);
483 positionData.width = gridUtil.elementWidth($columnElement, true);
491 * @methodOf ui.grid.service:uiGridColumnMenuService
492 * @name repositionMenu
493 * @description Reposition the menu below the new column. If the menu has no child nodes
494 * (i.e. it's not currently visible) then we guess it's width at 100, we'll be called again
496 * @param {$scope} $scope the $scope from the uiGridColumnMenu
497 * @param {GridCol} column the column we want to position below
498 * @param {hash} positionData a hash containing left, top, offset, height, width
499 * @param {element} $elm the column menu element that we want to reposition
500 * @param {element} $columnElement the column element that we want to reposition underneath
503 repositionMenu: function( $scope, column, positionData, $elm, $columnElement ) {
504 var menu = $elm[0].querySelectorAll('.ui-grid-menu');
506 // It's possible that the render container of the column we're attaching to is
507 // offset from the grid (i.e. pinned containers), we need to get the difference in the offsetLeft
508 // between the render container and the grid
509 var renderContainerElm = gridUtil.closestElm($columnElement, '.ui-grid-render-container');
510 var renderContainerOffset = renderContainerElm.getBoundingClientRect().left - $scope.grid.element[0].getBoundingClientRect().left;
512 var containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].scrollLeft;
514 // default value the last width for _this_ column, otherwise last width for _any_ column, otherwise default to 170
515 var myWidth = column.lastMenuWidth ? column.lastMenuWidth : ( $scope.lastMenuWidth ? $scope.lastMenuWidth : 170);
516 var paddingRight = column.lastMenuPaddingRight ? column.lastMenuPaddingRight : ( $scope.lastMenuPaddingRight ? $scope.lastMenuPaddingRight : 10);
518 if ( menu.length !== 0 ){
519 var mid = menu[0].querySelectorAll('.ui-grid-menu-mid');
520 if ( mid.length !== 0 && !angular.element(mid).hasClass('ng-hide') ) {
521 myWidth = gridUtil.elementWidth(menu, true);
522 $scope.lastMenuWidth = myWidth;
523 column.lastMenuWidth = myWidth;
525 // TODO(c0bra): use padding-left/padding-right based on document direction (ltr/rtl), place menu on proper side
526 // Get the column menu right padding
527 paddingRight = parseInt(gridUtil.getStyles(angular.element(menu)[0])['paddingRight'], 10);
528 $scope.lastMenuPaddingRight = paddingRight;
529 column.lastMenuPaddingRight = paddingRight;
533 var left = positionData.left + renderContainerOffset - containerScrollLeft + positionData.parentLeft + positionData.width - myWidth + paddingRight;
534 if (left < positionData.offset){
535 left = positionData.offset;
538 $elm.css('left', left + 'px');
539 $elm.css('top', (positionData.top + positionData.height) + 'px');
548 .directive('uiGridColumnMenu', ['$timeout', 'gridUtil', 'uiGridConstants', 'uiGridColumnMenuService', '$document',
549 function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $document) {
552 * @name ui.grid.directive:uiGridColumnMenu
553 * @description Provides the column menu framework, leverages uiGridMenu underneath
557 var uiGridColumnMenu = {
561 templateUrl: 'ui-grid/uiGridColumnMenu',
563 link: function ($scope, $elm, $attrs, uiGridCtrl) {
564 uiGridColumnMenuService.initialize( $scope, uiGridCtrl );
566 $scope.defaultMenuItems = uiGridColumnMenuService.getDefaultMenuItems( $scope );
568 // Set the menu items for use with the column menu. The user can later add additional items via the watch
569 $scope.menuItems = $scope.defaultMenuItems;
570 uiGridColumnMenuService.setColMenuItemWatch( $scope );
575 * @methodOf ui.grid.directive:uiGridColumnMenu
577 * @description Shows the column menu. If the menu is already displayed it
578 * calls the menu to ask it to hide (it will animate), then it repositions the menu
579 * to the right place whilst hidden (it will make an assumption on menu width),
580 * then it asks the menu to show (it will animate), then it repositions the menu again
581 * once we can calculate it's size.
582 * @param {GridCol} column the column we want to position below
583 * @param {element} $columnElement the column element we want to position below
585 $scope.showMenu = function(column, $columnElement, event) {
586 // Swap to this column
589 // Get the position information for the column element
590 var colElementPosition = uiGridColumnMenuService.getColumnElementPosition( $scope, column, $columnElement );
592 if ($scope.menuShown) {
593 // we want to hide, then reposition, then show, but we want to wait for animations
594 // we set a variable, and then rely on the menu-hidden event to call the reposition and show
595 $scope.colElement = $columnElement;
596 $scope.colElementPosition = colElementPosition;
597 $scope.hideThenShow = true;
599 $scope.$broadcast('hide-menu', { originalEvent: event });
601 $scope.menuShown = true;
602 uiGridColumnMenuService.repositionMenu( $scope, column, colElementPosition, $elm, $columnElement );
604 $scope.colElement = $columnElement;
605 $scope.colElementPosition = colElementPosition;
606 $scope.$broadcast('show-menu', { originalEvent: event });
614 * @methodOf ui.grid.directive:uiGridColumnMenu
616 * @description Hides the column menu.
617 * @param {boolean} broadcastTrigger true if we were triggered by a broadcast
618 * from the menu itself - in which case don't broadcast again as we'll get
621 $scope.hideMenu = function( broadcastTrigger ) {
622 $scope.menuShown = false;
623 if ( !broadcastTrigger ){
624 $scope.$broadcast('hide-menu');
629 $scope.$on('menu-hidden', function() {
630 if ( $scope.hideThenShow ){
631 delete $scope.hideThenShow;
633 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
634 $scope.$broadcast('show-menu');
636 $scope.menuShown = true;
638 $scope.hideMenu( true );
641 //Focus on the menu button
642 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false);
647 $scope.$on('menu-shown', function() {
648 $timeout( function() {
649 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
650 //Focus on the first item
651 gridUtil.focus.bySelector($document, '.ui-grid-menu-items .ui-grid-menu-item', true);
652 delete $scope.colElementPosition;
653 delete $scope.columnElement;
659 $scope.sortColumn = function (event, dir) {
660 event.stopPropagation();
662 $scope.grid.sortColumn($scope.col, dir, true)
664 $scope.grid.refresh();
669 $scope.unsortColumn = function () {
672 $scope.grid.refresh();
676 //Since we are hiding this column the default hide action will fail so we need to focus somewhere else.
677 var setFocusOnHideColumn = function(){
679 // Get the UID of the first
680 var focusToGridMenu = function(){
681 return gridUtil.focus.byId('grid-menu', $scope.grid);
685 $scope.grid.columns.some(function(element, index){
686 if (angular.equals(element, $scope.col)) {
692 var previousVisibleCol;
693 // Try and find the next lower or nearest column to focus on
694 $scope.grid.columns.some(function(element, index){
695 if (!element.visible){
697 } // This columns index is below the current column index
698 else if ( index < thisIndex){
699 previousVisibleCol = element;
700 } // This elements index is above this column index and we haven't found one that is lower
701 else if ( index > thisIndex && !previousVisibleCol) {
702 // This is the next best thing
703 previousVisibleCol = element;
704 // We've found one so use it.
706 } // We've reached an element with an index above this column and the previousVisibleCol variable has been set
707 else if (index > thisIndex && previousVisibleCol) {
712 // If found then focus on it
713 if (previousVisibleCol){
714 var colClass = previousVisibleCol.getColClass();
715 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + colClass+ ' .ui-grid-header-cell-primary-focus', true).then(angular.noop, function(reason){
716 if (reason !== 'canceled'){ // If this is canceled then don't perform the action
717 //The fallback action is to focus on the grid menu
718 return focusToGridMenu();
722 // Fallback action to focus on the grid menu
728 $scope.hideColumn = function () {
729 $scope.col.colDef.visible = false;
730 $scope.col.visible = false;
732 $scope.grid.queueGridRefresh();
734 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
735 $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col );
737 // We are hiding so the default action of focusing on the button that opened this menu will fail.
738 setFocusOnHideColumn();
744 controller: ['$scope', function ($scope) {
747 $scope.$watch('menuItems', function (n) {
753 return uiGridColumnMenu;
762 angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', 'i18nService', 'gridUtil', function ($compile, $templateCache, i18nService, gridUtil) {
765 compile: function() {
767 pre: function ($scope, $elm, $attrs, controllers) {
768 $scope.col.updateFilters = function( filterable ){
769 $elm.children().remove();
771 var template = $scope.col.filterHeaderTemplate;
773 $elm.append($compile(template)($scope));
777 $scope.$on( '$destroy', function() {
778 delete $scope.col.updateFilters;
781 post: function ($scope, $elm, $attrs, controllers){
782 $scope.aria = i18nService.getSafeText('headerCell.aria');
783 $scope.removeFilter = function(colFilter, index){
784 colFilter.term = null;
785 //Set the focus to the filter input after the action disables the button
786 gridUtil.focus.bySelector($elm, '.ui-grid-filter-input-' + index);
798 angular.module('ui.grid').directive('uiGridFooterCell', ['$timeout', 'gridUtil', 'uiGridConstants', '$compile',
799 function ($timeout, gridUtil, uiGridConstants, $compile) {
800 var uiGridFooterCell = {
809 compile: function compile(tElement, tAttrs, transclude) {
811 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
812 var cellFooter = $compile($scope.col.footerCellTemplate)($scope);
813 $elm.append(cellFooter);
815 post: function ($scope, $elm, $attrs, uiGridCtrl) {
816 //$elm.addClass($scope.col.getColClass(false));
817 $scope.grid = uiGridCtrl.grid;
819 var initColClass = $scope.col.getColClass(false);
820 $elm.addClass(initColClass);
822 // apply any footerCellClass
824 var updateClass = function( grid ){
827 contents.removeClass( classAdded );
831 if (angular.isFunction($scope.col.footerCellClass)) {
832 classAdded = $scope.col.footerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
835 classAdded = $scope.col.footerCellClass;
837 contents.addClass(classAdded);
840 if ($scope.col.footerCellClass) {
844 $scope.col.updateAggregationValue();
846 // Watch for column changes so we can alter the col cell class properly
847 /* shouldn't be needed any more, given track by col.name
848 $scope.$watch('col', function (n, o) {
850 // See if the column's internal class has changed
851 var newColClass = $scope.col.getColClass(false);
852 if (newColClass !== initColClass) {
853 $elm.removeClass(initColClass);
854 $elm.addClass(newColClass);
855 initColClass = newColClass;
862 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
863 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]);
864 // listen for visible rows change and update aggregation values
865 $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue );
866 $scope.grid.api.core.on.rowsRendered( $scope, updateClass );
867 $scope.$on( '$destroy', dataChangeDereg );
873 return uiGridFooterCell;
881 angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
887 require: ['^uiGrid', '^uiGridRenderContainer'],
889 compile: function ($elm, $attrs) {
891 pre: function ($scope, $elm, $attrs, controllers) {
892 var uiGridCtrl = controllers[0];
893 var containerCtrl = controllers[1];
895 $scope.grid = uiGridCtrl.grid;
896 $scope.colContainer = containerCtrl.colContainer;
898 containerCtrl.footer = $elm;
900 var footerTemplate = $scope.grid.options.footerTemplate;
901 gridUtil.getTemplate(footerTemplate)
902 .then(function (contents) {
903 var template = angular.element(contents);
905 var newElm = $compile(template)($scope);
909 // Inject a reference to the footer viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
910 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
912 if (footerViewport) {
913 containerCtrl.footerViewport = footerViewport;
919 post: function ($scope, $elm, $attrs, controllers) {
920 var uiGridCtrl = controllers[0];
921 var containerCtrl = controllers[1];
923 // gridUtil.logDebug('ui-grid-footer link');
925 var grid = uiGridCtrl.grid;
927 // Don't animate footer cells
928 gridUtil.disableAnimations($elm);
930 containerCtrl.footer = $elm;
932 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
933 if (footerViewport) {
934 containerCtrl.footerViewport = footerViewport;
946 angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
954 compile: function ($elm, $attrs) {
956 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
958 $scope.grid = uiGridCtrl.grid;
962 var footerTemplate = $scope.grid.options.gridFooterTemplate;
963 gridUtil.getTemplate(footerTemplate)
964 .then(function (contents) {
965 var template = angular.element(contents);
967 var newElm = $compile(template)($scope);
972 post: function ($scope, $elm, $attrs, controllers) {
984 angular.module('ui.grid').directive('uiGridGroupPanel', ["$compile", "uiGridConstants", "gridUtil", function($compile, uiGridConstants, gridUtil) {
985 var defaultTemplate = 'ui-grid/ui-grid-group-panel';
992 compile: function($elm, $attrs) {
994 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
995 var groupPanelTemplate = $scope.grid.options.groupPanelTemplate || defaultTemplate;
997 gridUtil.getTemplate(groupPanelTemplate)
998 .then(function (contents) {
999 var template = angular.element(contents);
1001 var newElm = $compile(template)($scope);
1002 $elm.append(newElm);
1006 post: function ($scope, $elm, $attrs, uiGridCtrl) {
1007 $elm.bind('$destroy', function() {
1008 // scrollUnbinder();
1020 angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService',
1021 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) {
1022 // Do stuff after mouse has been down this many ms on the header cell
1023 var mousedownTimeout = 500;
1024 var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa
1026 var uiGridHeaderCell = {
1033 require: ['^uiGrid', '^uiGridRenderContainer'],
1035 compile: function() {
1037 pre: function ($scope, $elm, $attrs) {
1038 var cellHeader = $compile($scope.col.headerCellTemplate)($scope);
1039 $elm.append(cellHeader);
1042 post: function ($scope, $elm, $attrs, controllers) {
1043 var uiGridCtrl = controllers[0];
1044 var renderContainerCtrl = controllers[1];
1047 headerCell: i18nService.getSafeText('headerCell'),
1048 sort: i18nService.getSafeText('sort')
1050 $scope.isSortPriorityVisible = function() {
1051 //show sort priority if column is sorted and there is at least one other sorted column
1052 return angular.isNumber($scope.col.sort.priority) && $scope.grid.columns.some(function(element, index){
1053 return angular.isNumber(element.sort.priority) && element !== $scope.col;
1056 $scope.getSortDirectionAriaLabel = function(){
1057 var col = $scope.col;
1058 //Trying to recreate this sort of thing but it was getting messy having it in the template.
1059 //Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''}
1060 var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none);
1061 var label = sortDirectionText;
1063 if ($scope.isSortPriorityVisible()) {
1064 label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority;
1069 $scope.grid = uiGridCtrl.grid;
1071 $scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId];
1073 var initColClass = $scope.col.getColClass(false);
1074 $elm.addClass(initColClass);
1076 // Hide the menu by default
1077 $scope.menuShown = false;
1079 // Put asc and desc sort directions in scope
1080 $scope.asc = uiGridConstants.ASC;
1081 $scope.desc = uiGridConstants.DESC;
1083 // Store a reference to menu element
1084 var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') );
1086 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
1089 // apply any headerCellClass
1094 var filterDeregisters = [];
1098 * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart).
1099 * Once we have a down event, we need to work out whether we have a click, a drag, or a
1100 * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
1101 * we ignore it. A hold would open the menu.
1103 * So, on down event, we put in place handlers for move and up events, and a timer. If the
1104 * timer expires before we see a move or up, then we have a long press and hence a column menu open.
1105 * If the up happens before the timer, then we have a click, and we sort if the column is sortable.
1106 * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature
1109 * To deal with touch enabled devices that also have mice, we only create our handlers when
1110 * we get the down event, and we create the corresponding handlers - if we're touchstart then
1111 * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup.
1113 * We also suppress the click action whilst this is happening - otherwise after the mouseup there
1114 * will be a click event and that can cause the column menu to close
1118 $scope.downFn = function( event ){
1119 event.stopPropagation();
1121 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
1122 event = event.originalEvent;
1125 // Don't show the menu if it's not the left button
1126 if (event.button && event.button !== 0) {
1129 previousMouseX = event.pageX;
1131 $scope.mousedownStartTime = (new Date()).getTime();
1132 $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout);
1134 $scope.mousedownTimeout.then(function () {
1135 if ( $scope.colMenu ) {
1136 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event);
1140 uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name});
1142 $scope.offAllEvents();
1143 if ( event.type === 'touchstart'){
1144 $document.on('touchend', $scope.upFn);
1145 $document.on('touchmove', $scope.moveFn);
1146 } else if ( event.type === 'mousedown' ){
1147 $document.on('mouseup', $scope.upFn);
1148 $document.on('mousemove', $scope.moveFn);
1152 $scope.upFn = function( event ){
1153 event.stopPropagation();
1154 $timeout.cancel($scope.mousedownTimeout);
1155 $scope.offAllEvents();
1156 $scope.onDownEvents(event.type);
1158 var mousedownEndTime = (new Date()).getTime();
1159 var mousedownTime = mousedownEndTime - $scope.mousedownStartTime;
1161 if (mousedownTime > mousedownTimeout) {
1162 // long click, handled above with mousedown
1166 if ( $scope.sortable ){
1167 $scope.handleClick(event);
1172 $scope.moveFn = function( event ){
1173 // Chrome is known to fire some bogus move events.
1174 var changeValue = event.pageX - previousMouseX;
1175 if ( changeValue === 0 ){ return; }
1177 // we're a move, so do nothing and leave for column move (if enabled) to take over
1178 $timeout.cancel($scope.mousedownTimeout);
1179 $scope.offAllEvents();
1180 $scope.onDownEvents(event.type);
1183 $scope.clickFn = function ( event ){
1184 event.stopPropagation();
1185 $contentsElm.off('click', $scope.clickFn);
1189 $scope.offAllEvents = function(){
1190 $contentsElm.off('touchstart', $scope.downFn);
1191 $contentsElm.off('mousedown', $scope.downFn);
1193 $document.off('touchend', $scope.upFn);
1194 $document.off('mouseup', $scope.upFn);
1196 $document.off('touchmove', $scope.moveFn);
1197 $document.off('mousemove', $scope.moveFn);
1199 $contentsElm.off('click', $scope.clickFn);
1202 $scope.onDownEvents = function( type ){
1203 // If there is a previous event, then wait a while before
1204 // activating the other mode - i.e. if the last event was a touch event then
1205 // don't enable mouse events for a wee while (500ms or so)
1206 // Avoids problems with devices that emulate mouse events when you have touch events
1211 $contentsElm.on('click', $scope.clickFn);
1212 $contentsElm.on('touchstart', $scope.downFn);
1213 $timeout(function(){
1214 $contentsElm.on('mousedown', $scope.downFn);
1215 }, changeModeTimeout);
1219 $contentsElm.on('click', $scope.clickFn);
1220 $contentsElm.on('mousedown', $scope.downFn);
1221 $timeout(function(){
1222 $contentsElm.on('touchstart', $scope.downFn);
1223 }, changeModeTimeout);
1226 $contentsElm.on('click', $scope.clickFn);
1227 $contentsElm.on('touchstart', $scope.downFn);
1228 $contentsElm.on('mousedown', $scope.downFn);
1233 var updateHeaderOptions = function( grid ){
1234 var contents = $elm;
1236 contents.removeClass( classAdded );
1240 if (angular.isFunction($scope.col.headerCellClass)) {
1241 classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
1244 classAdded = $scope.col.headerCellClass;
1246 contents.addClass(classAdded);
1248 $timeout(function (){
1249 var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body'];
1250 $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] );
1253 // Figure out whether this column is sortable or not
1254 if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) {
1255 $scope.sortable = true;
1258 $scope.sortable = false;
1261 // Figure out whether this column is filterable or not
1262 var oldFilterable = $scope.filterable;
1263 if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) {
1264 $scope.filterable = true;
1267 $scope.filterable = false;
1270 if ( oldFilterable !== $scope.filterable){
1271 if ( typeof($scope.col.updateFilters) !== 'undefined' ){
1272 $scope.col.updateFilters($scope.filterable);
1275 // if column is filterable add a filter watcher
1276 if ($scope.filterable) {
1277 $scope.col.filters.forEach( function(filter, i) {
1278 filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) {
1280 uiGridCtrl.grid.api.core.raise.filterChanged();
1281 uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1282 uiGridCtrl.grid.queueGridRefresh();
1286 $scope.$on('$destroy', function() {
1287 filterDeregisters.forEach( function(filterDeregister) {
1292 filterDeregisters.forEach( function(filterDeregister) {
1299 // figure out whether we support column menus
1300 if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
1301 $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){
1302 $scope.colMenu = true;
1304 $scope.colMenu = false;
1309 * @name enableColumnMenu
1310 * @propertyOf ui.grid.class:GridOptions.columnDef
1311 * @description if column menus are enabled, controls the column menus for this specific
1312 * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus
1313 * using this option. If gridOptions.enableColumnMenus === false then you get no column
1314 * menus irrespective of the value of this option ). Defaults to true.
1319 * @name enableColumnMenus
1320 * @propertyOf ui.grid.class:GridOptions.columnDef
1321 * @description Override for column menus everywhere - if set to false then you get no
1322 * column menus. Defaults to true.
1326 $scope.offAllEvents();
1328 if ($scope.sortable || $scope.colMenu) {
1329 $scope.onDownEvents();
1331 $scope.$on('$destroy', function () {
1332 $scope.offAllEvents();
1338 $scope.$watch('col', function (n, o) {
1340 // See if the column's internal class has changed
1341 var newColClass = $scope.col.getColClass(false);
1342 if (newColClass !== initColClass) {
1343 $elm.removeClass(initColClass);
1344 $elm.addClass(newColClass);
1345 initColClass = newColClass;
1350 updateHeaderOptions();
1352 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
1353 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]);
1355 $scope.$on( '$destroy', dataChangeDereg );
1357 $scope.handleClick = function(event) {
1358 // If the shift key is being held down, add this column to the sort
1360 if (event.shiftKey) {
1364 // Sort this column then rebuild the grid's rows
1365 uiGridCtrl.grid.sortColumn($scope.col, add)
1367 if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
1368 uiGridCtrl.grid.refresh();
1373 $scope.toggleMenu = function(event) {
1374 event.stopPropagation();
1376 // If the menu is already showing...
1377 if (uiGridCtrl.columnMenuScope.menuShown) {
1378 // ... and we're the column the menu is on...
1379 if (uiGridCtrl.columnMenuScope.col === $scope.col) {
1381 uiGridCtrl.columnMenuScope.hideMenu();
1383 // ... and we're NOT the column the menu is on
1385 // ... move the menu to our column
1386 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1389 // If the menu is NOT showing
1391 // ... show it on our column
1392 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1400 return uiGridHeaderCell;
1408 angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent',
1409 function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) {
1410 var defaultTemplate = 'ui-grid/ui-grid-header';
1411 var emptyTemplate = 'ui-grid/ui-grid-no-header';
1415 // templateUrl: 'ui-grid/ui-grid-header',
1418 require: ['^uiGrid', '^uiGridRenderContainer'],
1420 compile: function($elm, $attrs) {
1422 pre: function ($scope, $elm, $attrs, controllers) {
1423 var uiGridCtrl = controllers[0];
1424 var containerCtrl = controllers[1];
1426 $scope.grid = uiGridCtrl.grid;
1427 $scope.colContainer = containerCtrl.colContainer;
1429 updateHeaderReferences();
1432 if (!$scope.grid.options.showHeader) {
1433 headerTemplate = emptyTemplate;
1436 headerTemplate = ($scope.grid.options.headerTemplate) ? $scope.grid.options.headerTemplate : defaultTemplate;
1439 gridUtil.getTemplate(headerTemplate)
1440 .then(function (contents) {
1441 var template = angular.element(contents);
1443 var newElm = $compile(template)($scope);
1444 $elm.replaceWith(newElm);
1446 // And update $elm to be the new element
1449 updateHeaderReferences();
1451 if (containerCtrl) {
1452 // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
1453 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1456 if (headerViewport) {
1457 containerCtrl.headerViewport = headerViewport;
1458 angular.element(headerViewport).on('scroll', scrollHandler);
1459 $scope.$on('$destroy', function () {
1460 angular.element(headerViewport).off('scroll', scrollHandler);
1465 $scope.grid.queueRefresh();
1468 function updateHeaderReferences() {
1469 containerCtrl.header = containerCtrl.colContainer.header = $elm;
1471 var headerCanvases = $elm[0].getElementsByClassName('ui-grid-header-canvas');
1473 if (headerCanvases.length > 0) {
1474 containerCtrl.headerCanvas = containerCtrl.colContainer.headerCanvas = headerCanvases[0];
1477 containerCtrl.headerCanvas = null;
1481 function scrollHandler(evt) {
1482 if (uiGridCtrl.grid.isScrollingHorizontally) {
1485 var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid);
1486 var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft);
1488 var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll);
1489 scrollEvent.newScrollLeft = newScrollLeft;
1490 if ( horizScrollPercentage > -1 ){
1491 scrollEvent.x = { percentage: horizScrollPercentage };
1494 uiGridCtrl.grid.scrollContainers(null, scrollEvent);
1498 post: function ($scope, $elm, $attrs, controllers) {
1499 var uiGridCtrl = controllers[0];
1500 var containerCtrl = controllers[1];
1502 // gridUtil.logDebug('ui-grid-header link');
1504 var grid = uiGridCtrl.grid;
1506 // Don't animate header cells
1507 gridUtil.disableAnimations($elm);
1509 function updateColumnWidths() {
1510 // this styleBuilder always runs after the renderContainer, so we can rely on the column widths
1511 // already being populated correctly
1513 var columnCache = containerCtrl.colContainer.visibleColumnCache;
1516 // uiGridCtrl.grid.columns.forEach(function (column) {
1518 var canvasWidth = 0;
1519 columnCache.forEach(function (column) {
1520 ret = ret + column.getColClassDefinition();
1521 canvasWidth += column.drawnWidth;
1524 containerCtrl.colContainer.canvasWidth = canvasWidth;
1526 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
1530 containerCtrl.header = $elm;
1532 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1533 if (headerViewport) {
1534 containerCtrl.headerViewport = headerViewport;
1537 //todo: remove this if by injecting gridCtrl into unit tests
1539 uiGridCtrl.grid.registerStyleComputation({
1541 func: updateColumnWidths
1554 angular.module('ui.grid')
1555 .service('uiGridGridMenuService', [ 'gridUtil', 'i18nService', 'uiGridConstants', function( gridUtil, i18nService, uiGridConstants ) {
1558 * @name ui.grid.gridMenuService
1560 * @description Methods for working with the grid menu
1566 * @methodOf ui.grid.gridMenuService
1568 * @description Sets up the gridMenu. Most importantly, sets our
1569 * scope onto the grid object as grid.gridMenuScope, allowing us
1570 * to operate when passed only the grid. Second most importantly,
1571 * we register the 'addToGridMenu' and 'removeFromGridMenu' methods
1573 * @param {$scope} $scope the scope of this gridMenu
1574 * @param {Grid} grid the grid to which this gridMenu is associated
1576 initialize: function( $scope, grid ){
1577 grid.gridMenuScope = $scope;
1579 $scope.registeredMenuItems = [];
1581 // not certain this is needed, but would be bad to create a memory leak
1582 $scope.$on('$destroy', function() {
1583 if ( $scope.grid && $scope.grid.gridMenuScope ){
1584 $scope.grid.gridMenuScope = null;
1589 if ( $scope.registeredMenuItems ){
1590 $scope.registeredMenuItems = null;
1594 $scope.registeredMenuItems = [];
1598 * @name addToGridMenu
1599 * @methodOf ui.grid.core.api:PublicApi
1600 * @description add items to the grid menu. Used by features
1601 * to add their menu items if they are enabled, can also be used by
1602 * end users to add menu items. This method has the advantage of allowing
1603 * remove again, which can simplify management of which items are included
1604 * in the menu when. (Noting that in most cases the shown and active functions
1605 * provide a better way to handle visibility of menu items)
1606 * @param {Grid} grid the grid on which we are acting
1607 * @param {array} items menu items in the format as described in the tutorial, with
1608 * the added note that if you want to use remove you must also specify an `id` field,
1609 * which is provided when you want to remove an item. The id should be unique.
1612 grid.api.registerMethod( 'core', 'addToGridMenu', service.addToGridMenu );
1616 * @name removeFromGridMenu
1617 * @methodOf ui.grid.core.api:PublicApi
1618 * @description Remove an item from the grid menu based on a provided id. Assumes
1619 * that the id is unique, removes only the last instance of that id. Does nothing if
1620 * the specified id is not found
1621 * @param {Grid} grid the grid on which we are acting
1622 * @param {string} id the id we'd like to remove from the menu
1625 grid.api.registerMethod( 'core', 'removeFromGridMenu', service.removeFromGridMenu );
1631 * @name addToGridMenu
1632 * @propertyOf ui.grid.gridMenuService
1633 * @description add items to the grid menu. Used by features
1634 * to add their menu items if they are enabled, can also be used by
1635 * end users to add menu items. This method has the advantage of allowing
1636 * remove again, which can simplify management of which items are included
1637 * in the menu when. (Noting that in most cases the shown and active functions
1638 * provide a better way to handle visibility of menu items)
1639 * @param {Grid} grid the grid on which we are acting
1640 * @param {array} items menu items in the format as described in the tutorial, with
1641 * the added note that if you want to use remove you must also specify an `id` field,
1642 * which is provided when you want to remove an item. The id should be unique.
1645 addToGridMenu: function( grid, menuItems ) {
1646 if ( !angular.isArray( menuItems ) ) {
1647 gridUtil.logError( 'addToGridMenu: menuItems must be an array, and is not, not adding any items');
1649 if ( grid.gridMenuScope ){
1650 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems ? grid.gridMenuScope.registeredMenuItems : [];
1651 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems.concat( menuItems );
1653 gridUtil.logError( 'Asked to addToGridMenu, but gridMenuScope not present. Timing issue? Please log issue with ui-grid');
1661 * @name removeFromGridMenu
1662 * @methodOf ui.grid.gridMenuService
1663 * @description Remove an item from the grid menu based on a provided id. Assumes
1664 * that the id is unique, removes only the last instance of that id. Does nothing if
1665 * the specified id is not found. If there is no gridMenuScope or registeredMenuItems
1666 * then do nothing silently - the desired result is those menu items not be present and they
1668 * @param {Grid} grid the grid on which we are acting
1669 * @param {string} id the id we'd like to remove from the menu
1672 removeFromGridMenu: function( grid, id ){
1673 var foundIndex = -1;
1675 if ( grid && grid.gridMenuScope ){
1676 grid.gridMenuScope.registeredMenuItems.forEach( function( value, index ) {
1677 if ( value.id === id ){
1678 if (foundIndex > -1) {
1679 gridUtil.logError( 'removeFromGridMenu: found multiple items with the same id, removing only the last' );
1688 if ( foundIndex > -1 ){
1689 grid.gridMenuScope.registeredMenuItems.splice( foundIndex, 1 );
1696 * @name gridMenuCustomItems
1697 * @propertyOf ui.grid.class:GridOptions
1698 * @description (optional) An array of menu items that should be added to
1699 * the gridMenu. Follow the format documented in the tutorial for column
1700 * menu customisation. The context provided to the action function will
1701 * include context.grid. An alternative if working with dynamic menus is to use the
1702 * provided api - core.addToGridMenu and core.removeFromGridMenu, which handles
1703 * some of the management of items for you.
1708 * @name gridMenuShowHideColumns
1709 * @propertyOf ui.grid.class:GridOptions
1710 * @description true by default, whether the grid menu should allow hide/show
1716 * @methodOf ui.grid.gridMenuService
1717 * @name getMenuItems
1718 * @description Decides the menu items to show in the menu. This is a
1721 * - the default menu items that are always included,
1722 * - any menu items that have been provided through the addMenuItem api. These
1723 * are typically added by features within the grid
1724 * - any menu items included in grid.options.gridMenuCustomItems. These can be
1725 * changed dynamically, as they're always recalculated whenever we show the
1727 * @param {$scope} $scope the scope of this gridMenu, from which we can find all
1728 * the information that we need
1729 * @returns {array} an array of menu items that can be shown
1731 getMenuItems: function( $scope ) {
1733 // this is where we add any menu items we want to always include
1736 if ( $scope.grid.options.gridMenuCustomItems ){
1737 if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){
1738 gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not');
1740 menuItems = menuItems.concat( $scope.grid.options.gridMenuCustomItems );
1744 var clearFilters = [{
1745 title: i18nService.getSafeText('gridMenu.clearAllFilters'),
1746 action: function ($event) {
1747 $scope.grid.clearAllFilters(undefined, true, undefined);
1750 return $scope.grid.options.enableFiltering;
1754 menuItems = menuItems.concat( clearFilters );
1756 menuItems = menuItems.concat( $scope.registeredMenuItems );
1758 if ( $scope.grid.options.gridMenuShowHideColumns !== false ){
1759 menuItems = menuItems.concat( service.showHideColumns( $scope ) );
1762 menuItems.sort(function(a, b){
1763 return a.order - b.order;
1772 * @name gridMenuTitleFilter
1773 * @propertyOf ui.grid.class:GridOptions
1774 * @description (optional) A function that takes a title string
1775 * (usually the col.displayName), and converts it into a display value. The function
1776 * must return either a string or a promise.
1778 * Used for internationalization of the grid menu column names - for angular-translate
1779 * you can pass $translate as the function, for i18nService you can pass getSafeText as the
1784 * gridMenuTitleFilter: $translate
1790 * @methodOf ui.grid.gridMenuService
1791 * @name showHideColumns
1792 * @description Adds two menu items for each of the columns in columnDefs. One
1793 * menu item for hide, one menu item for show. Each is visible when appropriate
1794 * (show when column is not visible, hide when column is visible). Each toggles
1795 * the visible property on the columnDef using toggleColumnVisibility
1796 * @param {$scope} $scope of a gridMenu, which contains a reference to the grid
1798 showHideColumns: function( $scope ){
1799 var showHideColumns = [];
1800 if ( !$scope.grid.options.columnDefs || $scope.grid.options.columnDefs.length === 0 || $scope.grid.columns.length === 0 ) {
1801 return showHideColumns;
1804 // add header for columns
1805 showHideColumns.push({
1806 title: i18nService.getSafeText('gridMenu.columns'),
1810 $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; };
1812 $scope.grid.options.columnDefs.forEach( function( colDef, index ){
1813 if ( colDef.enableHiding !== false ){
1814 // add hide menu item - shows an OK icon as we only show when column is already visible
1816 icon: 'ui-grid-icon-ok',
1817 action: function($event) {
1818 $event.stopPropagation();
1819 service.toggleColumnVisibility( this.context.gridCol );
1822 return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined;
1824 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1826 order: 301 + index * 2
1828 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1829 showHideColumns.push( menuItem );
1831 // add show menu item - shows no icon as we only show when column is invisible
1833 icon: 'ui-grid-icon-cancel',
1834 action: function($event) {
1835 $event.stopPropagation();
1836 service.toggleColumnVisibility( this.context.gridCol );
1839 return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined);
1841 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1843 order: 301 + index * 2 + 1
1845 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1846 showHideColumns.push( menuItem );
1849 return showHideColumns;
1855 * @methodOf ui.grid.gridMenuService
1856 * @name setMenuItemTitle
1857 * @description Handles the response from gridMenuTitleFilter, adding it directly to the menu
1858 * item if it returns a string, otherwise waiting for the promise to resolve or reject then
1859 * putting the result into the title
1860 * @param {object} menuItem the menuItem we want to put the title on
1861 * @param {object} colDef the colDef from which we can get displayName, name or field
1862 * @param {Grid} grid the grid, from which we can get the options.gridMenuTitleFilter
1865 setMenuItemTitle: function( menuItem, colDef, grid ){
1866 var title = grid.options.gridMenuTitleFilter( colDef.displayName || gridUtil.readableColumnName(colDef.name) || colDef.field );
1868 if ( typeof(title) === 'string' ){
1869 menuItem.title = title;
1870 } else if ( title.then ){
1871 // must be a promise
1872 menuItem.title = "";
1873 title.then( function( successValue ) {
1874 menuItem.title = successValue;
1875 }, function( errorValue ) {
1876 menuItem.title = errorValue;
1879 gridUtil.logError('Expected gridMenuTitleFilter to return a string or a promise, it has returned neither, bad config');
1880 menuItem.title = 'badconfig';
1886 * @methodOf ui.grid.gridMenuService
1887 * @name toggleColumnVisibility
1888 * @description Toggles the visibility of an individual column. Expects to be
1889 * provided a context that has on it a gridColumn, which is the column that
1890 * we'll operate upon. We change the visibility, and refresh the grid as appropriate
1891 * @param {GridCol} gridCol the column that we want to toggle
1894 toggleColumnVisibility: function( gridCol ) {
1895 gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined );
1897 gridCol.grid.refresh();
1898 gridCol.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1899 gridCol.grid.api.core.raise.columnVisibilityChanged( gridCol );
1908 .directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 'i18nService',
1909 function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) {
1914 require: ['^uiGrid'],
1915 templateUrl: 'ui-grid/ui-grid-menu-button',
1918 link: function ($scope, $elm, $attrs, controllers) {
1919 var uiGridCtrl = controllers[0];
1921 // For the aria label
1923 aria: i18nService.getSafeText('gridMenu.aria')
1926 uiGridGridMenuService.initialize($scope, uiGridCtrl.grid);
1928 $scope.shown = false;
1930 $scope.toggleMenu = function () {
1931 if ( $scope.shown ){
1932 $scope.$broadcast('hide-menu');
1933 $scope.shown = false;
1935 $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope );
1936 $scope.$broadcast('show-menu');
1937 $scope.shown = true;
1941 $scope.$on('menu-hidden', function() {
1942 $scope.shown = false;
1943 gridUtil.focus.bySelector($elm, '.ui-grid-icon-container');
1956 * @name ui.grid.directive:uiGridMenu
1961 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
1964 <doc:example module="app">
1967 var app = angular.module('app', ['ui.grid']);
1969 app.controller('MainCtrl', ['$scope', function ($scope) {
1974 <div ng-controller="MainCtrl">
1975 <div ui-grid-menu shown="true" ></div>
1982 angular.module('ui.grid')
1984 .directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'i18nService',
1985 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18nService) {
1993 require: '?^uiGrid',
1994 templateUrl: 'ui-grid/uiGridMenu',
1996 link: function ($scope, $elm, $attrs, uiGridCtrl) {
1997 var gridMenuMaxHeight;
1999 $scope.dynamicStyles = '';
2002 // magic number of 30 because the grid menu displays somewhat below
2003 // the top of the grid. It is approximately 30px.
2004 gridMenuMaxHeight = uiGridCtrl.grid.gridHeight - 30;
2005 $scope.dynamicStyles = [
2006 '.grid' + uiGridCtrl.grid.id + ' .ui-grid-menu-mid {',
2007 'max-height: ' + gridMenuMaxHeight + 'px;',
2013 close: i18nService.getSafeText('columnMenu.close')
2016 // *** Show/Hide functions ******
2017 $scope.showMenu = function(event, args) {
2018 if ( !$scope.shown ){
2021 * In order to animate cleanly we remove the ng-if, wait a digest cycle, then
2022 * animate the removal of the ng-hide. We can't successfully (so far as I can tell)
2023 * animate removal of the ng-if, as the menu items aren't there yet. And we don't want
2024 * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated
2027 * Note when testing animation that animations don't run on the tutorials. When debugging it looks
2028 * like they do, but angular has a default $animate provider that is just a stub, and that's what's
2029 * being called. ALso don't be fooled by the fact that your browser has actually loaded the
2030 * angular-translate.js, it's not using it. You need to test animations in an external application.
2032 $scope.shown = true;
2034 $timeout( function() {
2035 $scope.shownMid = true;
2036 $scope.$emit('menu-shown');
2038 } else if ( !$scope.shownMid ) {
2039 // we're probably doing a hide then show, so we don't need to wait for ng-if
2040 $scope.shownMid = true;
2041 $scope.$emit('menu-shown');
2044 var docEventType = 'click';
2045 if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') {
2046 docEventType = args.originalEvent.type;
2049 // Turn off an existing document click handler
2050 angular.element(document).off('click touchstart', applyHideMenu);
2051 $elm.off('keyup', checkKeyUp);
2052 $elm.off('keydown', checkKeyDown);
2054 // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one
2055 $timeout(function() {
2056 angular.element(document).on(docEventType, applyHideMenu);
2057 $elm.on('keyup', checkKeyUp);
2058 $elm.on('keydown', checkKeyDown);
2061 //automatically set the focus to the first button element in the now open menu.
2062 gridUtil.focus.bySelector($elm, 'button[type=button]', true);
2066 $scope.hideMenu = function(event) {
2067 if ( $scope.shown ){
2069 * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to
2070 * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the
2071 * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason.
2073 * The user may have clicked on the menu again whilst
2074 * we're waiting, so we check that the mid isn't shown before applying the ng-if.
2076 $scope.shownMid = false;
2077 $timeout( function() {
2078 if ( !$scope.shownMid ){
2079 $scope.shown = false;
2080 $scope.$emit('menu-hidden');
2085 angular.element(document).off('click touchstart', applyHideMenu);
2086 $elm.off('keyup', checkKeyUp);
2087 $elm.off('keydown', checkKeyDown);
2090 $scope.$on('hide-menu', function (event, args) {
2091 $scope.hideMenu(event, args);
2094 $scope.$on('show-menu', function (event, args) {
2095 $scope.showMenu(event, args);
2099 // *** Auto hide when click elsewhere ******
2100 var applyHideMenu = function(){
2102 $scope.$apply(function () {
2108 // close menu on ESC and keep tab cyclical
2109 var checkKeyUp = function(event) {
2110 if (event.keyCode === 27) {
2115 var checkKeyDown = function(event) {
2116 var setFocus = function(elm) {
2118 event.preventDefault();
2121 if (event.keyCode === 9) {
2122 var firstMenuItem, lastMenuItem;
2123 var menuItemButtons = $elm[0].querySelectorAll('button:not(.ng-hide)');
2124 if (menuItemButtons.length > 0) {
2125 firstMenuItem = menuItemButtons[0];
2126 lastMenuItem = menuItemButtons[menuItemButtons.length - 1];
2127 if (event.target === lastMenuItem && !event.shiftKey) {
2128 setFocus(firstMenuItem);
2129 } else if (event.target === firstMenuItem && event.shiftKey) {
2130 setFocus(lastMenuItem);
2136 if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) {
2137 $scope.autoHide = true;
2140 if ($scope.autoHide) {
2141 angular.element($window).on('resize', applyHideMenu);
2144 $scope.$on('$destroy', function () {
2145 angular.element(document).off('click touchstart', applyHideMenu);
2149 $scope.$on('$destroy', function() {
2150 angular.element($window).off('resize', applyHideMenu);
2154 $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollBegin($scope, applyHideMenu ));
2157 $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu ));
2164 .directive('uiGridMenuItem', ['gridUtil', '$compile', 'i18nService', function (gridUtil, $compile, i18nService) {
2165 var uiGridMenuItem = {
2176 screenReaderOnly: '='
2178 require: ['?^uiGrid'],
2179 templateUrl: 'ui-grid/uiGridMenuItem',
2181 compile: function() {
2183 pre: function ($scope, $elm) {
2184 if ($scope.templateUrl) {
2185 gridUtil.getTemplate($scope.templateUrl)
2186 .then(function (contents) {
2187 var template = angular.element(contents);
2189 var newElm = $compile(template)($scope);
2190 $elm.replaceWith(newElm);
2194 post: function ($scope, $elm, $attrs, controllers) {
2195 var uiGridCtrl = controllers[0];
2197 // TODO(c0bra): validate that shown and active are functions if they're defined. An exception is already thrown above this though
2198 // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') {
2199 // throw new TypeError("$scope.shown is defined but not a function");
2201 if (typeof($scope.shown) === 'undefined' || $scope.shown === null) {
2202 $scope.shown = function() { return true; };
2205 $scope.itemShown = function () {
2207 if ($scope.context) {
2208 context.context = $scope.context;
2211 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2212 context.grid = uiGridCtrl.grid;
2215 return $scope.shown.call(context);
2218 $scope.itemAction = function($event,title) {
2219 gridUtil.logDebug('itemAction');
2220 $event.stopPropagation();
2222 if (typeof($scope.action) === 'function') {
2225 if ($scope.context) {
2226 context.context = $scope.context;
2229 // Add the grid to the function call context if the uiGrid controller is present
2230 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2231 context.grid = uiGridCtrl.grid;
2234 $scope.action.call(context, $event, title);
2236 if ( !$scope.leaveOpen ){
2237 $scope.$emit('hide-menu');
2240 * XXX: Fix after column refactor
2241 * Ideally the focus would remain on the item.
2242 * However, since there are two menu items that have their 'show' property toggled instead. This is a quick fix.
2244 gridUtil.focus.bySelector(angular.element(gridUtil.closestElm($elm, ".ui-grid-menu-items")), 'button[type=button]', true);
2249 $scope.i18n = i18nService.get();
2255 return uiGridMenuItem;
2264 * @name ui.grid.directive:uiGridOneBind
2265 * @summary A group of directives that provide a one time bind to a dom element.
2266 * @description A group of directives that provide a one time bind to a dom element.
2267 * As one time bindings are not supported in Angular 1.2.* this directive provdes this capability.
2268 * This is done to reduce the number of watchers on the dom.
2270 * <h2>Short Example ({@link ui.grid.directive:uiGridOneBindSrc ui-grid-one-bind-src})</h2>
2272 <div ng-init="imageName = 'myImageDir.jpg'">
2273 <img ui-grid-one-bind-src="imageName"></img>
2278 <div ng-init="imageName = 'myImageDir.jpg'">
2279 <img ui-grid-one-bind-src="imageName" src="myImageDir.jpg"></img>
2283 <h2>Short Example ({@link ui.grid.directive:uiGridOneBindText ui-grid-one-bind-text})</h2>
2285 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text"></div>
2289 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text">Add this text</div>
2292 * <b>Note:</b> This behavior is slightly different for the {@link ui.grid.directive:uiGridOneBindIdGrid uiGridOneBindIdGrid}
2293 * and {@link ui.grid.directive:uiGridOneBindAriaLabelledbyGrid uiGridOneBindAriaLabelledbyGrid} directives.
2296 //https://github.com/joshkurz/Black-Belt-AngularJS-Directives/blob/master/directives/Optimization/oneBind.js
2297 var oneBinders = angular.module('ui.grid');
2301 * @name ui.grid.directive:uiGridOneBindSrc
2302 * @memberof ui.grid.directive:uiGridOneBind
2305 * @param {String} uiGridOneBindSrc The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2306 * @description One time binding for the src dom tag.
2309 {tag: 'Src', method: 'attr'},
2312 * @name ui.grid.directive:uiGridOneBindText
2315 * @param {String} uiGridOneBindText The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2316 * @description One time binding for the text dom tag.
2318 {tag: 'Text', method: 'text'},
2321 * @name ui.grid.directive:uiGridOneBindHref
2324 * @param {String} uiGridOneBindHref The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2325 * @description One time binding for the href dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2327 {tag: 'Href', method: 'attr'},
2330 * @name ui.grid.directive:uiGridOneBindClass
2333 * @param {String} uiGridOneBindClass The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2334 * @param {Object} uiGridOneBindClass The object that you want to bind. At least one of the values in the object must be something other than null or undefined for the watcher to be removed.
2335 * this is to prevent the watcher from being removed before the scope is initialized.
2336 * @param {Array} uiGridOneBindClass An array of classes to bind to this element.
2337 * @description One time binding for the class dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2339 {tag: 'Class', method: 'addClass'},
2342 * @name ui.grid.directive:uiGridOneBindHtml
2345 * @param {String} uiGridOneBindHtml The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2346 * @description One time binding for the html method on a dom element. For more information see {@link ui.grid.directive:uiGridOneBind}.
2348 {tag: 'Html', method: 'html'},
2351 * @name ui.grid.directive:uiGridOneBindAlt
2354 * @param {String} uiGridOneBindAlt The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2355 * @description One time binding for the alt dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2357 {tag: 'Alt', method: 'attr'},
2360 * @name ui.grid.directive:uiGridOneBindStyle
2363 * @param {String} uiGridOneBindStyle The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2364 * @description One time binding for the style dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2366 {tag: 'Style', method: 'css'},
2369 * @name ui.grid.directive:uiGridOneBindValue
2372 * @param {String} uiGridOneBindValue The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2373 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2375 {tag: 'Value', method: 'attr'},
2378 * @name ui.grid.directive:uiGridOneBindId
2381 * @param {String} uiGridOneBindId The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2382 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2384 {tag: 'Id', method: 'attr'},
2387 * @name ui.grid.directive:uiGridOneBindIdGrid
2390 * @param {String} uiGridOneBindIdGrid The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2391 * @description One time binding for the id dom tag.
2392 * <h1>Important Note!</h1>
2393 * If the id tag passed as a parameter does <b>not</b> contain the grid id as a substring
2394 * then the directive will search the scope and the parent controller (if it is a uiGridController) for the grid.id value.
2395 * If this value is found then it is appended to the begining of the id tag. If the grid is not found then the directive throws an error.
2396 * This is done in order to ensure uniqueness of id tags across the grid.
2397 * This is to prevent two grids in the same document having duplicate id tags.
2399 {tag: 'Id', directiveName:'IdGrid', method: 'attr', appendGridId: true},
2402 * @name ui.grid.directive:uiGridOneBindTitle
2405 * @param {String} uiGridOneBindTitle The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2406 * @description One time binding for the title dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2408 {tag: 'Title', method: 'attr'},
2411 * @name ui.grid.directive:uiGridOneBindAriaLabel
2414 * @param {String} uiGridOneBindAriaLabel The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2415 * @description One time binding for the aria-label dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2418 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text"></div>
2422 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text" aria-label="Add this text"></div>
2425 {tag: 'Label', method: 'attr', aria:true},
2428 * @name ui.grid.directive:uiGridOneBindAriaLabelledby
2431 * @param {String} uiGridOneBindAriaLabelledby The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2432 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2435 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId"></div>
2439 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId" aria-labelledby="gridID32"></div>
2442 {tag: 'Labelledby', method: 'attr', aria:true},
2445 * @name ui.grid.directive:uiGridOneBindAriaLabelledbyGrid
2448 * @param {String} uiGridOneBindAriaLabelledbyGrid The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2449 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2450 * Works somewhat like {@link ui.grid.directive:uiGridOneBindIdGrid} however this one supports a list of ids (seperated by a space) and will dynamically add the
2451 * grid id to each one.
2454 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId"></div>
2456 * Will become ([grid.id] will be replaced by the actual grid id):
2458 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId" aria-labelledby-Grid="[grid.id]-gridID32"></div>
2461 {tag: 'Labelledby', directiveName:'LabelledbyGrid', appendGridId:true, method: 'attr', aria:true},
2464 * @name ui.grid.directive:uiGridOneBindAriaDescribedby
2467 * @param {String} uiGridOneBindAriaDescribedby The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2468 * @description One time binding for the aria-describedby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2471 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId"></div>
2475 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId" aria-describedby="gridID32"></div>
2478 {tag: 'Describedby', method: 'attr', aria:true},
2481 * @name ui.grid.directive:uiGridOneBindAriaDescribedbyGrid
2484 * @param {String} uiGridOneBindAriaDescribedbyGrid The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2485 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2486 * Works somewhat like {@link ui.grid.directive:uiGridOneBindIdGrid} however this one supports a list of ids (seperated by a space) and will dynamically add the
2487 * grid id to each one.
2490 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId"></div>
2492 * Will become ([grid.id] will be replaced by the actual grid id):
2494 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId" aria-describedby="[grid.id]-gridID32"></div>
2497 {tag: 'Describedby', directiveName:'DescribedbyGrid', appendGridId:true, method: 'attr', aria:true}],
2500 var baseDirectiveName = 'uiGridOneBind';
2501 //If it is an aria tag then append the aria label seperately
2502 //This is done because the aria tags are formatted aria-* and the directive name can't have a '-' character in it.
2503 //If the diretiveName has to be overridden then it does so here. This is because the tag being modified and the directive sometimes don't match up.
2504 var directiveName = (v.aria ? baseDirectiveName + 'Aria' : baseDirectiveName) + (v.directiveName ? v.directiveName : v.tag);
2505 oneBinders.directive(directiveName, ['gridUtil', function(gridUtil){
2508 require: ['?uiGrid','?^uiGrid'],
2509 link: function(scope, iElement, iAttrs, controllers){
2510 /* Appends the grid id to the beginnig of the value. */
2511 var appendGridId = function(val){
2512 var grid; //Get an instance of the grid if its available
2513 //If its available in the scope then we don't need to try to find it elsewhere
2517 //Another possible location to try to find the grid
2518 else if (scope.col && scope.col.grid){
2519 grid = scope.col.grid;
2521 //Last ditch effort: Search through the provided controllers.
2522 else if (!controllers.some( //Go through the controllers till one has the element we need
2523 function(controller){
2524 if (controller && controller.grid) {
2525 grid = controller.grid;
2526 return true; //We've found the grid
2529 //We tried our best to find it for you
2530 gridUtil.logError("["+directiveName+"] A valid grid could not be found to bind id. Are you using this directive " +
2531 "within the correct scope? Trying to generate id: [gridID]-" + val);
2532 throw new Error("No valid grid could be found");
2536 var idRegex = new RegExp(grid.id.toString());
2537 //If the grid id hasn't been appended already in the template declaration
2538 if (!idRegex.test(val)){
2539 val = grid.id.toString() + '-' + val;
2545 // The watch returns a function to remove itself.
2546 var rmWatcher = scope.$watch(iAttrs[directiveName], function(newV){
2548 //If we are trying to add an id element then we also apply the grid id if it isn't already there
2549 if (v.appendGridId) {
2550 var newIdString = null;
2551 //Append the id to all of the new ids.
2552 angular.forEach( newV.split(' '), function(s){
2553 newIdString = (newIdString ? (newIdString + ' ') : '') + appendGridId(s);
2558 // Append this newValue to the dom element.
2560 case 'attr': //The attr method takes two paraams the tag and the value
2562 //If it is an aria element then append the aria prefix
2563 iElement[v.method]('aria-' + v.tag.toLowerCase(),newV);
2565 iElement[v.method](v.tag.toLowerCase(),newV);
2569 //Pulled from https://github.com/Pasvaz/bindonce/blob/master/bindonce.js
2570 if (angular.isObject(newV) && !angular.isArray(newV)) {
2572 var nonNullFound = false; //We don't want to remove the binding unless the key is actually defined
2573 angular.forEach(newV, function (value, index) {
2574 if (value !== null && typeof(value) !== "undefined"){
2575 nonNullFound = true; //A non null value for a key was found so the object must have been initialized
2576 if (value) {results.push(index);}
2579 //A non null value for a key wasn't found so assume that the scope values haven't been fully initialized
2581 return; // If not initialized then the watcher should not be removed yet.
2587 iElement.addClass(angular.isArray(newV) ? newV.join(' ') : newV);
2593 iElement[v.method](newV);
2597 //Removes the watcher on itself after the bind
2600 // True ensures that equality is determined using angular.equals instead of ===
2601 }, true); //End rm watchers
2602 } //End compile function
2603 }; //End directive return
2604 } // End directive function
2606 }); // End angular foreach
2612 var module = angular.module('ui.grid');
2614 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'uiGridConstants', 'gridUtil', 'ScrollEvent',
2615 function($timeout, $document, uiGridConstants, gridUtil, ScrollEvent) {
2619 templateUrl: 'ui-grid/uiGridRenderContainer',
2620 require: ['^uiGrid', 'uiGridRenderContainer'],
2623 rowContainerName: '=',
2624 colContainerName: '=',
2625 bindScrollHorizontal: '=',
2626 bindScrollVertical: '=',
2627 enableVerticalScrollbar: '=',
2628 enableHorizontalScrollbar: '='
2630 controller: 'uiGridRenderContainer as RenderContainer',
2631 compile: function () {
2633 pre: function prelink($scope, $elm, $attrs, controllers) {
2635 var uiGridCtrl = controllers[0];
2636 var containerCtrl = controllers[1];
2637 var grid = $scope.grid = uiGridCtrl.grid;
2639 // Verify that the render container for this element exists
2640 if (!$scope.rowContainerName) {
2641 throw "No row render container name specified";
2643 if (!$scope.colContainerName) {
2644 throw "No column render container name specified";
2647 if (!grid.renderContainers[$scope.rowContainerName]) {
2648 throw "Row render container '" + $scope.rowContainerName + "' is not registered.";
2650 if (!grid.renderContainers[$scope.colContainerName]) {
2651 throw "Column render container '" + $scope.colContainerName + "' is not registered.";
2654 var rowContainer = $scope.rowContainer = grid.renderContainers[$scope.rowContainerName];
2655 var colContainer = $scope.colContainer = grid.renderContainers[$scope.colContainerName];
2657 containerCtrl.containerId = $scope.containerId;
2658 containerCtrl.rowContainer = rowContainer;
2659 containerCtrl.colContainer = colContainer;
2661 post: function postlink($scope, $elm, $attrs, controllers) {
2663 var uiGridCtrl = controllers[0];
2664 var containerCtrl = controllers[1];
2666 var grid = uiGridCtrl.grid;
2667 var rowContainer = containerCtrl.rowContainer;
2668 var colContainer = containerCtrl.colContainer;
2669 var scrollTop = null;
2670 var scrollLeft = null;
2673 var renderContainer = grid.renderContainers[$scope.containerId];
2675 // Put the container name on this element as a class
2676 $elm.addClass('ui-grid-render-container-' + $scope.containerId);
2678 // Scroll the render container viewport when the mousewheel is used
2679 gridUtil.on.mousewheel($elm, function (event) {
2680 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.RenderContainerMouseWheel);
2681 if (event.deltaY !== 0) {
2682 var scrollYAmount = event.deltaY * -1 * event.deltaFactor;
2684 scrollTop = containerCtrl.viewport[0].scrollTop;
2686 // Get the scroll percentage
2687 scrollEvent.verticalScrollLength = rowContainer.getVerticalScrollLength();
2688 var scrollYPercentage = (scrollTop + scrollYAmount) / scrollEvent.verticalScrollLength;
2690 // If we should be scrolled 100%, make sure the scrollTop matches the maximum scroll length
2691 // Viewports that have "overflow: hidden" don't let the mousewheel scroll all the way to the bottom without this check
2692 if (scrollYPercentage >= 1 && scrollTop < scrollEvent.verticalScrollLength) {
2693 containerCtrl.viewport[0].scrollTop = scrollEvent.verticalScrollLength;
2696 // Keep scrollPercentage within the range 0-1.
2697 if (scrollYPercentage < 0) { scrollYPercentage = 0; }
2698 else if (scrollYPercentage > 1) { scrollYPercentage = 1; }
2700 scrollEvent.y = { percentage: scrollYPercentage, pixels: scrollYAmount };
2702 if (event.deltaX !== 0) {
2703 var scrollXAmount = event.deltaX * event.deltaFactor;
2705 // Get the scroll percentage
2706 scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport, grid);
2707 scrollEvent.horizontalScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
2708 var scrollXPercentage = (scrollLeft + scrollXAmount) / scrollEvent.horizontalScrollLength;
2710 // Keep scrollPercentage within the range 0-1.
2711 if (scrollXPercentage < 0) { scrollXPercentage = 0; }
2712 else if (scrollXPercentage > 1) { scrollXPercentage = 1; }
2714 scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount };
2717 // Let the parent container scroll if the grid is already at the top/bottom
2718 if ((event.deltaY !== 0 && (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop))) ||
2719 (event.deltaX !== 0 && (scrollEvent.atLeft(scrollLeft) || scrollEvent.atRight(scrollLeft)))) {
2720 //parent controller scrolls
2723 event.preventDefault();
2724 event.stopPropagation();
2725 scrollEvent.fireThrottledScrollingEvent('', scrollEvent);
2730 $elm.bind('$destroy', function() {
2731 $elm.unbind('keydown');
2733 ['touchstart', 'touchmove', 'touchend','keydown', 'wheel', 'mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'].forEach(function (eventName) {
2734 $elm.unbind(eventName);
2738 // TODO(c0bra): Handle resizing the inner canvas based on the number of elements
2742 var canvasWidth = colContainer.canvasWidth;
2743 var viewportWidth = colContainer.getViewportWidth();
2745 var canvasHeight = rowContainer.getCanvasHeight();
2747 //add additional height for scrollbar on left and right container
2748 //if ($scope.containerId !== 'body') {
2749 // canvasHeight -= grid.scrollbarHeight;
2752 var viewportHeight = rowContainer.getViewportHeight();
2753 //shorten the height to make room for a scrollbar placeholder
2754 if (colContainer.needsHScrollbarPlaceholder()) {
2755 viewportHeight -= grid.scrollbarHeight;
2758 var headerViewportWidth,
2759 footerViewportWidth;
2760 headerViewportWidth = footerViewportWidth = colContainer.getHeaderViewportWidth();
2762 // Set canvas dimensions
2763 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }';
2765 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2767 if (renderContainer.explicitHeaderCanvasHeight) {
2768 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }';
2771 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: inherit; }';
2774 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }';
2775 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }';
2777 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2778 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-viewport { width: ' + footerViewportWidth + 'px; }';
2783 uiGridCtrl.grid.registerStyleComputation({
2794 module.controller('uiGridRenderContainer', ['$scope', 'gridUtil', function ($scope, gridUtil) {
2803 angular.module('ui.grid').directive('uiGridRow', ['gridUtil', function(gridUtil) {
2807 // templateUrl: 'ui-grid/ui-grid-row',
2808 require: ['^uiGrid', '^uiGridRenderContainer'],
2811 //rowRenderIndex is added to scope to give the true visual index of the row to any directives that need it
2814 compile: function() {
2816 pre: function($scope, $elm, $attrs, controllers) {
2817 var uiGridCtrl = controllers[0];
2818 var containerCtrl = controllers[1];
2820 var grid = uiGridCtrl.grid;
2822 $scope.grid = uiGridCtrl.grid;
2823 $scope.colContainer = containerCtrl.colContainer;
2825 // Function for attaching the template to this scope
2826 var clonedElement, cloneScope;
2827 function compileTemplate() {
2828 $scope.row.getRowTemplateFn.then(function (compiledElementFn) {
2829 // var compiledElementFn = $scope.row.compiledElementFn;
2831 // Create a new scope for the contents of this row, so we can destroy it later if need be
2832 var newScope = $scope.$new();
2834 compiledElementFn(newScope, function (newElm, scope) {
2835 // If we already have a cloned element, we need to remove it and destroy its scope
2836 if (clonedElement) {
2837 clonedElement.remove();
2838 cloneScope.$destroy();
2841 // Empty the row and append the new element
2842 $elm.empty().append(newElm);
2844 // Save the new cloned element and scope
2845 clonedElement = newElm;
2846 cloneScope = newScope;
2851 // Initially attach the compiled template to this scope
2854 // If the row's compiled element function changes, we need to replace this element's contents with the new compiled template
2855 $scope.$watch('row.getRowTemplateFn', function (newFunc, oldFunc) {
2856 if (newFunc !== oldFunc) {
2861 post: function($scope, $elm, $attrs, controllers) {
2875 * @name ui.grid.directive:uiGridStyle
2880 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
2883 <doc:example module="app">
2886 var app = angular.module('app', ['ui.grid']);
2888 app.controller('MainCtrl', ['$scope', function ($scope) {
2889 $scope.myStyle = '.blah { border: 1px solid }';
2893 <div ng-controller="MainCtrl">
2894 <style ui-grid-style>{{ myStyle }}</style>
2895 <span class="blah">I am in a box.</span>
2899 it('should apply the right class to the element', function () {
2900 element(by.css('.blah')).getCssValue('border-top-width')
2902 expect(c).toContain('1px');
2910 angular.module('ui.grid').directive('uiGridStyle', ['gridUtil', '$interpolate', function(gridUtil, $interpolate) {
2914 // require: '?^uiGrid',
2915 link: function($scope, $elm, $attrs, uiGridCtrl) {
2916 // gridUtil.logDebug('ui-grid-style link');
2917 // if (uiGridCtrl === undefined) {
2918 // gridUtil.logWarn('[ui-grid-style link] uiGridCtrl is undefined!');
2921 var interpolateFn = $interpolate($elm.text(), true);
2923 if (interpolateFn) {
2924 $scope.$watch(interpolateFn, function(value) {
2929 // uiGridCtrl.recalcRowStyles = function() {
2930 // var offset = (scope.options.offsetTop || 0) - (scope.options.excessRows * scope.options.rowHeight);
2931 // var rowHeight = scope.options.rowHeight;
2934 // var rowStyleCount = uiGridCtrl.minRowsToRender() + (scope.options.excessRows * 2);
2935 // for (var i = 1; i <= rowStyleCount; i++) {
2936 // ret = ret + ' .grid' + scope.gridId + ' .ui-grid-row:nth-child(' + i + ') { top: ' + offset + 'px; }';
2937 // offset = offset + rowHeight;
2940 // scope.rowStyles = ret;
2943 // uiGridCtrl.styleComputions.push(uiGridCtrl.recalcRowStyles);
2954 angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', '$log',
2955 function(gridUtil, ScrollEvent, uiGridConstants, $log) {
2959 controllerAs: 'Viewport',
2960 templateUrl: 'ui-grid/uiGridViewport',
2961 require: ['^uiGrid', '^uiGridRenderContainer'],
2962 link: function($scope, $elm, $attrs, controllers) {
2963 // gridUtil.logDebug('viewport post-link');
2965 var uiGridCtrl = controllers[0];
2966 var containerCtrl = controllers[1];
2968 $scope.containerCtrl = containerCtrl;
2970 var rowContainer = containerCtrl.rowContainer;
2971 var colContainer = containerCtrl.colContainer;
2973 var grid = uiGridCtrl.grid;
2975 $scope.grid = uiGridCtrl.grid;
2977 // Put the containers in scope so we can get rows and columns from them
2978 $scope.rowContainer = containerCtrl.rowContainer;
2979 $scope.colContainer = containerCtrl.colContainer;
2981 // Register this viewport with its container
2982 containerCtrl.viewport = $elm;
2985 $elm.on('scroll', scrollHandler);
2987 var ignoreScroll = false;
2989 function scrollHandler(evt) {
2990 //Leaving in this commented code in case it can someday be used
2991 //It does improve performance, but because the horizontal scroll is normalized,
2992 // using this code will lead to the column header getting slightly out of line with columns
2994 //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) {
2995 // //don't ask for scrollTop if we just set it
2996 // ignoreScroll = false;
2999 //ignoreScroll = true;
3001 var newScrollTop = $elm[0].scrollTop;
3002 var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid);
3004 var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop);
3005 var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft);
3007 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll);
3008 scrollEvent.newScrollLeft = newScrollLeft;
3009 scrollEvent.newScrollTop = newScrollTop;
3010 if ( horizScrollPercentage > -1 ){
3011 scrollEvent.x = { percentage: horizScrollPercentage };
3014 if ( vertScrollPercentage > -1 ){
3015 scrollEvent.y = { percentage: vertScrollPercentage };
3018 grid.scrollContainers($scope.$parent.containerId, scrollEvent);
3021 if ($scope.$parent.bindScrollVertical) {
3022 grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll);
3025 if ($scope.$parent.bindScrollHorizontal) {
3026 grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll);
3027 grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader);
3028 grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter);
3031 function syncVerticalScroll(scrollEvent){
3032 containerCtrl.prevScrollArgs = scrollEvent;
3033 var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport);
3034 $elm[0].scrollTop = newScrollTop;
3038 function syncHorizontalScroll(scrollEvent){
3039 containerCtrl.prevScrollArgs = scrollEvent;
3040 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3041 $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3044 function syncHorizontalHeader(scrollEvent){
3045 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3046 if (containerCtrl.headerViewport) {
3047 containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3051 function syncHorizontalFooter(scrollEvent){
3052 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3053 if (containerCtrl.footerViewport) {
3054 containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3060 controller: ['$scope', function ($scope) {
3061 this.rowStyle = function (index) {
3062 var rowContainer = $scope.rowContainer;
3063 var colContainer = $scope.colContainer;
3067 if (index === 0 && rowContainer.currentTopRow !== 0) {
3068 // The row offset-top is just the height of the rows above the current top-most row, which are no longer rendered
3069 var hiddenRowWidth = (rowContainer.currentTopRow) * rowContainer.grid.options.rowHeight;
3071 // return { 'margin-top': hiddenRowWidth + 'px' };
3072 styles['margin-top'] = hiddenRowWidth + 'px';
3075 if (colContainer.currentFirstColumn !== 0) {
3076 if (colContainer.grid.isRTL()) {
3077 styles['margin-right'] = colContainer.columnOffset + 'px';
3080 styles['margin-left'] = colContainer.columnOffset + 'px';
3095 angular.module('ui.grid')
3096 .directive('uiGridVisible', function uiGridVisibleAction() {
3097 return function ($scope, $elm, $attr) {
3098 $scope.$watch($attr.uiGridVisible, function (visible) {
3099 // $elm.css('visibility', visible ? 'visible' : 'hidden');
3100 $elm[visible ? 'removeClass' : 'addClass']('ui-grid-invisible');
3109 angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants',
3110 '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile',
3111 function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants,
3112 $templateCache, gridClassFactory, $timeout, $parse, $compile) {
3113 // gridUtil.logDebug('ui-grid controller');
3117 self.grid = gridClassFactory.createGrid($scope.uiGrid);
3119 //assign $scope.$parent if appScope not already assigned
3120 self.grid.appScope = self.grid.appScope || $scope.$parent;
3122 $elm.addClass('grid' + self.grid.id);
3123 self.grid.rtl = gridUtil.getStyles($elm[0])['direction'] === 'rtl';
3126 // angular.extend(self.grid.options, );
3128 //all properties of grid are available on scope
3129 $scope.grid = self.grid;
3131 if ($attrs.uiGridColumns) {
3132 $attrs.$observe('uiGridColumns', function(value) {
3133 self.grid.options.columnDefs = value;
3134 self.grid.buildColumns()
3136 self.grid.preCompileCellTemplates();
3138 self.grid.refreshCanvas(true);
3144 // if fastWatch is set we watch only the length and the reference, not every individual object
3145 var deregFunctions = [];
3146 if (self.grid.options.fastWatch) {
3147 self.uiGrid = $scope.uiGrid;
3148 if (angular.isString($scope.uiGrid.data)) {
3149 deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) );
3150 deregFunctions.push( $scope.$parent.$watch(function() {
3151 if ( self.grid.appScope[$scope.uiGrid.data] ){
3152 return self.grid.appScope[$scope.uiGrid.data].length;
3156 }, dataWatchFunction) );
3158 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3159 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, function(){ dataWatchFunction($scope.uiGrid.data); }) );
3161 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3162 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, function(){ columnDefsWatchFunction($scope.uiGrid.columnDefs); }) );
3164 if (angular.isString($scope.uiGrid.data)) {
3165 deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) );
3167 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3169 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3173 function columnDefsWatchFunction(n, o) {
3175 self.grid.options.columnDefs = $scope.uiGrid.columnDefs;
3176 self.grid.buildColumns({ orderByColumnDefs: true })
3179 self.grid.preCompileCellTemplates();
3181 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.COLUMN);
3188 function dataWatchFunction(newData) {
3189 // gridUtil.logDebug('dataWatch fired');
3192 if ( self.grid.options.fastWatch ){
3193 if (angular.isString($scope.uiGrid.data)) {
3194 newData = self.grid.appScope[$scope.uiGrid.data];
3196 newData = $scope.uiGrid.data;
3200 mostRecentData = newData;
3203 // columns length is greater than the number of row header columns, which don't count because they're created automatically
3204 var hasColumns = self.grid.columns.length > (self.grid.rowHeaderColumns ? self.grid.rowHeaderColumns.length : 0);
3207 // If we have no columns
3209 // ... and we don't have a ui-grid-columns attribute, which would define columns for us
3210 !$attrs.uiGridColumns &&
3211 // ... and we have no pre-defined columns
3212 self.grid.options.columnDefs.length === 0 &&
3213 // ... but we DO have data
3216 // ... then build the column definitions from the data that we have
3217 self.grid.buildColumnDefsFromData(newData);
3220 // If we haven't built columns before and either have some columns defined or some data defined
3221 if (!hasColumns && (self.grid.options.columnDefs.length > 0 || newData.length > 0)) {
3222 // Build the column set, then pre-compile the column cell templates
3223 promises.push(self.grid.buildColumns()
3225 self.grid.preCompileCellTemplates();
3229 $q.all(promises).then(function() {
3230 // use most recent data, rather than the potentially outdated data passed into watcher handler
3231 self.grid.modifyRows(mostRecentData)
3233 // if (self.viewport) {
3234 self.grid.redrawInPlace(true);
3237 $scope.$evalAsync(function() {
3238 self.grid.refreshCanvas(true);
3239 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW);
3246 var styleWatchDereg = $scope.$watch(function () { return self.grid.styleComputations; }, function() {
3247 self.grid.refreshCanvas(true);
3250 $scope.$on('$destroy', function() {
3251 deregFunctions.forEach( function( deregFn ){ deregFn(); });
3255 self.fireEvent = function(eventName, args) {
3256 // Add the grid to the event arguments if it's not there
3257 if (typeof(args) === 'undefined' || args === undefined) {
3261 if (typeof(args.grid) === 'undefined' || args.grid === undefined) {
3262 args.grid = self.grid;
3265 $scope.$broadcast(eventName, args);
3268 self.innerCompile = function innerCompile(elm) {
3269 $compile(elm)($scope);
3276 * @name ui.grid.directive:uiGrid
3279 * @param {Object} uiGrid Options for the grid to use
3281 * @description Create a very basic grid.
3284 <example module="app">
3285 <file name="app.js">
3286 var app = angular.module('app', ['ui.grid']);
3288 app.controller('MainCtrl', ['$scope', function ($scope) {
3290 { name: 'Bob', title: 'CEO' },
3291 { name: 'Frank', title: 'Lowly Developer' }
3295 <file name="index.html">
3296 <div ng-controller="MainCtrl">
3297 <div ui-grid="{ data: data }"></div>
3302 angular.module('ui.grid').directive('uiGrid', uiGridDirective);
3304 uiGridDirective.$inject = ['$compile', '$templateCache', '$timeout', '$window', 'gridUtil', 'uiGridConstants'];
3305 function uiGridDirective($compile, $templateCache, $timeout, $window, gridUtil, uiGridConstants) {
3307 templateUrl: 'ui-grid/ui-grid',
3313 controller: 'uiGridController',
3314 compile: function () {
3316 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3317 var grid = uiGridCtrl.grid;
3318 // Initialize scrollbars (TODO: move to controller??)
3319 uiGridCtrl.scrollbars = [];
3320 grid.element = $elm;
3323 // See if the grid has a rendered width, if not, wait a bit and try again
3324 var sizeCheckInterval = 100; // ms
3325 var maxSizeChecks = 20; // 2 seconds total
3328 // Setup (event listeners) the grid
3331 // And initialize it
3334 // Mark rendering complete so API events can happen
3335 grid.renderingComplete();
3337 // If the grid doesn't have size currently, wait for a bit to see if it gets size
3342 function checkSize() {
3343 // If the grid has no width and we haven't checked more than <maxSizeChecks> times, check again in <sizeCheckInterval> milliseconds
3344 if ($elm[0].offsetWidth <= 0 && sizeChecks < maxSizeChecks) {
3345 setTimeout(checkSize, sizeCheckInterval);
3353 // Setup event listeners and watchers
3355 // Bind to window resize events
3356 angular.element($window).on('resize', gridResize);
3358 // Unbind from window resize events when the grid is destroyed
3359 $elm.on('$destroy', function () {
3360 angular.element($window).off('resize', gridResize);
3363 // If we add a left container after render, we need to watch and react
3364 $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) {
3365 if (newValue === oldValue) {
3368 grid.refreshCanvas(true);
3371 // If we add a right container after render, we need to watch and react
3372 $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) {
3373 if (newValue === oldValue) {
3376 grid.refreshCanvas(true);
3380 // Initialize the directive
3382 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3384 // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from
3385 grid.canvasWidth = uiGridCtrl.grid.gridWidth;
3387 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3389 // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows
3390 if (grid.gridHeight <= grid.options.rowHeight && grid.options.enableMinHeightCheck) {
3394 // Run initial canvas refresh
3395 grid.refreshCanvas(true);
3398 // Set the grid's height ourselves in the case that its height would be unusably small
3399 function autoAdjustHeight() {
3400 // Figure out the new height
3401 var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight;
3402 var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0;
3403 var footerHeight = grid.calcFooterHeight();
3405 var scrollbarHeight = 0;
3406 if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3407 scrollbarHeight = gridUtil.getScrollbarWidth();
3410 var maxNumberOfFilters = 0;
3411 // Calculates the maximum number of filters in the columns
3412 angular.forEach(grid.options.columnDefs, function(col) {
3413 if (col.hasOwnProperty('filter')) {
3414 if (maxNumberOfFilters < 1) {
3415 maxNumberOfFilters = 1;
3418 else if (col.hasOwnProperty('filters')) {
3419 if (maxNumberOfFilters < col.filters.length) {
3420 maxNumberOfFilters = col.filters.length;
3425 if (grid.options.enableFiltering && !maxNumberOfFilters) {
3426 var allColumnsHaveFilteringTurnedOff = grid.options.columnDefs.length && grid.options.columnDefs.every(function(col) {
3427 return col.enableFiltering === false;
3430 if (!allColumnsHaveFilteringTurnedOff) {
3431 maxNumberOfFilters = 1;
3435 var filterHeight = maxNumberOfFilters * headerHeight;
3437 var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight;
3439 $elm.css('height', newHeight + 'px');
3441 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3444 // Resize the grid on window resize events
3445 function gridResize($event) {
3446 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3447 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3449 grid.refreshCanvas(true);
3462 // TODO: rename this file to ui-grid-pinned-container.js
3464 angular.module('ui.grid').directive('uiGridPinnedContainer', ['gridUtil', function (gridUtil) {
3468 template: '<div class="ui-grid-pinned-container"><div ui-grid-render-container container-id="side" row-container-name="\'body\'" col-container-name="side" bind-scroll-vertical="true" class="{{ side }} ui-grid-render-container-{{ side }}"></div></div>',
3470 side: '=uiGridPinnedContainer'
3473 compile: function compile() {
3475 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3476 // gridUtil.logDebug('ui-grid-pinned-container ' + $scope.side + ' link');
3478 var grid = uiGridCtrl.grid;
3482 $elm.addClass('ui-grid-pinned-container-' + $scope.side);
3484 // Monkey-patch the viewport width function
3485 if ($scope.side === 'left' || $scope.side === 'right') {
3486 grid.renderContainers[$scope.side].getViewportWidth = monkeyPatchedGetViewportWidth;
3489 function monkeyPatchedGetViewportWidth() {
3490 /*jshint validthis: true */
3493 var viewportWidth = 0;
3494 self.visibleColumnCache.forEach(function (column) {
3495 viewportWidth += column.drawnWidth;
3498 var adjustment = self.getViewportAdjustment();
3500 viewportWidth = viewportWidth + adjustment.width;
3502 return viewportWidth;
3505 function updateContainerWidth() {
3506 if ($scope.side === 'left' || $scope.side === 'right') {
3507 var cols = grid.renderContainers[$scope.side].visibleColumnCache;
3509 for (var i = 0; i < cols.length; i++) {
3511 width += col.drawnWidth || col.width || 0;
3518 function updateContainerDimensions() {
3521 // Column containers
3522 if ($scope.side === 'left' || $scope.side === 'right') {
3523 myWidth = updateContainerWidth();
3525 // gridUtil.logDebug('myWidth', myWidth);
3527 // TODO(c0bra): Subtract sum of col widths from grid viewport width and update it
3528 $elm.attr('style', null);
3530 // var myHeight = grid.renderContainers.body.getViewportHeight(); // + grid.horizontalScrollbarHeight;
3532 ret += '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ', .grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ' .ui-grid-render-container-' + $scope.side + ' .ui-grid-viewport { width: ' + myWidth + 'px; } ';
3538 grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
3539 myWidth = updateContainerWidth();
3541 // Subtract our own width
3542 adjustment.width -= myWidth;
3543 adjustment.side = $scope.side;
3548 // Register style computation to adjust for columns in `side`'s render container
3549 grid.registerStyleComputation({
3551 func: updateContainerDimensions
3562 angular.module('ui.grid')
3563 .factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout','ScrollEvent',
3564 function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout, ScrollEvent) {
3568 * @name ui.grid.core.api:PublicApi
3569 * @description Public Api for the core grid features
3575 * @name ui.grid.class:Grid
3576 * @description Grid is the main viewModel. Any properties or methods needed to maintain state are defined in
3577 * this prototype. One instance of Grid is created per Grid directive instance.
3578 * @param {object} options Object map of options to pass into the grid. An 'id' property is expected.
3580 var Grid = function Grid(options) {
3582 // Get the id out of the options, then remove it
3583 if (options !== undefined && typeof(options.id) !== 'undefined' && options.id) {
3584 if (!/^[_a-zA-Z0-9-]+$/.test(options.id)) {
3585 throw new Error("Grid id '" + options.id + '" is invalid. It must follow CSS selector syntax rules.');
3589 throw new Error('No ID provided. An ID must be given when creating a grid.');
3592 self.id = options.id;
3595 // Get default options
3596 self.options = GridOptions.initialize( options );
3601 * @propertyOf ui.grid.class:Grid
3602 * @description reference to the application scope (the parent scope of the ui-grid element). Assigned in ui-grid controller
3604 * use gridOptions.appScopeProvider to override the default assignment of $scope.$parent with any reference
3606 self.appScope = self.options.appScopeProvider;
3608 self.headerHeight = self.options.headerRowHeight;
3613 * @name footerHeight
3614 * @propertyOf ui.grid.class:Grid
3615 * @description returns the total footer height gridFooter + columnFooter
3617 self.footerHeight = self.calcFooterHeight();
3622 * @name columnFooterHeight
3623 * @propertyOf ui.grid.class:Grid
3624 * @description returns the total column footer height
3626 self.columnFooterHeight = self.calcColumnFooterHeight();
3629 self.gridHeight = 0;
3631 self.columnBuilders = [];
3632 self.rowBuilders = [];
3633 self.rowsProcessors = [];
3634 self.columnsProcessors = [];
3635 self.styleComputations = [];
3636 self.viewportAdjusters = [];
3637 self.rowHeaderColumns = [];
3638 self.dataChangeCallbacks = {};
3639 self.verticalScrollSyncCallBackFns = {};
3640 self.horizontalScrollSyncCallBackFns = {};
3642 // self.visibleRowCache = [];
3644 // Set of 'render' containers for self grid, which can render sets of rows
3645 self.renderContainers = {};
3648 self.renderContainers.body = new GridRenderContainer('body', self);
3650 self.cellValueGetterCache = {};
3652 // Cached function to use with custom row templates
3653 self.getRowTemplateFn = null;
3656 //representation of the rows on the grid.
3657 //these are wrapped references to the actual data rows (options.data)
3660 //represents the columns on the grid
3665 * @name isScrollingVertically
3666 * @propertyOf ui.grid.class:Grid
3667 * @description set to true when Grid is scrolling vertically. Set to false via debounced method
3669 self.isScrollingVertically = false;
3673 * @name isScrollingHorizontally
3674 * @propertyOf ui.grid.class:Grid
3675 * @description set to true when Grid is scrolling horizontally. Set to false via debounced method
3677 self.isScrollingHorizontally = false;
3681 * @name scrollDirection
3682 * @propertyOf ui.grid.class:Grid
3683 * @description set one of the uiGridConstants.scrollDirection values (UP, DOWN, LEFT, RIGHT, NONE), which tells
3684 * us which direction we are scrolling. Set to NONE via debounced method
3686 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3688 //if true, grid will not respond to any scroll events
3689 self.disableScrolling = false;
3692 function vertical (scrollEvent) {
3693 self.isScrollingVertically = false;
3694 self.api.core.raise.scrollEnd(scrollEvent);
3695 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3698 var debouncedVertical = gridUtil.debounce(vertical, self.options.scrollDebounce);
3699 var debouncedVerticalMinDelay = gridUtil.debounce(vertical, 0);
3701 function horizontal (scrollEvent) {
3702 self.isScrollingHorizontally = false;
3703 self.api.core.raise.scrollEnd(scrollEvent);
3704 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3707 var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce);
3708 var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0);
3713 * @name flagScrollingVertically
3714 * @methodOf ui.grid.class:Grid
3715 * @description sets isScrollingVertically to true and sets it to false in a debounced function
3717 self.flagScrollingVertically = function(scrollEvent) {
3718 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3719 self.api.core.raise.scrollBegin(scrollEvent);
3721 self.isScrollingVertically = true;
3722 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3723 debouncedVerticalMinDelay(scrollEvent);
3726 debouncedVertical(scrollEvent);
3732 * @name flagScrollingHorizontally
3733 * @methodOf ui.grid.class:Grid
3734 * @description sets isScrollingHorizontally to true and sets it to false in a debounced function
3736 self.flagScrollingHorizontally = function(scrollEvent) {
3737 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3738 self.api.core.raise.scrollBegin(scrollEvent);
3740 self.isScrollingHorizontally = true;
3741 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3742 debouncedHorizontalMinDelay(scrollEvent);
3745 debouncedHorizontal(scrollEvent);
3749 self.scrollbarHeight = 0;
3750 self.scrollbarWidth = 0;
3751 if (self.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3752 self.scrollbarHeight = gridUtil.getScrollbarWidth();
3755 if (self.options.enableVerticalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3756 self.scrollbarWidth = gridUtil.getScrollbarWidth();
3761 self.api = new GridApi(self);
3766 * @methodOf ui.grid.core.api:PublicApi
3767 * @description Refresh the rendered grid on screen.
3768 * The refresh method re-runs both the columnProcessors and the
3769 * rowProcessors, as well as calling refreshCanvas to update all
3770 * the grid sizing. In general you should prefer to use queueGridRefresh
3771 * instead, which is basically a debounced version of refresh.
3773 * If you only want to resize the grid, not regenerate all the rows
3774 * and columns, you should consider directly calling refreshCanvas instead.
3777 self.api.registerMethod( 'core', 'refresh', this.refresh );
3781 * @name queueGridRefresh
3782 * @methodOf ui.grid.core.api:PublicApi
3783 * @description Request a refresh of the rendered grid on screen, if multiple
3784 * calls to queueGridRefresh are made within a digest cycle only one will execute.
3785 * The refresh method re-runs both the columnProcessors and the
3786 * rowProcessors, as well as calling refreshCanvas to update all
3787 * the grid sizing. In general you should prefer to use queueGridRefresh
3788 * instead, which is basically a debounced version of refresh.
3791 self.api.registerMethod( 'core', 'queueGridRefresh', this.queueGridRefresh );
3796 * @methodOf ui.grid.core.api:PublicApi
3797 * @description Runs only the rowProcessors, columns remain as they were.
3798 * It then calls redrawInPlace and refreshCanvas, which adjust the grid sizing.
3799 * @returns {promise} promise that is resolved when render completes?
3802 self.api.registerMethod( 'core', 'refreshRows', this.refreshRows );
3806 * @name queueRefresh
3807 * @methodOf ui.grid.core.api:PublicApi
3808 * @description Requests execution of refreshCanvas, if multiple requests are made
3809 * during a digest cycle only one will run. RefreshCanvas updates the grid sizing.
3810 * @returns {promise} promise that is resolved when render completes?
3813 self.api.registerMethod( 'core', 'queueRefresh', this.queueRefresh );
3817 * @name handleWindowResize
3818 * @methodOf ui.grid.core.api:PublicApi
3819 * @description Trigger a grid resize, normally this would be picked
3820 * up by a watch on window size, but in some circumstances it is necessary
3821 * to call this manually
3822 * @returns {promise} promise that is resolved when render completes?
3825 self.api.registerMethod( 'core', 'handleWindowResize', this.handleWindowResize );
3830 * @name addRowHeaderColumn
3831 * @methodOf ui.grid.core.api:PublicApi
3832 * @description adds a row header column to the grid
3833 * @param {object} column def
3836 self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn );
3840 * @name scrollToIfNecessary
3841 * @methodOf ui.grid.core.api:PublicApi
3842 * @description Scrolls the grid to make a certain row and column combo visible,
3843 * in the case that it is not completely visible on the screen already.
3844 * @param {GridRow} gridRow row to make visible
3845 * @param {GridCol} gridCol column to make visible
3846 * @returns {promise} a promise that is resolved when scrolling is complete
3849 self.api.registerMethod( 'core', 'scrollToIfNecessary', function(gridRow, gridCol) { return self.scrollToIfNecessary(gridRow, gridCol);} );
3854 * @methodOf ui.grid.core.api:PublicApi
3855 * @description Scroll the grid such that the specified
3856 * row and column is in view
3857 * @param {object} rowEntity gridOptions.data[] array instance to make visible
3858 * @param {object} colDef to make visible
3859 * @returns {promise} a promise that is resolved after any scrolling is finished
3861 self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} );
3865 * @name registerRowsProcessor
3866 * @methodOf ui.grid.core.api:PublicApi
3868 * Register a "rows processor" function. When the rows are updated,
3869 * the grid calls each registered "rows processor", which has a chance
3870 * to alter the set of rows (sorting, etc) as long as the count is not
3873 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
3874 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3875 * return the updated rows list, which is passed to the next processor in the chain
3876 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3877 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
3879 * At present allRowsVisible is running at 50, sort manipulations running at 60-65, filter is running at 100,
3880 * sort is at 200, grouping and treeview at 400-410, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
3882 self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor );
3886 * @name registerColumnsProcessor
3887 * @methodOf ui.grid.core.api:PublicApi
3889 * Register a "columns processor" function. When the columns are updated,
3890 * the grid calls each registered "columns processor", which has a chance
3891 * to alter the set of columns as long as the count is not
3894 * @param {function(renderedColumnsToProcess, rows )} processorFunction columns processor function, which
3895 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3896 * return the updated columns list, which is passed to the next processor in the chain
3897 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3898 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
3900 * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
3902 self.api.registerMethod( 'core', 'registerColumnsProcessor', this.registerColumnsProcessor );
3908 * @name sortHandleNulls
3909 * @methodOf ui.grid.core.api:PublicApi
3910 * @description A null handling method that can be used when building custom sort
3914 * mySortFn = function(a, b) {
3915 * var nulls = $scope.gridApi.core.sortHandleNulls(a, b);
3916 * if ( nulls !== null ){
3919 * // your code for sorting here
3922 * @param {object} a sort value a
3923 * @param {object} b sort value b
3924 * @returns {number} null if there were no nulls/undefineds, otherwise returns
3925 * a sort value that should be passed back from the sort function
3928 self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls );
3934 * @methodOf ui.grid.core.api:PublicApi
3935 * @description The sort criteria on one or more columns has
3936 * changed. Provides as parameters the grid and the output of
3937 * getColumnSorting, which is an array of gridColumns
3938 * that have sorting on them, sorted in priority order.
3940 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3941 * @param {Function} callBack Will be called when the event is emited. The function passes back the grid and an array of
3942 * columns with sorts on them, in priority order.
3946 * gridApi.core.on.sortChanged( $scope, function(grid, sortColumns){
3951 self.api.registerEvent( 'core', 'sortChanged' );
3955 * @name columnVisibilityChanged
3956 * @methodOf ui.grid.core.api:PublicApi
3957 * @description The visibility of a column has changed,
3958 * the column itself is passed out as a parameter of the event.
3960 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3961 * @param {Function} callBack Will be called when the event is emited. The function passes back the GridCol that has changed.
3965 * gridApi.core.on.columnVisibilityChanged( $scope, function (column) {
3970 self.api.registerEvent( 'core', 'columnVisibilityChanged' );
3974 * @name notifyDataChange
3975 * @methodOf ui.grid.core.api:PublicApi
3976 * @description Notify the grid that a data or config change has occurred,
3977 * where that change isn't something the grid was otherwise noticing. This
3978 * might be particularly relevant where you've changed values within the data
3979 * and you'd like cell classes to be re-evaluated, or changed config within
3980 * the columnDef and you'd like headerCellClasses to be re-evaluated.
3981 * @param {string} type one of the
3982 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN), which tells
3983 * us which refreshes to fire.
3986 self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange );
3990 * @name clearAllFilters
3991 * @methodOf ui.grid.core.api:PublicApi
3992 * @description Clears all filters and optionally refreshes the visible rows.
3993 * @param {object} refreshRows Defaults to true.
3994 * @param {object} clearConditions Defaults to false.
3995 * @param {object} clearFlags Defaults to false.
3996 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
3998 self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters);
4000 self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]);
4001 self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]);
4002 self.registerDataChangeCallback( self.updateFooterHeightCallback, [uiGridConstants.dataChange.OPTIONS]);
4004 self.registerStyleComputation({
4006 func: self.getFooterStyles
4010 Grid.prototype.calcFooterHeight = function () {
4011 if (!this.hasFooter()) {
4016 if (this.options.showGridFooter) {
4017 height += this.options.gridFooterHeight;
4020 height += this.calcColumnFooterHeight();
4025 Grid.prototype.calcColumnFooterHeight = function () {
4028 if (this.options.showColumnFooter) {
4029 height += this.options.columnFooterHeight;
4035 Grid.prototype.getFooterStyles = function () {
4036 var style = '.grid' + this.id + ' .ui-grid-footer-aggregates-row { height: ' + this.options.columnFooterHeight + 'px; }';
4037 style += ' .grid' + this.id + ' .ui-grid-footer-info { height: ' + this.options.gridFooterHeight + 'px; }';
4041 Grid.prototype.hasFooter = function () {
4042 return this.options.showGridFooter || this.options.showColumnFooter;
4048 * @methodOf ui.grid.class:Grid
4049 * @description Returns true if grid is RightToLeft
4051 Grid.prototype.isRTL = function () {
4058 * @name registerColumnBuilder
4059 * @methodOf ui.grid.class:Grid
4060 * @description When the build creates columns from column definitions, the columnbuilders will be called to add
4061 * additional properties to the column.
4062 * @param {function(colDef, col, gridOptions)} columnBuilder function to be called
4064 Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) {
4065 this.columnBuilders.push(columnBuilder);
4070 * @name buildColumnDefsFromData
4071 * @methodOf ui.grid.class:Grid
4072 * @description Populates columnDefs from the provided data
4073 * @param {function(colDef, col, gridOptions)} rowBuilder function to be called
4075 Grid.prototype.buildColumnDefsFromData = function (dataRows){
4076 this.options.columnDefs = gridUtil.getColumnsFromData(dataRows, this.options.excludeProperties);
4081 * @name registerRowBuilder
4082 * @methodOf ui.grid.class:Grid
4083 * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add
4084 * additional properties to the row.
4085 * @param {function(row, gridOptions)} rowBuilder function to be called
4087 Grid.prototype.registerRowBuilder = function registerRowBuilder(rowBuilder) {
4088 this.rowBuilders.push(rowBuilder);
4094 * @name registerDataChangeCallback
4095 * @methodOf ui.grid.class:Grid
4096 * @description When a data change occurs, the data change callbacks of the specified type
4097 * will be called. The rules are:
4099 * - when the data watch fires, that is considered a ROW change (the data watch only notices
4100 * added or removed rows)
4101 * - when the api is called to inform us of a change, the declared type of that change is used
4102 * - when a cell edit completes, the EDIT callbacks are triggered
4103 * - when the columnDef watch fires, the COLUMN callbacks are triggered
4104 * - when the options watch fires, the OPTIONS callbacks are triggered
4106 * For a given event:
4107 * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks
4108 * - ROW calls ROW and ALL callbacks
4109 * - EDIT calls EDIT and ALL callbacks
4110 * - COLUMN calls COLUMN and ALL callbacks
4111 * - OPTIONS calls OPTIONS and ALL callbacks
4113 * @param {function(grid)} callback function to be called
4114 * @param {array} types the types of data change you want to be informed of. Values from
4115 * the uiGridConstants.dataChange values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to
4117 * @returns {function} deregister function - a function that can be called to deregister this callback
4119 Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) {
4120 var uid = gridUtil.nextUid();
4122 types = [uiGridConstants.dataChange.ALL];
4124 if ( !Array.isArray(types)){
4125 gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types );
4127 this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this };
4130 var deregisterFunction = function() {
4131 delete self.dataChangeCallbacks[uid];
4133 return deregisterFunction;
4138 * @name callDataChangeCallbacks
4139 * @methodOf ui.grid.class:Grid
4140 * @description Calls the callbacks based on the type of data change that
4141 * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the
4142 * event type is matching, or if the type is ALL.
4143 * @param {number} type the type of event that occurred - one of the
4144 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN, OPTIONS)
4146 Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) {
4147 angular.forEach( this.dataChangeCallbacks, function( callback, uid ){
4148 if ( callback.types.indexOf( uiGridConstants.dataChange.ALL ) !== -1 ||
4149 callback.types.indexOf( type ) !== -1 ||
4150 type === uiGridConstants.dataChange.ALL ) {
4151 if (callback._this) {
4152 callback.callback.apply(callback._this,this);
4155 callback.callback( this );
4163 * @name notifyDataChange
4164 * @methodOf ui.grid.class:Grid
4165 * @description Notifies us that a data change has occurred, used in the public
4166 * api for users to tell us when they've changed data or some other event that
4167 * our watches cannot pick up
4168 * @param {string} type the type of event that occurred - one of the
4169 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN)
4171 Grid.prototype.notifyDataChange = function notifyDataChange(type) {
4172 var constants = uiGridConstants.dataChange;
4173 if ( type === constants.ALL ||
4174 type === constants.COLUMN ||
4175 type === constants.EDIT ||
4176 type === constants.ROW ||
4177 type === constants.OPTIONS ){
4178 this.callDataChangeCallbacks( type );
4180 gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type);
4187 * @name columnRefreshCallback
4188 * @methodOf ui.grid.class:Grid
4189 * @description refreshes the grid when a column refresh
4190 * is notified, which triggers handling of the visible flag.
4191 * This is called on uiGridConstants.dataChange.COLUMN, and is
4192 * registered as a dataChangeCallback in grid.js
4193 * @param {string} name column name
4195 Grid.prototype.columnRefreshCallback = function columnRefreshCallback( grid ){
4196 grid.buildColumns();
4197 grid.queueGridRefresh();
4203 * @name processRowsCallback
4204 * @methodOf ui.grid.class:Grid
4205 * @description calls the row processors, specifically
4206 * intended to reset the sorting when an edit is called,
4207 * registered as a dataChangeCallback on uiGridConstants.dataChange.EDIT
4208 * @param {string} name column name
4210 Grid.prototype.processRowsCallback = function processRowsCallback( grid ){
4211 grid.queueGridRefresh();
4217 * @name updateFooterHeightCallback
4218 * @methodOf ui.grid.class:Grid
4219 * @description recalculates the footer height,
4220 * registered as a dataChangeCallback on uiGridConstants.dataChange.OPTIONS
4221 * @param {string} name column name
4223 Grid.prototype.updateFooterHeightCallback = function updateFooterHeightCallback( grid ){
4224 grid.footerHeight = grid.calcFooterHeight();
4225 grid.columnFooterHeight = grid.calcColumnFooterHeight();
4232 * @methodOf ui.grid.class:Grid
4233 * @description returns a grid column for the column name
4234 * @param {string} name column name
4236 Grid.prototype.getColumn = function getColumn(name) {
4237 var columns = this.columns.filter(function (column) {
4238 return column.colDef.name === name;
4240 return columns.length > 0 ? columns[0] : null;
4246 * @methodOf ui.grid.class:Grid
4247 * @description returns a grid colDef for the column name
4248 * @param {string} name column.field
4250 Grid.prototype.getColDef = function getColDef(name) {
4251 var colDefs = this.options.columnDefs.filter(function (colDef) {
4252 return colDef.name === name;
4254 return colDefs.length > 0 ? colDefs[0] : null;
4260 * @methodOf ui.grid.class:Grid
4261 * @description uses the first row of data to assign colDef.type for any types not defined.
4266 * @propertyOf ui.grid.class:GridOptions.columnDef
4267 * @description the type of the column, used in sorting. If not provided then the
4268 * grid will guess the type. Add this only if the grid guessing is not to your
4269 * satisfaction. One of:
4276 * Note that if you choose date, your dates should be in a javascript date type
4279 Grid.prototype.assignTypes = function(){
4281 self.options.columnDefs.forEach(function (colDef, index) {
4283 //Assign colDef type if not specified
4285 var col = new GridColumn(colDef, index, self);
4286 var firstRow = self.rows.length > 0 ? self.rows[0] : null;
4288 colDef.type = gridUtil.guessType(self.getCellValue(firstRow, col));
4291 colDef.type = 'string';
4300 * @name isRowHeaderColumn
4301 * @methodOf ui.grid.class:Grid
4302 * @description returns true if the column is a row Header
4303 * @param {object} column column
4305 Grid.prototype.isRowHeaderColumn = function isRowHeaderColumn(column) {
4306 return this.rowHeaderColumns.indexOf(column) !== -1;
4311 * @name addRowHeaderColumn
4312 * @methodOf ui.grid.class:Grid
4313 * @description adds a row header column to the grid
4314 * @param {object} column def
4316 Grid.prototype.addRowHeaderColumn = function addRowHeaderColumn(colDef) {
4318 var rowHeaderCol = new GridColumn(colDef, gridUtil.nextUid(), self);
4319 rowHeaderCol.isRowHeader = true;
4321 self.createRightContainer();
4322 rowHeaderCol.renderContainer = 'right';
4325 self.createLeftContainer();
4326 rowHeaderCol.renderContainer = 'left';
4329 // relies on the default column builder being first in array, as it is instantiated
4330 // as part of grid creation
4331 self.columnBuilders[0](colDef,rowHeaderCol,self.options)
4333 rowHeaderCol.enableFiltering = false;
4334 rowHeaderCol.enableSorting = false;
4335 rowHeaderCol.enableHiding = false;
4336 self.rowHeaderColumns.push(rowHeaderCol);
4339 self.preCompileCellTemplates();
4340 self.queueGridRefresh();
4347 * @name getOnlyDataColumns
4348 * @methodOf ui.grid.class:Grid
4349 * @description returns all columns except for rowHeader columns
4351 Grid.prototype.getOnlyDataColumns = function getOnlyDataColumns() {
4354 self.columns.forEach(function (col) {
4355 if (self.rowHeaderColumns.indexOf(col) === -1) {
4364 * @name buildColumns
4365 * @methodOf ui.grid.class:Grid
4366 * @description creates GridColumn objects from the columnDefinition. Calls each registered
4367 * columnBuilder to further process the column
4368 * @param {object} options An object contains options to use when building columns
4370 * * **orderByColumnDefs**: defaults to **false**. When true, `buildColumns` will reorder existing columns according to the order within the column definitions.
4372 * @returns {Promise} a promise to load any needed column resources
4374 Grid.prototype.buildColumns = function buildColumns(opts) {
4376 orderByColumnDefs: false
4379 angular.extend(options, opts);
4381 // gridUtil.logDebug('buildColumns');
4383 var builderPromises = [];
4384 var headerOffset = self.rowHeaderColumns.length;
4387 // Remove any columns for which a columnDef cannot be found
4388 // Deliberately don't use forEach, as it doesn't like splice being called in the middle
4389 // Also don't cache columns.length, as it will change during this operation
4390 for (i = 0; i < self.columns.length; i++){
4391 if (!self.getColDef(self.columns[i].name)) {
4392 self.columns.splice(i, 1);
4397 //add row header columns to the grid columns array _after_ columns without columnDefs have been removed
4398 self.rowHeaderColumns.forEach(function (rowHeaderColumn) {
4399 self.columns.unshift(rowHeaderColumn);
4403 // look at each column def, and update column properties to match. If the column def
4404 // doesn't have a column, then splice in a new gridCol
4405 self.options.columnDefs.forEach(function (colDef, index) {
4406 self.preprocessColDef(colDef);
4407 var col = self.getColumn(colDef.name);
4410 col = new GridColumn(colDef, gridUtil.nextUid(), self);
4411 self.columns.splice(index + headerOffset, 0, col);
4414 // tell updateColumnDef that the column was pre-existing
4415 col.updateColumnDef(colDef, false);
4418 self.columnBuilders.forEach(function (builder) {
4419 builderPromises.push(builder.call(self, colDef, col, self.options));
4423 /*** Reorder columns if necessary ***/
4424 if (!!options.orderByColumnDefs) {
4425 // Create a shallow copy of the columns as a cache
4426 var columnCache = self.columns.slice(0);
4428 // We need to allow for the "row headers" when mapping from the column defs array to the columns array
4429 // If we have a row header in columns[0] and don't account for it we'll overwrite it with the column in columnDefs[0]
4431 // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then
4432 // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result
4433 var len = Math.min(self.options.columnDefs.length, self.columns.length);
4434 for (i = 0; i < len; i++) {
4435 // If the column at this index has a different name than the column at the same index in the column defs...
4436 if (self.columns[i + headerOffset].name !== self.options.columnDefs[i].name) {
4437 // Replace the one in the cache with the appropriate column
4438 columnCache[i + headerOffset] = self.getColumn(self.options.columnDefs[i].name);
4441 // Otherwise just copy over the one from the initial columns
4442 columnCache[i + headerOffset] = self.columns[i + headerOffset];
4446 // Empty out the columns array, non-destructively
4447 self.columns.length = 0;
4449 // And splice in the updated, ordered columns from the cache
4450 Array.prototype.splice.apply(self.columns, [0, 0].concat(columnCache));
4453 return $q.all(builderPromises).then(function(){
4454 if (self.rows.length > 0){
4462 * @name preCompileCellTemplates
4463 * @methodOf ui.grid.class:Grid
4464 * @description precompiles all cell templates
4466 Grid.prototype.preCompileCellTemplates = function() {
4469 var preCompileTemplate = function( col ) {
4470 var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col));
4471 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
4473 var compiledElementFn = $compile(html);
4474 col.compiledElementFn = compiledElementFn;
4476 if (col.compiledElementFnDefer) {
4477 col.compiledElementFnDefer.resolve(col.compiledElementFn);
4481 this.columns.forEach(function (col) {
4482 if ( col.cellTemplate ){
4483 preCompileTemplate( col );
4484 } else if ( col.cellTemplatePromise ){
4485 col.cellTemplatePromise.then( function() {
4486 preCompileTemplate( col );
4494 * @name getGridQualifiedColField
4495 * @methodOf ui.grid.class:Grid
4496 * @description Returns the $parse-able accessor for a column within its $scope
4497 * @param {GridColumn} col col object
4499 Grid.prototype.getQualifiedColField = function (col) {
4500 return 'row.entity.' + gridUtil.preEval(col.field);
4505 * @name createLeftContainer
4506 * @methodOf ui.grid.class:Grid
4507 * @description creates the left render container if it doesn't already exist
4509 Grid.prototype.createLeftContainer = function() {
4510 if (!this.hasLeftContainer()) {
4511 this.renderContainers.left = new GridRenderContainer('left', this, { disableColumnOffset: true });
4517 * @name createRightContainer
4518 * @methodOf ui.grid.class:Grid
4519 * @description creates the right render container if it doesn't already exist
4521 Grid.prototype.createRightContainer = function() {
4522 if (!this.hasRightContainer()) {
4523 this.renderContainers.right = new GridRenderContainer('right', this, { disableColumnOffset: true });
4529 * @name hasLeftContainer
4530 * @methodOf ui.grid.class:Grid
4531 * @description returns true if leftContainer exists
4533 Grid.prototype.hasLeftContainer = function() {
4534 return this.renderContainers.left !== undefined;
4539 * @name hasRightContainer
4540 * @methodOf ui.grid.class:Grid
4541 * @description returns true if rightContainer exists
4543 Grid.prototype.hasRightContainer = function() {
4544 return this.renderContainers.right !== undefined;
4549 * undocumented function
4550 * @name preprocessColDef
4551 * @methodOf ui.grid.class:Grid
4552 * @description defaults the name property from field to maintain backwards compatibility with 2.x
4553 * validates that name or field is present
4555 Grid.prototype.preprocessColDef = function preprocessColDef(colDef) {
4558 if (!colDef.field && !colDef.name) {
4559 throw new Error('colDef.name or colDef.field property is required');
4562 //maintain backwards compatibility with 2.x
4563 //field was required in 2.x. now name is required
4564 if (colDef.name === undefined && colDef.field !== undefined) {
4565 // See if the column name already exists:
4566 var newName = colDef.field,
4568 while (self.getColumn(newName)) {
4569 newName = colDef.field + counter.toString();
4572 colDef.name = newName;
4576 // Return a list of items that exist in the `n` array but not the `o` array. Uses optional property accessors passed as third & fourth parameters
4577 Grid.prototype.newInN = function newInN(o, n, oAccessor, nAccessor) {
4581 for (var i = 0; i < n.length; i++) {
4582 var nV = nAccessor ? n[i][nAccessor] : n[i];
4585 for (var j = 0; j < o.length; j++) {
4586 var oV = oAccessor ? o[j][oAccessor] : o[j];
4587 if (self.options.rowEquality(nV, oV)) {
4603 * @methodOf ui.grid.class:Grid
4604 * @description returns the GridRow that contains the rowEntity
4605 * @param {object} rowEntity the gridOptions.data array element instance
4606 * @param {array} rows [optional] the rows to look in - if not provided then
4607 * looks in grid.rows
4609 Grid.prototype.getRow = function getRow(rowEntity, lookInRows) {
4612 lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows;
4614 var rows = lookInRows.filter(function (row) {
4615 return self.options.rowEquality(row.entity, rowEntity);
4617 return rows.length > 0 ? rows[0] : null;
4624 * @methodOf ui.grid.class:Grid
4625 * @description creates or removes GridRow objects from the newRawData array. Calls each registered
4626 * rowBuilder to further process the row
4627 * @param {array} newRawData Modified set of data
4629 * This method aims to achieve three things:
4630 * 1. the resulting rows array is in the same order as the newRawData, we'll call
4631 * rowsProcessors immediately after to sort the data anyway
4632 * 2. if we have row hashing available, we try to use the rowHash to find the row
4633 * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected
4635 * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates
4636 * the newRows and newHash
4639 * newRawData.forEach newEntity
4640 * if (hashing enabled)
4641 * check oldHash for newEntity
4643 * look for old row directly in oldRows
4644 * if !oldRowFound // must be a new row
4646 * append to the newRows and add to newHash
4647 * run the processors
4650 * Rows are identified using the hashKey if configured. If not configured, then rows
4651 * are identified using the gridOptions.rowEquality function
4653 * This method is useful when trying to select rows immediately after loading data without
4654 * using a $timeout/$interval, e.g.:
4656 * $scope.gridOptions.data = someData;
4657 * $scope.gridApi.grid.modifyRows($scope.gridOptions.data);
4658 * $scope.gridApi.selection.selectRow($scope.gridOptions.data[0]);
4660 * OR to persist row selection after data update (e.g. rows selected, new data loaded, want
4661 * originally selected rows to be re-selected))
4663 Grid.prototype.modifyRows = function modifyRows(newRawData) {
4665 var oldRows = self.rows.slice(0);
4666 var oldRowHash = self.rowHashMap || self.createRowHashMap();
4667 self.rowHashMap = self.createRowHashMap();
4668 self.rows.length = 0;
4670 newRawData.forEach( function( newEntity, i ) {
4672 if ( self.options.enableRowHashing ){
4673 // if hashing is enabled, then this row will be in the hash if we already know about it
4674 newRow = oldRowHash.get( newEntity );
4676 // otherwise, manually search the oldRows to see if we can find this row
4677 newRow = self.getRow(newEntity, oldRows);
4680 // if we didn't find the row, it must be new, so create it
4682 newRow = self.processRowBuilders(new GridRow(newEntity, i, self));
4685 self.rows.push( newRow );
4686 self.rowHashMap.put( newEntity, newRow );
4691 var p1 = $q.when(self.processRowsProcessors(self.rows))
4692 .then(function (renderableRows) {
4693 return self.setVisibleRows(renderableRows);
4696 var p2 = $q.when(self.processColumnsProcessors(self.columns))
4697 .then(function (renderableColumns) {
4698 return self.setVisibleColumns(renderableColumns);
4701 return $q.all([p1, p2]);
4706 * Private Undocumented Method
4708 * @methodOf ui.grid.class:Grid
4709 * @description adds the newRawData array of rows to the grid and calls all registered
4710 * rowBuilders. this keyword will reference the grid
4712 Grid.prototype.addRows = function addRows(newRawData) {
4715 var existingRowCount = self.rows.length;
4716 for (var i = 0; i < newRawData.length; i++) {
4717 var newRow = self.processRowBuilders(new GridRow(newRawData[i], i + existingRowCount, self));
4719 if (self.options.enableRowHashing) {
4720 var found = self.rowHashMap.get(newRow.entity);
4726 self.rows.push(newRow);
4732 * @name processRowBuilders
4733 * @methodOf ui.grid.class:Grid
4734 * @description processes all RowBuilders for the gridRow
4735 * @param {GridRow} gridRow reference to gridRow
4736 * @returns {GridRow} the gridRow with all additional behavior added
4738 Grid.prototype.processRowBuilders = function processRowBuilders(gridRow) {
4741 self.rowBuilders.forEach(function (builder) {
4742 builder.call(self, gridRow, self.options);
4750 * @name registerStyleComputation
4751 * @methodOf ui.grid.class:Grid
4752 * @description registered a styleComputation function
4754 * If the function returns a value it will be appended into the grid's `<style>` block
4755 * @param {function($scope)} styleComputation function
4757 Grid.prototype.registerStyleComputation = function registerStyleComputation(styleComputationInfo) {
4758 this.styleComputations.push(styleComputationInfo);
4762 // NOTE (c0bra): We already have rowBuilders. I think these do exactly the same thing...
4763 // Grid.prototype.registerRowFilter = function(filter) {
4764 // // TODO(c0bra): validate filter?
4766 // this.rowFilters.push(filter);
4769 // Grid.prototype.removeRowFilter = function(filter) {
4770 // var idx = this.rowFilters.indexOf(filter);
4772 // if (typeof(idx) !== 'undefined' && idx !== undefined) {
4773 // this.rowFilters.slice(idx, 1);
4777 // Grid.prototype.processRowFilters = function(rows) {
4779 // self.rowFilters.forEach(function (filter) {
4780 // filter.call(self, rows);
4787 * @name registerRowsProcessor
4788 * @methodOf ui.grid.class:Grid
4791 * Register a "rows processor" function. When the rows are updated,
4792 * the grid calls each registered "rows processor", which has a chance
4793 * to alter the set of rows (sorting, etc) as long as the count is not
4796 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
4797 * is run in the context of the grid (i.e. this for the function will be the grid), and must
4798 * return the updated rows list, which is passed to the next processor in the chain
4799 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4800 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
4802 * At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
4805 Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor, priority) {
4806 if (!angular.isFunction(processor)) {
4807 throw 'Attempt to register non-function rows processor: ' + processor;
4810 this.rowsProcessors.push({processor: processor, priority: priority});
4811 this.rowsProcessors.sort(function sortByPriority( a, b ){
4812 return a.priority - b.priority;
4818 * @name removeRowsProcessor
4819 * @methodOf ui.grid.class:Grid
4820 * @param {function(renderableRows)} rows processor function
4821 * @description Remove a registered rows processor
4823 Grid.prototype.removeRowsProcessor = function removeRowsProcessor(processor) {
4825 this.rowsProcessors.forEach(function(rowsProcessor, index){
4826 if ( rowsProcessor.processor === processor ){
4832 this.rowsProcessors.splice(idx, 1);
4837 * Private Undocumented Method
4838 * @name processRowsProcessors
4839 * @methodOf ui.grid.class:Grid
4840 * @param {Array[GridRow]} The array of "renderable" rows
4841 * @param {Array[GridColumn]} The array of columns
4842 * @description Run all the registered rows processors on the array of renderable rows
4844 Grid.prototype.processRowsProcessors = function processRowsProcessors(renderableRows) {
4847 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4848 var myRenderableRows = renderableRows.slice(0);
4850 // Return myRenderableRows with no processing if we have no rows processors
4851 if (self.rowsProcessors.length === 0) {
4852 return $q.when(myRenderableRows);
4855 // Counter for iterating through rows processors
4858 // Promise for when we're done with all the processors
4859 var finished = $q.defer();
4861 // This function will call the processor in self.rowsProcessors at index 'i', and then
4862 // when done will call the next processor in the list, using the output from the processor
4863 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4865 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4867 function startProcessor(i, renderedRowsToProcess) {
4868 // Get the processor at 'i'
4869 var processor = self.rowsProcessors[i].processor;
4871 // Call the processor, passing in the rows to process and the current columns
4872 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4873 return $q.when( processor.call(self, renderedRowsToProcess, self.columns) )
4874 .then(function handleProcessedRows(processedRows) {
4876 if (!processedRows) {
4877 throw "Processor at index " + i + " did not return a set of renderable rows";
4880 if (!angular.isArray(processedRows)) {
4881 throw "Processor at index " + i + " did not return an array";
4884 // Processor is done, increment the counter
4887 // If we're not done with the processors, call the next one
4888 if (i <= self.rowsProcessors.length - 1) {
4889 return startProcessor(i, processedRows);
4891 // We're done! Resolve the 'finished' promise
4893 finished.resolve(processedRows);
4898 // Start on the first processor
4899 startProcessor(0, myRenderableRows);
4901 return finished.promise;
4904 Grid.prototype.setVisibleRows = function setVisibleRows(rows) {
4907 // Reset all the render container row caches
4908 for (var i in self.renderContainers) {
4909 var container = self.renderContainers[i];
4911 container.canvasHeightShouldUpdate = true;
4913 if ( typeof(container.visibleRowCache) === 'undefined' ){
4914 container.visibleRowCache = [];
4916 container.visibleRowCache.length = 0;
4920 // rows.forEach(function (row) {
4921 for (var ri = 0; ri < rows.length; ri++) {
4924 var targetContainer = (typeof(row.renderContainer) !== 'undefined' && row.renderContainer) ? row.renderContainer : 'body';
4926 // If the row is visible
4928 self.renderContainers[targetContainer].visibleRowCache.push(row);
4931 self.api.core.raise.rowsRendered(this.api);
4936 * @name registerColumnsProcessor
4937 * @methodOf ui.grid.class:Grid
4938 * @param {function(renderedColumnsToProcess, rows)} columnProcessor column processor function, which
4939 * is run in the context of the grid (i.e. this for the function will be the grid), and
4940 * which must return an updated renderedColumnsToProcess which can be passed to the next processor
4942 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4943 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
4945 * At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
4948 Register a "columns processor" function. When the columns are updated,
4949 the grid calls each registered "columns processor", which has a chance
4950 to alter the set of columns, as long as the count is not modified.
4952 Grid.prototype.registerColumnsProcessor = function registerColumnsProcessor(processor, priority) {
4953 if (!angular.isFunction(processor)) {
4954 throw 'Attempt to register non-function rows processor: ' + processor;
4957 this.columnsProcessors.push({processor: processor, priority: priority});
4958 this.columnsProcessors.sort(function sortByPriority( a, b ){
4959 return a.priority - b.priority;
4963 Grid.prototype.removeColumnsProcessor = function removeColumnsProcessor(processor) {
4964 var idx = this.columnsProcessors.indexOf(processor);
4966 if (typeof(idx) !== 'undefined' && idx !== undefined) {
4967 this.columnsProcessors.splice(idx, 1);
4971 Grid.prototype.processColumnsProcessors = function processColumnsProcessors(renderableColumns) {
4974 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4975 var myRenderableColumns = renderableColumns.slice(0);
4977 // Return myRenderableRows with no processing if we have no rows processors
4978 if (self.columnsProcessors.length === 0) {
4979 return $q.when(myRenderableColumns);
4982 // Counter for iterating through rows processors
4985 // Promise for when we're done with all the processors
4986 var finished = $q.defer();
4988 // This function will call the processor in self.rowsProcessors at index 'i', and then
4989 // when done will call the next processor in the list, using the output from the processor
4990 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4992 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4994 function startProcessor(i, renderedColumnsToProcess) {
4995 // Get the processor at 'i'
4996 var processor = self.columnsProcessors[i].processor;
4998 // Call the processor, passing in the rows to process and the current columns
4999 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
5000 return $q.when( processor.call(self, renderedColumnsToProcess, self.rows) )
5001 .then(function handleProcessedRows(processedColumns) {
5003 if (!processedColumns) {
5004 throw "Processor at index " + i + " did not return a set of renderable rows";
5007 if (!angular.isArray(processedColumns)) {
5008 throw "Processor at index " + i + " did not return an array";
5011 // Processor is done, increment the counter
5014 // If we're not done with the processors, call the next one
5015 if (i <= self.columnsProcessors.length - 1) {
5016 return startProcessor(i, myRenderableColumns);
5018 // We're done! Resolve the 'finished' promise
5020 finished.resolve(myRenderableColumns);
5025 // Start on the first processor
5026 startProcessor(0, myRenderableColumns);
5028 return finished.promise;
5031 Grid.prototype.setVisibleColumns = function setVisibleColumns(columns) {
5032 // gridUtil.logDebug('setVisibleColumns');
5036 // Reset all the render container row caches
5037 for (var i in self.renderContainers) {
5038 var container = self.renderContainers[i];
5040 container.visibleColumnCache.length = 0;
5043 for (var ci = 0; ci < columns.length; ci++) {
5044 var column = columns[ci];
5046 // If the column is visible
5047 if (column.visible) {
5048 // If the column has a container specified
5049 if (typeof(column.renderContainer) !== 'undefined' && column.renderContainer) {
5050 self.renderContainers[column.renderContainer].visibleColumnCache.push(column);
5052 // If not, put it into the body container
5054 self.renderContainers.body.visibleColumnCache.push(column);
5062 * @name handleWindowResize
5063 * @methodOf ui.grid.class:Grid
5064 * @description Triggered when the browser window resizes; automatically resizes the grid
5065 * @returns {Promise} A resolved promise once the window resize has completed.
5067 Grid.prototype.handleWindowResize = function handleWindowResize($event) {
5070 self.gridWidth = gridUtil.elementWidth(self.element);
5071 self.gridHeight = gridUtil.elementHeight(self.element);
5073 return self.queueRefresh();
5078 * @name queueRefresh
5079 * @methodOf ui.grid.class:Grid
5080 * @description queues a grid refreshCanvas, a way of debouncing all the refreshes we might otherwise issue
5082 Grid.prototype.queueRefresh = function queueRefresh() {
5085 if (self.refreshCanceller) {
5086 $timeout.cancel(self.refreshCanceller);
5089 self.refreshCanceller = $timeout(function () {
5090 self.refreshCanvas(true);
5093 self.refreshCanceller.then(function () {
5094 self.refreshCanceller = null;
5097 return self.refreshCanceller;
5103 * @name queueGridRefresh
5104 * @methodOf ui.grid.class:Grid
5105 * @description queues a grid refresh, a way of debouncing all the refreshes we might otherwise issue
5107 Grid.prototype.queueGridRefresh = function queueGridRefresh() {
5110 if (self.gridRefreshCanceller) {
5111 $timeout.cancel(self.gridRefreshCanceller);
5114 self.gridRefreshCanceller = $timeout(function () {
5118 self.gridRefreshCanceller.then(function () {
5119 self.gridRefreshCanceller = null;
5122 return self.gridRefreshCanceller;
5128 * @name updateCanvasHeight
5129 * @methodOf ui.grid.class:Grid
5130 * @description flags all render containers to update their canvas height
5132 Grid.prototype.updateCanvasHeight = function updateCanvasHeight() {
5135 for (var containerId in self.renderContainers) {
5136 if (self.renderContainers.hasOwnProperty(containerId)) {
5137 var container = self.renderContainers[containerId];
5138 container.canvasHeightShouldUpdate = true;
5146 * @methodOf ui.grid.class:Grid
5147 * @description calls each styleComputation function
5149 // TODO: this used to take $scope, but couldn't see that it was used
5150 Grid.prototype.buildStyles = function buildStyles() {
5151 // gridUtil.logDebug('buildStyles');
5155 self.customStyles = '';
5157 self.styleComputations
5158 .sort(function(a, b) {
5159 if (a.priority === null) { return 1; }
5160 if (b.priority === null) { return -1; }
5161 if (a.priority === null && b.priority === null) { return 0; }
5162 return a.priority - b.priority;
5164 .forEach(function (compInfo) {
5165 // this used to provide $scope as a second parameter, but I couldn't find any
5166 // style builders that used it, so removed it as part of moving to grid from controller
5167 var ret = compInfo.func.call(self);
5169 if (angular.isString(ret)) {
5170 self.customStyles += '\n' + ret;
5176 Grid.prototype.minColumnsToRender = function minColumnsToRender() {
5178 var viewport = this.getViewportWidth();
5182 self.columns.forEach(function(col, i) {
5183 if (totalWidth < viewport) {
5184 totalWidth += col.drawnWidth;
5189 for (var j = i; j >= i - min; j--) {
5190 currWidth += self.columns[j].drawnWidth;
5192 if (currWidth < viewport) {
5201 Grid.prototype.getBodyHeight = function getBodyHeight() {
5202 // Start with the viewportHeight
5203 var bodyHeight = this.getViewportHeight();
5205 // Add the horizontal scrollbar height if there is one
5206 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5207 // bodyHeight = bodyHeight + this.horizontalScrollbarHeight;
5213 // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border)
5214 // TODO(c0bra): account for footer height
5215 Grid.prototype.getViewportHeight = function getViewportHeight() {
5218 var viewPortHeight = this.gridHeight - this.headerHeight - this.footerHeight;
5220 // Account for native horizontal scrollbar, if present
5221 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5222 // viewPortHeight = viewPortHeight - this.horizontalScrollbarHeight;
5225 var adjustment = self.getViewportAdjustment();
5227 viewPortHeight = viewPortHeight + adjustment.height;
5229 //gridUtil.logDebug('viewPortHeight', viewPortHeight);
5231 return viewPortHeight;
5234 Grid.prototype.getViewportWidth = function getViewportWidth() {
5237 var viewPortWidth = this.gridWidth;
5239 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5240 // viewPortWidth = viewPortWidth - this.verticalScrollbarWidth;
5243 var adjustment = self.getViewportAdjustment();
5245 viewPortWidth = viewPortWidth + adjustment.width;
5247 //gridUtil.logDebug('getviewPortWidth', viewPortWidth);
5249 return viewPortWidth;
5252 Grid.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
5253 var viewPortWidth = this.getViewportWidth();
5255 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5256 // viewPortWidth = viewPortWidth + this.verticalScrollbarWidth;
5259 return viewPortWidth;
5262 Grid.prototype.addVerticalScrollSync = function (containerId, callBackFn) {
5263 this.verticalScrollSyncCallBackFns[containerId] = callBackFn;
5266 Grid.prototype.addHorizontalScrollSync = function (containerId, callBackFn) {
5267 this.horizontalScrollSyncCallBackFns[containerId] = callBackFn;
5271 * Scroll needed containers by calling their ScrollSyncs
5272 * @param sourceContainerId the containerId that has already set it's top/left.
5273 * can be empty string which means all containers need to set top/left
5274 * @param scrollEvent
5276 Grid.prototype.scrollContainers = function (sourceContainerId, scrollEvent) {
5278 if (scrollEvent.y) {
5279 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5280 var verts = ['body','left', 'right'];
5282 this.flagScrollingVertically(scrollEvent);
5284 if (sourceContainerId === 'body') {
5285 verts = ['left', 'right'];
5287 else if (sourceContainerId === 'left') {
5288 verts = ['body', 'right'];
5290 else if (sourceContainerId === 'right') {
5291 verts = ['body', 'left'];
5294 for (var i = 0; i < verts.length; i++) {
5296 if (this.verticalScrollSyncCallBackFns[id]) {
5297 this.verticalScrollSyncCallBackFns[id](scrollEvent);
5303 if (scrollEvent.x) {
5304 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5305 var horizs = ['body','bodyheader', 'bodyfooter'];
5307 this.flagScrollingHorizontally(scrollEvent);
5308 if (sourceContainerId === 'body') {
5309 horizs = ['bodyheader', 'bodyfooter'];
5312 for (var j = 0; j < horizs.length; j++) {
5313 var idh = horizs[j];
5314 if (this.horizontalScrollSyncCallBackFns[idh]) {
5315 this.horizontalScrollSyncCallBackFns[idh](scrollEvent);
5323 Grid.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
5324 this.viewportAdjusters.push(func);
5327 Grid.prototype.removeViewportAdjuster = function registerViewportAdjuster(func) {
5328 var idx = this.viewportAdjusters.indexOf(func);
5330 if (typeof(idx) !== 'undefined' && idx !== undefined) {
5331 this.viewportAdjusters.splice(idx, 1);
5335 Grid.prototype.getViewportAdjustment = function getViewportAdjustment() {
5338 var adjustment = { height: 0, width: 0 };
5340 self.viewportAdjusters.forEach(function (func) {
5341 adjustment = func.call(this, adjustment);
5347 Grid.prototype.getVisibleRowCount = function getVisibleRowCount() {
5350 // this.rows.forEach(function (row) {
5351 // if (row.visible) {
5356 // return this.visibleRowCache.length;
5357 return this.renderContainers.body.visibleRowCache.length;
5360 Grid.prototype.getVisibleRows = function getVisibleRows() {
5361 return this.renderContainers.body.visibleRowCache;
5364 Grid.prototype.getVisibleColumnCount = function getVisibleColumnCount() {
5367 // this.rows.forEach(function (row) {
5368 // if (row.visible) {
5373 // return this.visibleRowCache.length;
5374 return this.renderContainers.body.visibleColumnCache.length;
5378 Grid.prototype.searchRows = function searchRows(renderableRows) {
5379 return rowSearcher.search(this, renderableRows, this.columns);
5382 Grid.prototype.sortByColumn = function sortByColumn(renderableRows) {
5383 return rowSorter.sort(this, renderableRows, this.columns);
5388 * @name getCellValue
5389 * @methodOf ui.grid.class:Grid
5390 * @description Gets the value of a cell for a particular row and column
5391 * @param {GridRow} row Row to access
5392 * @param {GridColumn} col Column to access
5394 Grid.prototype.getCellValue = function getCellValue(row, col){
5395 if ( typeof(row.entity[ '$$' + col.uid ]) !== 'undefined' ) {
5396 return row.entity[ '$$' + col.uid].rendered;
5397 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined' ){
5398 return row.entity[col.field];
5400 if (!col.cellValueGetterCache) {
5401 col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col));
5404 return col.cellValueGetterCache(row);
5410 * @name getCellDisplayValue
5411 * @methodOf ui.grid.class:Grid
5412 * @description Gets the displayed value of a cell after applying any the `cellFilter`
5413 * @param {GridRow} row Row to access
5414 * @param {GridColumn} col Column to access
5416 Grid.prototype.getCellDisplayValue = function getCellDisplayValue(row, col) {
5417 if ( !col.cellDisplayGetterCache ) {
5418 var custom_filter = col.cellFilter ? " | " + col.cellFilter : "";
5420 if (typeof(row.entity['$$' + col.uid]) !== 'undefined') {
5421 col.cellDisplayGetterCache = $parse(row.entity['$$' + col.uid].rendered + custom_filter);
5422 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined') {
5423 col.cellDisplayGetterCache = $parse(row.entity[col.field] + custom_filter);
5425 col.cellDisplayGetterCache = $parse(row.getEntityQualifiedColField(col) + custom_filter);
5429 return col.cellDisplayGetterCache(row);
5433 Grid.prototype.getNextColumnSortPriority = function getNextColumnSortPriority() {
5437 self.columns.forEach(function (col) {
5438 if (col.sort && col.sort.priority !== undefined && col.sort.priority >= p) {
5439 p = col.sort.priority + 1;
5448 * @name resetColumnSorting
5449 * @methodOf ui.grid.class:Grid
5450 * @description Return the columns that the grid is currently being sorted by
5451 * @param {GridColumn} [excludedColumn] Optional GridColumn to exclude from having its sorting reset
5453 Grid.prototype.resetColumnSorting = function resetColumnSorting(excludeCol) {
5456 self.columns.forEach(function (col) {
5457 if (col !== excludeCol && !col.suppressRemoveSort) {
5465 * @name getColumnSorting
5466 * @methodOf ui.grid.class:Grid
5467 * @description Return the columns that the grid is currently being sorted by
5468 * @returns {Array[GridColumn]} An array of GridColumn objects
5470 Grid.prototype.getColumnSorting = function getColumnSorting() {
5473 var sortedCols = [], myCols;
5475 // Iterate through all the columns, sorted by priority
5476 // Make local copy of column list, because sorting is in-place and we do not want to
5477 // change the original sequence of columns
5478 myCols = self.columns.slice(0);
5479 myCols.sort(rowSorter.prioritySort).forEach(function (col) {
5480 if (col.sort && typeof(col.sort.direction) !== 'undefined' && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
5481 sortedCols.push(col);
5491 * @methodOf ui.grid.class:Grid
5492 * @description Set the sorting on a given column, optionally resetting any existing sorting on the Grid.
5493 * Emits the sortChanged event whenever the sort criteria are changed.
5494 * @param {GridColumn} column Column to set the sorting on
5495 * @param {uiGridConstants.ASC|uiGridConstants.DESC} [direction] Direction to sort by, either descending or ascending.
5496 * If not provided, the column will iterate through the sort directions
5497 * specified in the {@link ui.grid.class:GridOptions.columnDef#sortDirectionCycle sortDirectionCycle} attribute.
5498 * @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
5499 * by this column only
5500 * @returns {Promise} A resolved promise that supplies the column.
5503 Grid.prototype.sortColumn = function sortColumn(column, directionOrAdd, add) {
5507 if (typeof(column) === 'undefined' || !column) {
5508 throw new Error('No column parameter provided');
5511 // Second argument can either be a direction or whether to add this column to the existing sort.
5512 // If it's a boolean, it's an add, otherwise, it's a direction
5513 if (typeof(directionOrAdd) === 'boolean') {
5514 add = directionOrAdd;
5517 direction = directionOrAdd;
5521 self.resetColumnSorting(column);
5522 column.sort.priority = undefined;
5523 // Get the actual priority since there may be columns which have suppressRemoveSort set
5524 column.sort.priority = self.getNextColumnSortPriority();
5526 else if (!column.sort.priority){
5527 column.sort.priority = self.getNextColumnSortPriority();
5531 // Find the current position in the cycle (or -1).
5532 var i = column.sortDirectionCycle.indexOf(column.sort.direction ? column.sort.direction : null);
5533 // Proceed to the next position in the cycle (or start at the beginning).
5534 i = (i+1) % column.sortDirectionCycle.length;
5535 // If suppressRemoveSort is set, and the next position in the cycle would
5536 // remove the sort, skip it.
5537 if (column.colDef && column.suppressRemoveSort && !column.sortDirectionCycle[i]) {
5538 i = (i+1) % column.sortDirectionCycle.length;
5541 if (column.sortDirectionCycle[i]) {
5542 column.sort.direction = column.sortDirectionCycle[i];
5548 column.sort.direction = direction;
5551 self.api.core.raise.sortChanged( self, self.getColumnSorting() );
5553 return $q.when(column);
5557 * communicate to outside world that we are done with initial rendering
5559 Grid.prototype.renderingComplete = function(){
5560 if (angular.isFunction(this.options.onRegisterApi)) {
5561 this.options.onRegisterApi(this.api);
5563 this.api.core.raise.renderingComplete( this.api );
5566 Grid.prototype.createRowHashMap = function createRowHashMap() {
5569 var hashMap = new RowHashMap();
5570 hashMap.grid = self;
5579 * @methodOf ui.grid.class:Grid
5580 * @description Refresh the rendered grid on screen.
5581 * @param {boolean} [rowsAltered] Optional flag for refreshing when the number of rows has changed.
5583 Grid.prototype.refresh = function refresh(rowsAltered) {
5586 var p1 = self.processRowsProcessors(self.rows).then(function (renderableRows) {
5587 self.setVisibleRows(renderableRows);
5590 var p2 = self.processColumnsProcessors(self.columns).then(function (renderableColumns) {
5591 self.setVisibleColumns(renderableColumns);
5594 return $q.all([p1, p2]).then(function () {
5595 self.redrawInPlace(rowsAltered);
5597 self.refreshCanvas(true);
5604 * @methodOf ui.grid.class:Grid
5605 * @description Refresh the rendered rows on screen? Note: not functional at present
5606 * @returns {promise} promise that is resolved when render completes?
5609 Grid.prototype.refreshRows = function refreshRows() {
5612 return self.processRowsProcessors(self.rows)
5613 .then(function (renderableRows) {
5614 self.setVisibleRows(renderableRows);
5616 self.redrawInPlace();
5618 self.refreshCanvas( true );
5624 * @name refreshCanvas
5625 * @methodOf ui.grid.class:Grid
5626 * @description Builds all styles and recalculates much of the grid sizing
5627 * @param {object} buildStyles optional parameter. Use TBD
5628 * @returns {promise} promise that is resolved when the canvas
5629 * has been refreshed
5632 Grid.prototype.refreshCanvas = function(buildStyles) {
5641 // Get all the header heights
5642 var containerHeadersToRecalc = [];
5643 for (var containerId in self.renderContainers) {
5644 if (self.renderContainers.hasOwnProperty(containerId)) {
5645 var container = self.renderContainers[containerId];
5647 // Skip containers that have no canvasWidth set yet
5648 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5652 if (container.header || container.headerCanvas) {
5653 container.explicitHeaderHeight = container.explicitHeaderHeight || null;
5654 container.explicitHeaderCanvasHeight = container.explicitHeaderCanvasHeight || null;
5656 containerHeadersToRecalc.push(container);
5663 * Here we loop through the headers, measuring each element as well as any header "canvas" it has within it.
5665 * If any header is less than the largest header height, it will be resized to that so that we don't have headers
5666 * with different heights, which looks like a rendering problem
5668 * We'll do the same thing with the header canvases, and give the header CELLS an explicit height if their canvas
5669 * is smaller than the largest canvas height. That was header cells without extra controls like filtering don't
5670 * appear shorter than other cells.
5673 if (containerHeadersToRecalc.length > 0) {
5674 // Build the styles without the explicit header heights
5679 // Putting in a timeout as it's not calculating after the grid element is rendered and filled out
5680 $timeout(function() {
5681 // var oldHeaderHeight = self.grid.headerHeight;
5682 // self.grid.headerHeight = gridUtil.outerElementHeight(self.header);
5684 var rebuildStyles = false;
5686 // Get all the header heights
5687 var maxHeaderHeight = 0;
5688 var maxHeaderCanvasHeight = 0;
5690 var getHeight = function(oldVal, newVal){
5691 if ( oldVal !== newVal){
5692 rebuildStyles = true;
5696 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5697 container = containerHeadersToRecalc[i];
5699 // Skip containers that have no canvasWidth set yet
5700 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5704 if (container.header) {
5705 var headerHeight = container.headerHeight = getHeight(container.headerHeight, parseInt(gridUtil.outerElementHeight(container.header), 10));
5707 // Get the "inner" header height, that is the height minus the top and bottom borders, if present. We'll use it to make sure all the headers have a consistent height
5708 var topBorder = gridUtil.getBorderSize(container.header, 'top');
5709 var bottomBorder = gridUtil.getBorderSize(container.header, 'bottom');
5710 var innerHeaderHeight = parseInt(headerHeight - topBorder - bottomBorder, 10);
5712 innerHeaderHeight = innerHeaderHeight < 0 ? 0 : innerHeaderHeight;
5714 container.innerHeaderHeight = innerHeaderHeight;
5716 // If the header doesn't have an explicit height set, save the largest header height for use later
5717 // Explicit header heights are based off of the max we are calculating here. We never want to base the max on something we're setting explicitly
5718 if (!container.explicitHeaderHeight && innerHeaderHeight > maxHeaderHeight) {
5719 maxHeaderHeight = innerHeaderHeight;
5723 if (container.headerCanvas) {
5724 var headerCanvasHeight = container.headerCanvasHeight = getHeight(container.headerCanvasHeight, parseInt(gridUtil.outerElementHeight(container.headerCanvas), 10));
5727 // If the header doesn't have an explicit canvas height, save the largest header canvas height for use later
5728 // Explicit header heights are based off of the max we are calculating here. We never want to base the max on something we're setting explicitly
5729 if (!container.explicitHeaderCanvasHeight && headerCanvasHeight > maxHeaderCanvasHeight) {
5730 maxHeaderCanvasHeight = headerCanvasHeight;
5735 // Go through all the headers
5736 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5737 container = containerHeadersToRecalc[i];
5740 1. We have a max header height
5741 2. This container has a header height defined
5742 3. And either this container has an explicit header height set, OR its header height is less than the max
5746 Give this container's header an explicit height so it will line up with the tallest header
5749 maxHeaderHeight > 0 && typeof(container.headerHeight) !== 'undefined' && container.headerHeight !== null &&
5750 (container.explicitHeaderHeight || container.headerHeight < maxHeaderHeight)
5752 container.explicitHeaderHeight = getHeight(container.explicitHeaderHeight, maxHeaderHeight);
5755 // Do the same as above except for the header canvas
5757 maxHeaderCanvasHeight > 0 && typeof(container.headerCanvasHeight) !== 'undefined' && container.headerCanvasHeight !== null &&
5758 (container.explicitHeaderCanvasHeight || container.headerCanvasHeight < maxHeaderCanvasHeight)
5760 container.explicitHeaderCanvasHeight = getHeight(container.explicitHeaderCanvasHeight, maxHeaderCanvasHeight);
5764 // Rebuild styles if the header height has changed
5765 // The header height is used in body/viewport calculations and those are then used in other styles so we need it to be available
5766 if (buildStyles && rebuildStyles) {
5774 // Timeout still needs to be here to trigger digest after styles have been rebuilt
5775 $timeout(function() {
5786 * @name redrawCanvas
5787 * @methodOf ui.grid.class:Grid
5788 * @description Redraw the rows and columns based on our current scroll position
5789 * @param {boolean} [rowsAdded] Optional to indicate rows are added and the scroll percentage must be recalculated
5792 Grid.prototype.redrawInPlace = function redrawInPlace(rowsAdded) {
5793 // gridUtil.logDebug('redrawInPlace');
5797 for (var i in self.renderContainers) {
5798 var container = self.renderContainers[i];
5800 // gridUtil.logDebug('redrawing container', i);
5803 container.adjustRows(container.prevScrollTop, null);
5804 container.adjustColumns(container.prevScrollLeft, null);
5807 container.adjustRows(null, container.prevScrolltopPercentage);
5808 container.adjustColumns(null, container.prevScrollleftPercentage);
5815 * @name hasLeftContainerColumns
5816 * @methodOf ui.grid.class:Grid
5817 * @description returns true if leftContainer has columns
5819 Grid.prototype.hasLeftContainerColumns = function () {
5820 return this.hasLeftContainer() && this.renderContainers.left.renderedColumns.length > 0;
5825 * @name hasRightContainerColumns
5826 * @methodOf ui.grid.class:Grid
5827 * @description returns true if rightContainer has columns
5829 Grid.prototype.hasRightContainerColumns = function () {
5830 return this.hasRightContainer() && this.renderContainers.right.renderedColumns.length > 0;
5835 * @methodOf ui.grid.class:Grid
5836 * @name scrollToIfNecessary
5837 * @description Scrolls the grid to make a certain row and column combo visible,
5838 * in the case that it is not completely visible on the screen already.
5839 * @param {GridRow} gridRow row to make visible
5840 * @param {GridCol} gridCol column to make visible
5841 * @returns {promise} a promise that is resolved when scrolling is complete
5843 Grid.prototype.scrollToIfNecessary = function (gridRow, gridCol) {
5846 var scrollEvent = new ScrollEvent(self, 'uiGrid.scrollToIfNecessary');
5848 // Alias the visible row and column caches
5849 var visRowCache = self.renderContainers.body.visibleRowCache;
5850 var visColCache = self.renderContainers.body.visibleColumnCache;
5852 /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/
5854 // The top boundary is the current Y scroll position PLUS the header height, because the header can obscure rows when the grid is scrolled downwards
5855 var topBound = self.renderContainers.body.prevScrollTop + self.headerHeight;
5857 // Don't the let top boundary be less than 0
5858 topBound = (topBound < 0) ? 0 : topBound;
5860 // The left boundary is the current X scroll position
5861 var leftBound = self.renderContainers.body.prevScrollLeft;
5863 // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height.
5864 // Basically this is the viewport height added on to the scroll position
5865 var bottomBound = self.renderContainers.body.prevScrollTop + self.gridHeight - self.renderContainers.body.headerHeight - self.footerHeight - self.scrollbarWidth;
5867 // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows
5868 //if (self.horizontalScrollbarHeight) {
5869 // bottomBound = bottomBound - self.horizontalScrollbarHeight;
5872 // The right position is the current X scroll position minus the grid width
5873 var rightBound = self.renderContainers.body.prevScrollLeft + Math.ceil(self.renderContainers.body.getViewportWidth());
5875 // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells
5876 //if (self.verticalScrollbarWidth) {
5877 // rightBound = rightBound - self.verticalScrollbarWidth;
5880 // We were given a row to scroll to
5881 if (gridRow !== null) {
5882 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5883 var seekRowIndex = visRowCache.indexOf(gridRow);
5885 // Total vertical scroll length of the grid
5886 var scrollLength = (self.renderContainers.body.getCanvasHeight() - self.renderContainers.body.getViewportHeight());
5888 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5889 //if (self.horizontalScrollbarHeight && self.horizontalScrollbarHeight > 0) {
5890 // scrollLength = scrollLength + self.horizontalScrollbarHeight;
5893 // This is the minimum amount of pixels we need to scroll vertical in order to see this row.
5894 var pixelsToSeeRow = (seekRowIndex * self.options.rowHeight + self.headerHeight);
5896 // Don't let the pixels required to see the row be less than zero
5897 pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow;
5899 var scrollPixels, percentage;
5901 // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the self...
5902 if (pixelsToSeeRow < topBound) {
5903 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5904 // to get the full position we need
5905 scrollPixels = self.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow);
5907 // Turn the scroll position into a percentage and make it an argument for a scroll event
5908 percentage = scrollPixels / scrollLength;
5909 scrollEvent.y = { percentage: percentage };
5911 // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the self...
5912 else if (pixelsToSeeRow > bottomBound) {
5913 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5914 // to get the full position we need
5915 scrollPixels = pixelsToSeeRow - bottomBound + self.renderContainers.body.prevScrollTop;
5917 // Turn the scroll position into a percentage and make it an argument for a scroll event
5918 percentage = scrollPixels / scrollLength;
5919 scrollEvent.y = { percentage: percentage };
5923 // We were given a column to scroll to
5924 if (gridCol !== null) {
5925 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5926 var seekColumnIndex = visColCache.indexOf(gridCol);
5928 // Total vertical scroll length of the grid
5929 var horizScrollLength = (self.renderContainers.body.getCanvasWidth() - self.renderContainers.body.getViewportWidth());
5931 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5932 // if (self.verticalScrollbarWidth && self.verticalScrollbarWidth > 0) {
5933 // horizScrollLength = horizScrollLength + self.verticalScrollbarWidth;
5936 // This is the minimum amount of pixels we need to scroll vertical in order to see this column
5937 var columnLeftEdge = 0;
5938 for (var i = 0; i < seekColumnIndex; i++) {
5939 var col = visColCache[i];
5940 columnLeftEdge += col.drawnWidth;
5942 columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge;
5944 var columnRightEdge = columnLeftEdge + gridCol.drawnWidth;
5946 // Don't let the pixels required to see the column be less than zero
5947 columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge;
5949 var horizScrollPixels, horizPercentage;
5951 // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the self...
5952 if (columnLeftEdge < leftBound) {
5953 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5954 // to get the full position we need
5955 horizScrollPixels = self.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge);
5957 // Turn the scroll position into a percentage and make it an argument for a scroll event
5958 horizPercentage = horizScrollPixels / horizScrollLength;
5959 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5960 scrollEvent.x = { percentage: horizPercentage };
5962 // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the self...
5963 else if (columnRightEdge > rightBound) {
5964 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5965 // to get the full position we need
5966 horizScrollPixels = columnRightEdge - rightBound + self.renderContainers.body.prevScrollLeft;
5968 // Turn the scroll position into a percentage and make it an argument for a scroll event
5969 horizPercentage = horizScrollPixels / horizScrollLength;
5970 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5971 scrollEvent.x = { percentage: horizPercentage };
5975 var deferred = $q.defer();
5977 // If we need to scroll on either the x or y axes, fire a scroll event
5978 if (scrollEvent.y || scrollEvent.x) {
5979 scrollEvent.withDelay = false;
5980 self.scrollContainers('',scrollEvent);
5981 var dereg = self.api.core.on.scrollEnd(null,function() {
5982 deferred.resolve(scrollEvent);
5990 return deferred.promise;
5995 * @methodOf ui.grid.class:Grid
5997 * @description Scroll the grid such that the specified
5998 * row and column is in view
5999 * @param {object} rowEntity gridOptions.data[] array instance to make visible
6000 * @param {object} colDef to make visible
6001 * @returns {promise} a promise that is resolved after any scrolling is finished
6003 Grid.prototype.scrollTo = function (rowEntity, colDef) {
6004 var gridRow = null, gridCol = null;
6006 if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) {
6007 gridRow = this.getRow(rowEntity);
6010 if (colDef !== null && typeof(colDef) !== 'undefined' ) {
6011 gridCol = this.getColumn(colDef.name ? colDef.name : colDef.field);
6013 return this.scrollToIfNecessary(gridRow, gridCol);
6018 * @name clearAllFilters
6019 * @methodOf ui.grid.class:Grid
6020 * @description Clears all filters and optionally refreshes the visible rows.
6021 * @param {object} refreshRows Defaults to true.
6022 * @param {object} clearConditions Defaults to false.
6023 * @param {object} clearFlags Defaults to false.
6024 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
6026 Grid.prototype.clearAllFilters = function clearAllFilters(refreshRows, clearConditions, clearFlags) {
6027 // Default `refreshRows` to true because it will be the most commonly desired behaviour.
6028 if (refreshRows === undefined) {
6031 if (clearConditions === undefined) {
6032 clearConditions = false;
6034 if (clearFlags === undefined) {
6038 this.columns.forEach(function(column) {
6039 column.filters.forEach(function(filter) {
6040 filter.term = undefined;
6042 if (clearConditions) {
6043 filter.condition = undefined;
6047 filter.flags = undefined;
6053 return this.refreshRows();
6058 // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?)
6059 function RowHashMap() {}
6061 RowHashMap.prototype = {
6063 * Store key value pair
6064 * @param key key to store can be any type
6065 * @param value value to store can be any type
6067 put: function(key, value) {
6068 this[this.grid.options.rowIdentity(key)] = value;
6073 * @returns {Object} the value for the key
6075 get: function(key) {
6076 return this[this.grid.options.rowIdentity(key)];
6080 * Remove the key/value pair
6083 remove: function(key) {
6084 var value = this[key = this.grid.options.rowIdentity(key)];
6100 angular.module('ui.grid')
6101 .factory('GridApi', ['$q', '$rootScope', 'gridUtil', 'uiGridConstants', 'GridRow', 'uiGridGridMenuService',
6102 function ($q, $rootScope, gridUtil, uiGridConstants, GridRow, uiGridGridMenuService) {
6105 * @name ui.grid.class:GridApi
6106 * @description GridApi provides the ability to register public methods events inside the grid and allow
6107 * for other components to use the api via featureName.raise.methodName and featureName.on.eventName(function(args){}.
6109 * To listen to events, you must add a callback to gridOptions.onRegisterApi
6111 * $scope.gridOptions.onRegisterApi = function(gridApi){
6112 * gridApi.cellNav.on.navigate($scope,function(newRowCol, oldRowCol){
6113 * $log.log('navigation event');
6117 * @param {object} grid grid that owns api
6119 var GridApi = function GridApi(grid) {
6121 this.listeners = [];
6125 * @name renderingComplete
6126 * @methodOf ui.grid.core.api:PublicApi
6127 * @description Rendering is complete, called at the same
6128 * time as `onRegisterApi`, but provides a way to obtain
6129 * that same event within features without stopping end
6130 * users from getting at the onRegisterApi method.
6132 * Included in gridApi so that it's always there - otherwise
6133 * there is still a timing problem with when a feature can
6136 * @param {GridApi} gridApi the grid api, as normally
6137 * returned in the onRegisterApi method
6141 * gridApi.core.on.renderingComplete( grid );
6144 this.registerEvent( 'core', 'renderingComplete' );
6148 * @name filterChanged
6149 * @eventOf ui.grid.core.api:PublicApi
6150 * @description is raised after the filter is changed. The nature
6151 * of the watch expression doesn't allow notification of what changed,
6152 * so the receiver of this event will need to re-extract the filter
6153 * conditions from the columns.
6156 this.registerEvent( 'core', 'filterChanged' );
6160 * @name setRowInvisible
6161 * @methodOf ui.grid.core.api:PublicApi
6162 * @description Sets an override on the row to make it always invisible,
6163 * which will override any filtering or other visibility calculations.
6164 * If the row is currently visible then sets it to invisible and calls
6165 * both grid refresh and emits the rowsVisibleChanged event
6166 * @param {object} rowEntity gridOptions.data[] array instance
6168 this.registerMethod( 'core', 'setRowInvisible', GridRow.prototype.setRowInvisible );
6172 * @name clearRowInvisible
6173 * @methodOf ui.grid.core.api:PublicApi
6174 * @description Clears any override on visibility for the row so that it returns to
6175 * using normal filtering and other visibility calculations.
6176 * If the row is currently invisible then sets it to visible and calls
6177 * both grid refresh and emits the rowsVisibleChanged event
6178 * TODO: if a filter is active then we can't just set it to visible?
6179 * @param {object} rowEntity gridOptions.data[] array instance
6181 this.registerMethod( 'core', 'clearRowInvisible', GridRow.prototype.clearRowInvisible );
6185 * @name getVisibleRows
6186 * @methodOf ui.grid.core.api:PublicApi
6187 * @description Returns all visible rows
6188 * @param {Grid} grid the grid you want to get visible rows from
6189 * @returns {array} an array of gridRow
6191 this.registerMethod( 'core', 'getVisibleRows', this.grid.getVisibleRows );
6195 * @name rowsVisibleChanged
6196 * @eventOf ui.grid.core.api:PublicApi
6197 * @description is raised after the rows that are visible
6198 * change. The filtering is zero-based, so it isn't possible
6199 * to say which rows changed (unlike in the selection feature).
6200 * We can plausibly know which row was changed when setRowInvisible
6201 * is called, but in that situation the user already knows which row
6202 * they changed. When a filter runs we don't know what changed,
6203 * and that is the one that would have been useful.
6206 this.registerEvent( 'core', 'rowsVisibleChanged' );
6210 * @name rowsRendered
6211 * @eventOf ui.grid.core.api:PublicApi
6212 * @description is raised after the cache of visible rows is changed.
6214 this.registerEvent( 'core', 'rowsRendered' );
6220 * @eventOf ui.grid.core.api:PublicApi
6221 * @description is raised when scroll begins. Is throttled, so won't be raised too frequently
6223 this.registerEvent( 'core', 'scrollBegin' );
6228 * @eventOf ui.grid.core.api:PublicApi
6229 * @description is raised when scroll has finished. Is throttled, so won't be raised too frequently
6231 this.registerEvent( 'core', 'scrollEnd' );
6235 * @name canvasHeightChanged
6236 * @eventOf ui.grid.core.api:PublicApi
6237 * @description is raised when the canvas height has changed
6239 * arguments: oldHeight, newHeight
6241 this.registerEvent( 'core', 'canvasHeightChanged');
6246 * @name ui.grid.class:suppressEvents
6247 * @methodOf ui.grid.class:GridApi
6248 * @description Used to execute a function while disabling the specified event listeners.
6249 * Disables the listenerFunctions, executes the callbackFn, and then enables
6250 * the listenerFunctions again
6251 * @param {object} listenerFuncs listenerFunc or array of listenerFuncs to suppress. These must be the same
6252 * functions that were used in the .on.eventName method
6253 * @param {object} callBackFn function to execute
6256 * var navigate = function (newRowCol, oldRowCol){
6257 * //do something on navigate
6260 * gridApi.cellNav.on.navigate(scope,navigate);
6263 * //call the scrollTo event and suppress our navigate listener
6264 * //scrollTo will still raise the event for other listeners
6265 * gridApi.suppressEvents(navigate, function(){
6266 * gridApi.cellNav.scrollTo(aRow, aCol);
6271 GridApi.prototype.suppressEvents = function (listenerFuncs, callBackFn) {
6273 var listeners = angular.isArray(listenerFuncs) ? listenerFuncs : [listenerFuncs];
6275 //find all registered listeners
6276 var foundListeners = self.listeners.filter(function(listener) {
6277 return listeners.some(function(l) {
6278 return listener.handler === l;
6282 //deregister all the listeners
6283 foundListeners.forEach(function(l){
6289 //reregister all the listeners
6290 foundListeners.forEach(function(l){
6291 l.dereg = registerEventWithAngular(l.eventId, l.handler, self.grid, l._this);
6298 * @name registerEvent
6299 * @methodOf ui.grid.class:GridApi
6300 * @description Registers a new event for the given feature. The event will get a
6301 * .raise and .on prepended to it
6303 * .raise.eventName() - takes no arguments
6306 * .on.eventName(scope, callBackFn, _this)
6308 * scope - a scope reference to add a deregister call to the scopes .$on('destroy'). Scope is optional and can be a null value,
6309 * but in this case you must deregister it yourself via the returned deregister function
6311 * callBackFn - The function to call
6313 * _this - optional this context variable for callbackFn. If omitted, grid.api will be used for the context
6315 * .on.eventName returns a dereg funtion that will remove the listener. It's not necessary to use it as the listener
6316 * will be removed when the scope is destroyed.
6317 * @param {string} featureName name of the feature that raises the event
6318 * @param {string} eventName name of the event
6320 GridApi.prototype.registerEvent = function (featureName, eventName) {
6322 if (!self[featureName]) {
6323 self[featureName] = {};
6326 var feature = self[featureName];
6332 var eventId = self.grid.id + featureName + eventName;
6334 // gridUtil.logDebug('Creating raise event method ' + featureName + '.raise.' + eventName);
6335 feature.raise[eventName] = function () {
6336 $rootScope.$emit.apply($rootScope, [eventId].concat(Array.prototype.slice.call(arguments)));
6339 // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName);
6340 feature.on[eventName] = function (scope, handler, _this) {
6341 if ( scope !== null && typeof(scope.$on) === 'undefined' ){
6342 gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters. It is legitimate to pass null, but you\'ve passed something else, so you probably forgot to provide scope rather than did it deliberately, not registering');
6345 var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this);
6347 //track our listener so we can turn off and on
6348 var listener = {handler: handler, dereg: deregAngularOn, eventId: eventId, scope: scope, _this:_this};
6349 self.listeners.push(listener);
6351 var removeListener = function(){
6353 var index = self.listeners.indexOf(listener);
6354 self.listeners.splice(index,1);
6357 //destroy tracking when scope is destroyed
6359 scope.$on('$destroy', function() {
6365 return removeListener;
6369 function registerEventWithAngular(eventId, handler, grid, _this) {
6370 return $rootScope.$on(eventId, function (event) {
6371 var args = Array.prototype.slice.call(arguments);
6372 args.splice(0, 1); //remove evt argument
6373 handler.apply(_this ? _this : grid.api, args);
6379 * @name registerEventsFromObject
6380 * @methodOf ui.grid.class:GridApi
6381 * @description Registers features and events from a simple objectMap.
6382 * eventObjectMap must be in this format (multiple features allowed)
6386 * eventNameOne:function(args){},
6387 * eventNameTwo:function(args){}
6391 * @param {object} eventObjectMap map of feature/event names
6393 GridApi.prototype.registerEventsFromObject = function (eventObjectMap) {
6396 angular.forEach(eventObjectMap, function (featProp, featPropName) {
6397 var feature = {name: featPropName, events: []};
6398 angular.forEach(featProp, function (prop, propName) {
6399 feature.events.push(propName);
6401 features.push(feature);
6404 features.forEach(function (feature) {
6405 feature.events.forEach(function (event) {
6406 self.registerEvent(feature.name, event);
6414 * @name registerMethod
6415 * @methodOf ui.grid.class:GridApi
6416 * @description Registers a new event for the given feature
6417 * @param {string} featureName name of the feature
6418 * @param {string} methodName name of the method
6419 * @param {object} callBackFn function to execute
6420 * @param {object} _this binds callBackFn 'this' to _this. Defaults to gridApi.grid
6422 GridApi.prototype.registerMethod = function (featureName, methodName, callBackFn, _this) {
6423 if (!this[featureName]) {
6424 this[featureName] = {};
6427 var feature = this[featureName];
6429 feature[methodName] = gridUtil.createBoundedWrapper(_this || this.grid, callBackFn);
6434 * @name registerMethodsFromObject
6435 * @methodOf ui.grid.class:GridApi
6436 * @description Registers features and methods from a simple objectMap.
6437 * eventObjectMap must be in this format (multiple features allowed)
6441 * methodNameOne:function(args){},
6442 * methodNameTwo:function(args){}
6444 * @param {object} eventObjectMap map of feature/event names
6445 * @param {object} _this binds this to _this for all functions. Defaults to gridApi.grid
6447 GridApi.prototype.registerMethodsFromObject = function (methodMap, _this) {
6450 angular.forEach(methodMap, function (featProp, featPropName) {
6451 var feature = {name: featPropName, methods: []};
6452 angular.forEach(featProp, function (prop, propName) {
6453 feature.methods.push({name: propName, fn: prop});
6455 features.push(feature);
6458 features.forEach(function (feature) {
6459 feature.methods.forEach(function (method) {
6460 self.registerMethod(feature.name, method.name, method.fn, _this);
6474 angular.module('ui.grid')
6475 .factory('GridColumn', ['gridUtil', 'uiGridConstants', 'i18nService', function(gridUtil, uiGridConstants, i18nService) {
6478 * ******************************************************************************************
6479 * PaulL1: Ugly hack here in documentation. These properties are clearly properties of GridColumn,
6480 * and need to be noted as such for those extending and building ui-grid itself.
6481 * However, from an end-developer perspective, they interact with all these through columnDefs,
6482 * and they really need to be documented there. I feel like they're relatively static, and
6483 * I can't find an elegant way for ngDoc to reference to both....so I've duplicated each
6484 * comment block. Ugh.
6491 * @propertyOf ui.grid.class:GridColumn
6492 * @description (mandatory) each column should have a name, although for backward
6493 * compatibility with 2.x name can be omitted if field is present
6500 * @propertyOf ui.grid.class:GridOptions.columnDef
6501 * @description (mandatory) each column should have a name, although for backward
6502 * compatibility with 2.x name can be omitted if field is present
6509 * @propertyOf ui.grid.class:GridColumn
6510 * @description Column name that will be shown in the header. If displayName is not
6511 * provided then one is generated using the name.
6518 * @propertyOf ui.grid.class:GridOptions.columnDef
6519 * @description Column name that will be shown in the header. If displayName is not
6520 * provided then one is generated using the name.
6527 * @propertyOf ui.grid.class:GridColumn
6528 * @description field must be provided if you wish to bind to a
6529 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6530 * array element. Can be a complex expression: <code>employee.address.city</code>, or can be a function: <code>employee.getFullAddress()</code>.
6531 * See the angular docs on binding expressions.
6538 * @propertyOf ui.grid.class:GridOptions.columnDef
6539 * @description field must be provided if you wish to bind to a
6540 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6541 * array element. Can be a complex expression: <code>employee.address.city</code>, or can be a function: <code>employee.getFullAddress()</code>. * See the angular docs on binding expressions. *
6547 * @propertyOf ui.grid.class:GridColumn
6548 * @description Filter on this column.
6550 * <pre>{ term: 'text', condition: uiGridConstants.filter.STARTS_WITH, placeholder: 'type to filter...', ariaLabel: 'Filter for text', flags: { caseSensitive: false }, type: uiGridConstants.filter.SELECT, [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] }</pre>
6556 * @name ui.grid.class:GridColumn
6557 * @description Represents the viewModel for each column. Any state or methods needed for a Grid Column
6558 * are defined on this prototype
6559 * @param {ColumnDef} colDef the column def to associate with this column
6560 * @param {number} uid the unique and immutable uid we'd like to allocate to this column
6561 * @param {Grid} grid the grid we'd like to create this column in
6563 function GridColumn(colDef, uid, grid) {
6569 self.updateColumnDef(colDef, true);
6571 self.aggregationValue = undefined;
6573 // The footer cell registers to listen for the rowsRendered event, and calls this. Needed to be
6574 // in something with a scope so that the dereg would get called
6575 self.updateAggregationValue = function() {
6577 // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name);
6581 * @name aggregationType
6582 * @propertyOf ui.grid.class:GridOptions.columnDef
6583 * @description The aggregation that you'd like to show in the columnFooter for this
6584 * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`,
6585 * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`,
6586 * `uiGridConstants.aggregationTypes.max`.
6588 * You can also provide a function as the aggregation type, in this case your function needs to accept the full
6589 * set of visible rows, and return a value that should be shown
6591 if (!self.aggregationType) {
6592 self.aggregationValue = undefined;
6597 var visibleRows = self.grid.getVisibleRows();
6599 var cellValues = function(){
6601 visibleRows.forEach(function (row) {
6602 var cellValue = self.grid.getCellValue(row, self);
6603 var cellNumber = Number(cellValue);
6604 if (!isNaN(cellNumber)) {
6605 values.push(cellNumber);
6611 if (angular.isFunction(self.aggregationType)) {
6612 self.aggregationValue = self.aggregationType(visibleRows, self);
6614 else if (self.aggregationType === uiGridConstants.aggregationTypes.count) {
6615 self.aggregationValue = self.grid.getVisibleRowCount();
6617 else if (self.aggregationType === uiGridConstants.aggregationTypes.sum) {
6618 cellValues().forEach(function (value) {
6621 self.aggregationValue = result;
6623 else if (self.aggregationType === uiGridConstants.aggregationTypes.avg) {
6624 cellValues().forEach(function (value) {
6627 result = result / cellValues().length;
6628 self.aggregationValue = result;
6630 else if (self.aggregationType === uiGridConstants.aggregationTypes.min) {
6631 self.aggregationValue = Math.min.apply(null, cellValues());
6633 else if (self.aggregationType === uiGridConstants.aggregationTypes.max) {
6634 self.aggregationValue = Math.max.apply(null, cellValues());
6637 self.aggregationValue = '\u00A0';
6641 // var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true, context: self.name });
6645 * @name getAggregationValue
6646 * @methodOf ui.grid.class:GridColumn
6647 * @description gets the aggregation value based on the aggregation type for this column.
6648 * Debounced using scrollDebounce option setting
6650 this.getAggregationValue = function() {
6651 // if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) {
6652 // throttledUpdateAggregationValue();
6655 return self.aggregationValue;
6662 * @methodOf ui.grid.class:GridColumn
6663 * @description Hides the column by setting colDef.visible = false
6665 GridColumn.prototype.hideColumn = function() {
6666 this.colDef.visible = false;
6672 * @methodOf ui.grid.class:GridColumn
6673 * @name setPropertyOrDefault
6674 * @description Sets a property on the column using the passed in columnDef, and
6675 * setting the defaultValue if the value cannot be found on the colDef
6676 * @param {ColumnDef} colDef the column def to look in for the property value
6677 * @param {string} propName the property name we'd like to set
6678 * @param {object} defaultValue the value to use if the colDef doesn't provide the setting
6680 GridColumn.prototype.setPropertyOrDefault = function (colDef, propName, defaultValue) {
6683 // Use the column definition filter if we were passed it
6684 if (typeof(colDef[propName]) !== 'undefined' && colDef[propName]) {
6685 self[propName] = colDef[propName];
6687 // Otherwise use our own if it's set
6688 else if (typeof(self[propName]) !== 'undefined') {
6689 self[propName] = self[propName];
6691 // Default to empty object for the filter
6693 self[propName] = defaultValue ? defaultValue : {};
6702 * @propertyOf ui.grid.class:GridOptions.columnDef
6703 * @description sets the column width. Can be either
6704 * a number or a percentage, or an * for auto.
6706 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', width: 100},
6707 * { field: 'field2', width: '20%'},
6708 * { field: 'field3', width: '*' }]; </pre>
6715 * @propertyOf ui.grid.class:GridOptions.columnDef
6716 * @description sets the minimum column width. Should be a number.
6718 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', minWidth: 100}]; </pre>
6725 * @propertyOf ui.grid.class:GridOptions.columnDef
6726 * @description sets the maximum column width. Should be a number.
6728 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', maxWidth: 100}]; </pre>
6735 * @propertyOf ui.grid.class:GridOptions.columnDef
6736 * @description sets whether or not the column is visible
6737 * </br>Default is true
6739 * <pre> $scope.gridOptions.columnDefs = [
6740 * { field: 'field1', visible: true},
6741 * { field: 'field2', visible: false }
6749 * @propertyOf ui.grid.class:GridOptions.columnDef
6750 * @description An object of sort information, attributes are:
6752 * - direction: values are uiGridConstants.ASC or uiGridConstants.DESC
6753 * - ignoreSort: if set to true this sort is ignored (used by tree to manipulate the sort functionality)
6754 * - priority: says what order to sort the columns in (lower priority gets sorted first).
6757 * $scope.gridOptions.columnDefs = [{
6760 * direction: uiGridConstants.ASC,
6771 * @name sortingAlgorithm
6772 * @propertyOf ui.grid.class:GridOptions.columnDef
6773 * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters
6774 * like any normal sorting function with additional 'rowA', 'rowB', and 'direction' parameters
6775 * that are the row objects and the current direction of the sort respectively.
6782 * @propertyOf ui.grid.class:GridOptions.columnDef
6783 * @description Specify multiple filter fields.
6785 * <pre>$scope.gridOptions.columnDefs = [
6787 * field: 'field1', filters: [
6790 * condition: uiGridConstants.filter.STARTS_WITH,
6791 * placeholder: 'starts with...',
6792 * ariaLabel: 'Filter for field1',
6793 * flags: { caseSensitive: false },
6794 * type: uiGridConstants.filter.SELECT,
6795 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6798 * condition: uiGridConstants.filter.ENDS_WITH,
6799 * placeholder: 'ends with...'
6811 * @propertyOf ui.grid.class:GridColumn
6812 * @description Filters for this column. Includes 'term' property bound to filter input elements.
6816 * term: 'foo', // ngModel for <input>
6817 * condition: uiGridConstants.filter.STARTS_WITH,
6818 * placeholder: 'starts with...',
6819 * ariaLabel: 'Filter for foo',
6820 * flags: { caseSensitive: false },
6821 * type: uiGridConstants.filter.SELECT,
6822 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6826 * condition: uiGridConstants.filter.ENDS_WITH,
6827 * placeholder: 'ends with...'
6837 * @propertyOf ui.grid.class:GridOptions.columnDef
6838 * @description used to add menu items to a column. Refer to the tutorial on this
6839 * functionality. A number of settings are supported:
6841 * - title: controls the title that is displayed in the menu
6842 * - icon: the icon shown alongside that title
6843 * - action: the method to call when the menu is clicked
6844 * - shown: a function to evaluate to determine whether or not to show the item
6845 * - active: a function to evaluate to determine whether or not the item is currently selected
6846 * - context: context to pass to the action function, available in this.context in your handler
6847 * - leaveOpen: if set to true, the menu should stay open after the action, defaults to false
6849 * <pre> $scope.gridOptions.columnDefs = [
6850 * { field: 'field1', menuItems: [
6852 * title: 'Outer Scope Alert',
6853 * icon: 'ui-grid-icon-info-circled',
6854 * action: function($event) {
6855 * this.context.blargh(); // $scope.blargh() would work too, this is just an example
6857 * shown: function() { return true; },
6858 * active: function() { return true; },
6863 * action: function() {
6864 * alert('Grid ID: ' + this.grid.id);
6873 * @methodOf ui.grid.class:GridColumn
6874 * @name updateColumnDef
6875 * @description Moves settings from the columnDef down onto the column,
6876 * and sets properties as appropriate
6877 * @param {ColumnDef} colDef the column def to look in for the property value
6878 * @param {boolean} isNew whether the column is being newly created, if not
6879 * we're updating an existing column, and some items such as the sort shouldn't
6882 GridColumn.prototype.updateColumnDef = function(colDef, isNew) {
6885 self.colDef = colDef;
6887 if (colDef.name === undefined) {
6888 throw new Error('colDef.name is required for column at index ' + self.grid.options.columnDefs.indexOf(colDef));
6891 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6893 if (!angular.isNumber(self.width) || !self.hasCustomWidth || colDef.allowCustomWidthOverride) {
6894 var colDefWidth = colDef.width;
6895 var parseErrorMsg = "Cannot parse column width '" + colDefWidth + "' for column named '" + colDef.name + "'";
6896 self.hasCustomWidth = false;
6898 if (!angular.isString(colDefWidth) && !angular.isNumber(colDefWidth)) {
6900 } else if (angular.isString(colDefWidth)) {
6901 // See if it ends with a percent
6902 if (gridUtil.endsWith(colDefWidth, '%')) {
6903 // If so we should be able to parse the non-percent-sign part to a number
6904 var percentStr = colDefWidth.replace(/%/g, '');
6905 var percent = parseInt(percentStr, 10);
6906 if (isNaN(percent)) {
6907 throw new Error(parseErrorMsg);
6909 self.width = colDefWidth;
6911 // And see if it's a number string
6912 else if (colDefWidth.match(/^(\d+)$/)) {
6913 self.width = parseInt(colDefWidth.match(/^(\d+)$/)[1], 10);
6915 // Otherwise it should be a string of asterisks
6916 else if (colDefWidth.match(/^\*+$/)) {
6917 self.width = colDefWidth;
6919 // No idea, throw an Error
6921 throw new Error(parseErrorMsg);
6924 // Is a number, use it as the width
6926 self.width = colDefWidth;
6930 ['minWidth', 'maxWidth'].forEach(function (name) {
6931 var minOrMaxWidth = colDef[name];
6932 var parseErrorMsg = "Cannot parse column " + name + " '" + minOrMaxWidth + "' for column named '" + colDef.name + "'";
6934 if (!angular.isString(minOrMaxWidth) && !angular.isNumber(minOrMaxWidth)) {
6935 //Sets default minWidth and maxWidth values
6936 self[name] = ((name === 'minWidth') ? 30 : 9000);
6937 } else if (angular.isString(minOrMaxWidth)) {
6938 if (minOrMaxWidth.match(/^(\d+)$/)) {
6939 self[name] = parseInt(minOrMaxWidth.match(/^(\d+)$/)[1], 10);
6941 throw new Error(parseErrorMsg);
6944 self[name] = minOrMaxWidth;
6948 //use field if it is defined; name if it is not
6949 self.field = (colDef.field === undefined) ? colDef.name : colDef.field;
6951 if ( typeof( self.field ) !== 'string' ){
6952 gridUtil.logError( 'Field is not a string, this is likely to break the code, Field is: ' + self.field );
6955 self.name = colDef.name;
6957 // Use colDef.displayName as long as it's not undefined, otherwise default to the field name
6958 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6960 //self.originalIndex = index;
6962 self.aggregationType = angular.isDefined(colDef.aggregationType) ? colDef.aggregationType : null;
6963 self.footerCellTemplate = angular.isDefined(colDef.footerCellTemplate) ? colDef.footerCellTemplate : null;
6968 * @propertyOf ui.grid.class:GridOptions.columnDef
6969 * @description Whether or not to show a tooltip when a user hovers over the cell.
6970 * If set to false, no tooltip. If true, the cell value is shown in the tooltip (useful
6971 * if you have long values in your cells), if a function then that function is called
6972 * passing in the row and the col `cellTooltip( row, col )`, and the return value is shown in the tooltip,
6973 * if it is a static string then displays that static string.
6978 if ( typeof(colDef.cellTooltip) === 'undefined' || colDef.cellTooltip === false ) {
6979 self.cellTooltip = false;
6980 } else if ( colDef.cellTooltip === true ){
6981 self.cellTooltip = function(row, col) {
6982 return self.grid.getCellValue( row, col );
6984 } else if (typeof(colDef.cellTooltip) === 'function' ){
6985 self.cellTooltip = colDef.cellTooltip;
6987 self.cellTooltip = function ( row, col ){
6988 return col.colDef.cellTooltip;
6994 * @name headerTooltip
6995 * @propertyOf ui.grid.class:GridOptions.columnDef
6996 * @description Whether or not to show a tooltip when a user hovers over the header cell.
6997 * If set to false, no tooltip. If true, the displayName is shown in the tooltip (useful
6998 * if you have long values in your headers), if a function then that function is called
6999 * passing in the row and the col `headerTooltip( col )`, and the return value is shown in the tooltip,
7000 * if a static string then shows that static string.
7005 if ( typeof(colDef.headerTooltip) === 'undefined' || colDef.headerTooltip === false ) {
7006 self.headerTooltip = false;
7007 } else if ( colDef.headerTooltip === true ){
7008 self.headerTooltip = function(col) {
7009 return col.displayName;
7011 } else if (typeof(colDef.headerTooltip) === 'function' ){
7012 self.headerTooltip = colDef.headerTooltip;
7014 self.headerTooltip = function ( col ) {
7015 return col.colDef.headerTooltip;
7022 * @name footerCellClass
7023 * @propertyOf ui.grid.class:GridOptions.columnDef
7024 * @description footerCellClass can be a string specifying the class to append to a cell
7025 * or it can be a function(grid, row, col, rowRenderIndex, colRenderIndex) that returns a class name
7028 self.footerCellClass = colDef.footerCellClass;
7033 * @propertyOf ui.grid.class:GridOptions.columnDef
7034 * @description cellClass can be a string specifying the class to append to a cell
7035 * or it can be a function(grid, row, col, rowRenderIndex, colRenderIndex) that returns a class name
7038 self.cellClass = colDef.cellClass;
7042 * @name headerCellClass
7043 * @propertyOf ui.grid.class:GridOptions.columnDef
7044 * @description headerCellClass can be a string specifying the class to append to a cell
7045 * or it can be a function(grid, row, col, rowRenderIndex, colRenderIndex) that returns a class name
7048 self.headerCellClass = colDef.headerCellClass;
7053 * @propertyOf ui.grid.class:GridOptions.columnDef
7054 * @description cellFilter is a filter to apply to the content of each cell
7057 * gridOptions.columnDefs[0].cellFilter = 'date'
7060 self.cellFilter = colDef.cellFilter ? colDef.cellFilter : "";
7064 * @name sortCellFiltered
7065 * @propertyOf ui.grid.class:GridOptions.columnDef
7066 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7067 * sorting the data. Note that when using this option uiGrid will assume that the displayed value is
7068 * a string, and use the {@link ui.grid.class:RowSorter#sortAlpha sortAlpha} `sortFn`. It is possible
7069 * to return a non-string value from an angularjs filter, in which case you should define a {@link ui.grid.class:GridOptions.columnDef#sortingAlgorithm sortingAlgorithm}
7070 * for the column which hanldes the returned type. You may specify one of the `sortingAlgorithms`
7071 * found in the {@link ui.grid.RowSorter rowSorter} service.
7073 self.sortCellFiltered = colDef.sortCellFiltered ? true : false;
7077 * @name filterCellFiltered
7078 * @propertyOf ui.grid.class:GridOptions.columnDef
7079 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7080 * applying "search" `filters`.
7082 self.filterCellFiltered = colDef.filterCellFiltered ? true : false;
7086 * @name headerCellFilter
7087 * @propertyOf ui.grid.class:GridOptions.columnDef
7088 * @description headerCellFilter is a filter to apply to the content of the column header
7091 * gridOptions.columnDefs[0].headerCellFilter = 'translate'
7094 self.headerCellFilter = colDef.headerCellFilter ? colDef.headerCellFilter : "";
7098 * @name footerCellFilter
7099 * @propertyOf ui.grid.class:GridOptions.columnDef
7100 * @description footerCellFilter is a filter to apply to the content of the column footer
7103 * gridOptions.columnDefs[0].footerCellFilter = 'date'
7106 self.footerCellFilter = colDef.footerCellFilter ? colDef.footerCellFilter : "";
7108 self.visible = gridUtil.isNullOrUndefined(colDef.visible) || colDef.visible;
7110 self.headerClass = colDef.headerClass;
7111 //self.cursor = self.sortable ? 'pointer' : 'default';
7113 // Turn on sorting by default
7114 self.enableSorting = typeof(colDef.enableSorting) !== 'undefined' ? colDef.enableSorting : true;
7115 self.sortingAlgorithm = colDef.sortingAlgorithm;
7119 * @name sortDirectionCycle
7120 * @propertyOf ui.grid.class:GridOptions.columnDef
7121 * @description (optional) An array of sort directions, specifying the order that they
7122 * should cycle through as the user repeatedly clicks on the column heading.
7123 * The default is `[null, uiGridConstants.ASC, uiGridConstants.DESC]`. Null
7124 * refers to the unsorted state. This does not affect the initial sort
7125 * direction; use the {@link ui.grid.class:GridOptions.columnDef#sort sort}
7126 * property for that. If
7127 * {@link ui.grid.class:GridOptions.columnDef#suppressRemoveSort suppressRemoveSort}
7128 * is also set, the unsorted state will be skipped even if it is listed here.
7129 * Each direction may not appear in the list more than once (e.g. `[ASC,
7130 * DESC, DESC]` is not allowed), and the list may not be empty.
7132 self.sortDirectionCycle = typeof(colDef.sortDirectionCycle) !== 'undefined' ?
7133 colDef.sortDirectionCycle :
7134 [null, uiGridConstants.ASC, uiGridConstants.DESC];
7138 * @name suppressRemoveSort
7139 * @propertyOf ui.grid.class:GridOptions.columnDef
7140 * @description (optional) False by default. When enabled, this setting hides the removeSort option
7141 * in the menu, and prevents users from manually removing the sort
7143 if ( typeof(self.suppressRemoveSort) === 'undefined'){
7144 self.suppressRemoveSort = typeof(colDef.suppressRemoveSort) !== 'undefined' ? colDef.suppressRemoveSort : false;
7149 * @name enableFiltering
7150 * @propertyOf ui.grid.class:GridOptions.columnDef
7151 * @description turn off filtering for an individual column, where
7152 * you've turned on filtering for the overall grid
7155 * gridOptions.columnDefs[0].enableFiltering = false;
7158 // Turn on filtering by default (it's disabled by default at the Grid level)
7159 self.enableFiltering = typeof(colDef.enableFiltering) !== 'undefined' ? colDef.enableFiltering : true;
7161 // self.menuItems = colDef.menuItems;
7162 self.setPropertyOrDefault(colDef, 'menuItems', []);
7164 // Use the column definition sort if we were passed it, but only if this is a newly added column
7166 self.setPropertyOrDefault(colDef, 'sort');
7169 // Set up default filters array for when one is not provided.
7170 // In other words, this (in column def):
7172 // filter: { term: 'something', flags: {}, condition: [CONDITION] }
7174 // is just shorthand for this:
7176 // filters: [{ term: 'something', flags: {}, condition: [CONDITION] }]
7178 var defaultFilters = [];
7179 if (colDef.filter) {
7180 defaultFilters.push(colDef.filter);
7182 else if ( colDef.filters ){
7183 defaultFilters = colDef.filters;
7185 // Add an empty filter definition object, which will
7186 // translate to a guessed condition and no pre-populated
7187 // value for the filter <input>.
7188 defaultFilters.push({});
7194 * @propertyOf ui.grid.class:GridOptions.columnDef
7195 * @description Specify a single filter field on this column.
7197 * A filter consists of a condition, a term, and a placeholder:
7199 * - condition defines how rows are chosen as matching the filter term. This can be set to
7200 * one of the constants in uiGridConstants.filter, or you can supply a custom filter function
7201 * that gets passed the following arguments: [searchTerm, cellValue, row, column].
7202 * - term: If set, the filter field will be pre-populated
7204 * - placeholder: String that will be set to the `<input>.placeholder` attribute.
7205 * - ariaLabel: String that will be set to the `<input>.ariaLabel` attribute. This is what is read as a label to screen reader users.
7206 * - noTerm: set this to true if you have defined a custom function in condition, and
7207 * your custom function doesn't require a term (so it can run even when the term is null)
7208 * - flags: only flag currently available is `caseSensitive`, set to false if you don't want
7209 * case sensitive matching
7210 * - type: defaults to uiGridConstants.filter.INPUT, which gives a text box. If set to uiGridConstants.filter.SELECT
7211 * then a select box will be shown with options selectOptions
7212 * - selectOptions: options in the format `[ { value: 1, label: 'male' }]`. No i18n filter is provided, you need
7213 * to perform the i18n on the values before you provide them
7214 * - disableCancelFilterButton: defaults to false. If set to true then the 'x' button that cancels/clears the filter
7215 * will not be shown.
7217 * <pre>$scope.gridOptions.columnDefs = [
7222 * condition: uiGridConstants.filter.STARTS_WITH,
7223 * placeholder: 'starts with...',
7224 * ariaLabel: 'Starts with filter for field1',
7225 * flags: { caseSensitive: false },
7226 * type: uiGridConstants.filter.SELECT,
7227 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ],
7228 * disableCancelFilterButton: true
7243 condition: uiGridConstants.filter.CONTAINS,
7244 placeholder: 'my placeholder',
7245 ariaLabel: 'Starts with filter for field1',
7254 // Only set filter if this is a newly added column, if we're updating an existing
7255 // column then we don't want to put the default filter back if the user may have already
7257 // However, we do want to keep the settings if they change, just not the term
7259 self.setPropertyOrDefault(colDef, 'filter');
7260 self.setPropertyOrDefault(colDef, 'filters', defaultFilters);
7261 } else if ( self.filters.length === defaultFilters.length ) {
7262 self.filters.forEach( function( filter, index ){
7263 if (typeof(defaultFilters[index].placeholder) !== 'undefined') {
7264 filter.placeholder = defaultFilters[index].placeholder;
7266 if (typeof(defaultFilters[index].ariaLabel) !== 'undefined') {
7267 filter.ariaLabel = defaultFilters[index].ariaLabel;
7269 if (typeof(defaultFilters[index].flags) !== 'undefined') {
7270 filter.flags = defaultFilters[index].flags;
7272 if (typeof(defaultFilters[index].type) !== 'undefined') {
7273 filter.type = defaultFilters[index].type;
7275 if (typeof(defaultFilters[index].selectOptions) !== 'undefined') {
7276 filter.selectOptions = defaultFilters[index].selectOptions;
7285 * @methodOf ui.grid.class:GridColumn
7286 * @description Removes column from the grid sorting
7288 GridColumn.prototype.unsort = function () {
7290 this.grid.api.core.raise.sortChanged( this.grid, this.grid.getColumnSorting() );
7297 * @methodOf ui.grid.class:GridColumn
7298 * @description Returns the class name for the column
7299 * @param {bool} prefixDot if true, will return .className instead of className
7301 GridColumn.prototype.getColClass = function (prefixDot) {
7302 var cls = uiGridConstants.COL_CLASS_PREFIX + this.uid;
7304 return prefixDot ? '.' + cls : cls;
7309 * @name isPinnedLeft
7310 * @methodOf ui.grid.class:GridColumn
7311 * @description Returns true if column is in the left render container
7313 GridColumn.prototype.isPinnedLeft = function () {
7314 return this.renderContainer === 'left';
7319 * @name isPinnedRight
7320 * @methodOf ui.grid.class:GridColumn
7321 * @description Returns true if column is in the right render container
7323 GridColumn.prototype.isPinnedRight = function () {
7324 return this.renderContainer === 'right';
7330 * @name getColClassDefinition
7331 * @methodOf ui.grid.class:GridColumn
7332 * @description Returns the class definition for th column
7334 GridColumn.prototype.getColClassDefinition = function () {
7335 return ' .grid' + this.grid.id + ' ' + this.getColClass(true) + ' { min-width: ' + this.drawnWidth + 'px; max-width: ' + this.drawnWidth + 'px; }';
7340 * @name getRenderContainer
7341 * @methodOf ui.grid.class:GridColumn
7342 * @description Returns the render container object that this column belongs to.
7344 * Columns will be default be in the `body` render container if they aren't allocated to one specifically.
7346 GridColumn.prototype.getRenderContainer = function getRenderContainer() {
7349 var containerId = self.renderContainer;
7351 if (containerId === null || containerId === '' || containerId === undefined) {
7352 containerId = 'body';
7355 return self.grid.renderContainers[containerId];
7361 * @methodOf ui.grid.class:GridColumn
7362 * @description Makes the column visible by setting colDef.visible = true
7364 GridColumn.prototype.showColumn = function() {
7365 this.colDef.visible = true;
7371 * @name aggregationHideLabel
7372 * @propertyOf ui.grid.class:GridOptions.columnDef
7373 * @description defaults to false, if set to true hides the label text
7374 * in the aggregation footer, so only the value is displayed.
7379 * @name getAggregationText
7380 * @methodOf ui.grid.class:GridColumn
7381 * @description Gets the aggregation label from colDef.aggregationLabel if
7382 * specified or by using i18n, including deciding whether or not to display
7383 * based on colDef.aggregationHideLabel.
7385 * @param {string} label the i18n lookup value to use for the column label
7388 GridColumn.prototype.getAggregationText = function () {
7390 if ( self.colDef.aggregationHideLabel ){
7393 else if ( self.colDef.aggregationLabel ) {
7394 return self.colDef.aggregationLabel;
7397 switch ( self.colDef.aggregationType ){
7398 case uiGridConstants.aggregationTypes.count:
7399 return i18nService.getSafeText('aggregation.count');
7400 case uiGridConstants.aggregationTypes.sum:
7401 return i18nService.getSafeText('aggregation.sum');
7402 case uiGridConstants.aggregationTypes.avg:
7403 return i18nService.getSafeText('aggregation.avg');
7404 case uiGridConstants.aggregationTypes.min:
7405 return i18nService.getSafeText('aggregation.min');
7406 case uiGridConstants.aggregationTypes.max:
7407 return i18nService.getSafeText('aggregation.max');
7414 GridColumn.prototype.getCellTemplate = function () {
7417 return self.cellTemplatePromise;
7420 GridColumn.prototype.getCompiledElementFn = function () {
7423 return self.compiledElementFnDefer.promise;
7433 angular.module('ui.grid')
7434 .factory('GridOptions', ['gridUtil','uiGridConstants', function(gridUtil,uiGridConstants) {
7438 * @name ui.grid.class:GridOptions
7439 * @description Default GridOptions class. GridOptions are defined by the application developer and overlaid
7440 * over this object. Setting gridOptions within your controller is the most common method for an application
7441 * developer to configure the behaviour of their ui-grid
7443 * @example To define your gridOptions within your controller:
7444 * <pre>$scope.gridOptions = {
7445 * data: $scope.myData,
7447 * { name: 'field1', displayName: 'pretty display name' },
7448 * { name: 'field2', visible: false }
7452 * You can then use this within your html template, when you define your grid:
7453 * <pre><div ui-grid="gridOptions"></div></pre>
7455 * To provide default options for all of the grids within your application, use an angular
7456 * decorator to modify the GridOptions factory.
7458 * app.config(function($provide){
7459 * $provide.decorator('GridOptions',function($delegate){
7461 * gridOptions = angular.copy($delegate);
7462 * gridOptions.initialize = function(options) {
7464 * initOptions = $delegate.initialize(options);
7465 * initOptions.enableColumnMenus = false;
7466 * return initOptions;
7468 * return gridOptions;
7474 initialize: function( baseOptions ){
7477 * @name onRegisterApi
7478 * @propertyOf ui.grid.class:GridOptions
7479 * @description A callback that returns the gridApi once the grid is instantiated, which is
7480 * then used to interact with the grid programatically.
7482 * Note that the gridApi.core.renderingComplete event is identical to this
7483 * callback, but has the advantage that it can be called from multiple places
7488 * $scope.gridOptions.onRegisterApi = function ( gridApi ) {
7489 * $scope.gridApi = gridApi;
7490 * $scope.gridApi.selection.selectAllRows( $scope.gridApi.grid );
7495 baseOptions.onRegisterApi = baseOptions.onRegisterApi || angular.noop();
7500 * @propertyOf ui.grid.class:GridOptions
7501 * @description (mandatory) Array of data to be rendered into the grid, providing the data source or data binding for
7504 * Most commonly the data is an array of objects, where each object has a number of attributes.
7505 * Each attribute automatically becomes a column in your grid. This array could, for example, be sourced from
7506 * an angularJS $resource query request. The array can also contain complex objects, refer the binding tutorial
7507 * for examples of that.
7509 * The most flexible usage is to set your data on $scope:
7511 * `$scope.data = data;`
7513 * And then direct the grid to resolve whatever is in $scope.data:
7515 * `$scope.gridOptions.data = 'data';`
7517 * This is the most flexible approach as it allows you to replace $scope.data whenever you feel like it without
7518 * getting pointer issues.
7520 * Alternatively you can directly set the data array:
7522 * `$scope.gridOptions.data = [ ];`
7525 * `$http.get('/data/100.json')
7526 * .success(function(data) {
7527 * $scope.myData = data;
7528 * $scope.gridOptions.data = $scope.myData;
7531 * Where you do this, you need to take care in updating the data - you can't just update `$scope.myData` to some other
7532 * array, you need to update $scope.gridOptions.data to point to that new array as well.
7535 baseOptions.data = baseOptions.data || [];
7540 * @propertyOf ui.grid.class:GridOptions
7541 * @description Array of columnDef objects. Only required property is name.
7542 * The individual options available in columnDefs are documented in the
7543 * {@link ui.grid.class:GridOptions.columnDef columnDef} section
7544 * </br>_field property can be used in place of name for backwards compatibility with 2.x_
7547 * <pre>var columnDefs = [{name:'field1'}, {name:'field2'}];</pre>
7550 baseOptions.columnDefs = baseOptions.columnDefs || [];
7554 * @name ui.grid.class:GridOptions.columnDef
7555 * @description Definition / configuration of an individual column, which would typically be
7556 * one of many column definitions within the gridOptions.columnDefs array
7558 * <pre>{name:'field1', field: 'field1', filter: { term: 'xxx' }}</pre>
7565 * @name excludeProperties
7566 * @propertyOf ui.grid.class:GridOptions
7567 * @description Array of property names in data to ignore when auto-generating column names. Provides the
7568 * inverse of columnDefs - columnDefs is a list of columns to include, excludeProperties is a list of columns
7571 * If columnDefs is defined, this will be ignored.
7573 * Defaults to ['$$hashKey']
7576 baseOptions.excludeProperties = baseOptions.excludeProperties || ['$$hashKey'];
7580 * @name enableRowHashing
7581 * @propertyOf ui.grid.class:GridOptions
7582 * @description True by default. When enabled, this setting allows uiGrid to add
7583 * `$$hashKey`-type properties (similar to Angular) to elements in the `data` array. This allows
7584 * the grid to maintain state while vastly speeding up the process of altering `data` by adding/moving/removing rows.
7586 * Note that this DOES add properties to your data that you may not want, but they are stripped out when using `angular.toJson()`. IF
7587 * you do not want this at all you can disable this setting but you will take a performance hit if you are using large numbers of rows
7588 * and are altering the data set often.
7590 baseOptions.enableRowHashing = baseOptions.enableRowHashing !== false;
7595 * @methodOf ui.grid.class:GridOptions
7596 * @description This function is used to get and, if necessary, set the value uniquely identifying this row (i.e. if an identity is not present it will set one).
7598 * By default it returns the `$$hashKey` property if it exists. If it doesn't it uses gridUtil.nextUid() to generate one
7600 baseOptions.rowIdentity = baseOptions.rowIdentity || function rowIdentity(row) {
7601 return gridUtil.hashKey(row);
7606 * @name getRowIdentity
7607 * @methodOf ui.grid.class:GridOptions
7608 * @description This function returns the identity value uniquely identifying this row, if one is not present it does not set it.
7610 * By default it returns the `$$hashKey` property but can be overridden to use any property or set of properties you want.
7612 baseOptions.getRowIdentity = baseOptions.getRowIdentity || function getRowIdentity(row) {
7613 return row.$$hashKey;
7618 * @name flatEntityAccess
7619 * @propertyOf ui.grid.class:GridOptions
7620 * @description Set to true if your columns are all related directly to fields in a flat object structure - i.e.
7621 * each of your columns associate directly with a property on each of the entities in your data array.
7623 * In that situation we can avoid all the logic associated with complex binding to functions or to properties of sub-objects,
7624 * which can provide a significant speed improvement with large data sets when filtering or sorting.
7628 baseOptions.flatEntityAccess = baseOptions.flatEntityAccess === true;
7633 * @propertyOf ui.grid.class:GridOptions
7634 * @description True by default. When set to false, this setting will replace the
7635 * standard header template with '<div></div>', resulting in no header being shown.
7637 baseOptions.showHeader = typeof(baseOptions.showHeader) !== "undefined" ? baseOptions.showHeader : true;
7639 /* (NOTE): Don't show this in the docs. We only use it internally
7641 * @name headerRowHeight
7642 * @propertyOf ui.grid.class:GridOptions
7643 * @description The height of the header in pixels, defaults to 30
7646 if (!baseOptions.showHeader) {
7647 baseOptions.headerRowHeight = 0;
7650 baseOptions.headerRowHeight = typeof(baseOptions.headerRowHeight) !== "undefined" ? baseOptions.headerRowHeight : 30;
7656 * @propertyOf ui.grid.class:GridOptions
7657 * @description The height of the row in pixels, defaults to 30
7660 baseOptions.rowHeight = baseOptions.rowHeight || 30;
7664 * @name minRowsToShow
7665 * @propertyOf ui.grid.class:GridOptions
7666 * @description Minimum number of rows to show when the grid doesn't have a defined height. Defaults to "10".
7668 baseOptions.minRowsToShow = typeof(baseOptions.minRowsToShow) !== "undefined" ? baseOptions.minRowsToShow : 10;
7672 * @name showGridFooter
7673 * @propertyOf ui.grid.class:GridOptions
7674 * @description Whether or not to show the footer, defaults to false
7675 * The footer display Total Rows and Visible Rows (filtered rows)
7677 baseOptions.showGridFooter = baseOptions.showGridFooter === true;
7681 * @name showColumnFooter
7682 * @propertyOf ui.grid.class:GridOptions
7683 * @description Whether or not to show the column footer, defaults to false
7684 * The column footer displays column aggregates
7686 baseOptions.showColumnFooter = baseOptions.showColumnFooter === true;
7690 * @name columnFooterHeight
7691 * @propertyOf ui.grid.class:GridOptions
7692 * @description The height of the footer rows (column footer and grid footer) in pixels
7695 baseOptions.columnFooterHeight = typeof(baseOptions.columnFooterHeight) !== "undefined" ? baseOptions.columnFooterHeight : 30;
7696 baseOptions.gridFooterHeight = typeof(baseOptions.gridFooterHeight) !== "undefined" ? baseOptions.gridFooterHeight : 30;
7698 baseOptions.columnWidth = typeof(baseOptions.columnWidth) !== "undefined" ? baseOptions.columnWidth : 50;
7702 * @name maxVisibleColumnCount
7703 * @propertyOf ui.grid.class:GridOptions
7704 * @description Defaults to 200
7707 baseOptions.maxVisibleColumnCount = typeof(baseOptions.maxVisibleColumnCount) !== "undefined" ? baseOptions.maxVisibleColumnCount : 200;
7711 * @name virtualizationThreshold
7712 * @propertyOf ui.grid.class:GridOptions
7713 * @description Turn virtualization on when number of data elements goes over this number, defaults to 20
7715 baseOptions.virtualizationThreshold = typeof(baseOptions.virtualizationThreshold) !== "undefined" ? baseOptions.virtualizationThreshold : 20;
7719 * @name columnVirtualizationThreshold
7720 * @propertyOf ui.grid.class:GridOptions
7721 * @description Turn virtualization on when number of columns goes over this number, defaults to 10
7723 baseOptions.columnVirtualizationThreshold = typeof(baseOptions.columnVirtualizationThreshold) !== "undefined" ? baseOptions.columnVirtualizationThreshold : 10;
7728 * @propertyOf ui.grid.class:GridOptions
7729 * @description Extra rows to to render outside of the viewport, which helps with smoothness of scrolling.
7732 baseOptions.excessRows = typeof(baseOptions.excessRows) !== "undefined" ? baseOptions.excessRows : 4;
7735 * @name scrollThreshold
7736 * @propertyOf ui.grid.class:GridOptions
7737 * @description Defaults to 4
7739 baseOptions.scrollThreshold = typeof(baseOptions.scrollThreshold) !== "undefined" ? baseOptions.scrollThreshold : 4;
7743 * @name excessColumns
7744 * @propertyOf ui.grid.class:GridOptions
7745 * @description Extra columns to to render outside of the viewport, which helps with smoothness of scrolling.
7748 baseOptions.excessColumns = typeof(baseOptions.excessColumns) !== "undefined" ? baseOptions.excessColumns : 4;
7751 * @name horizontalScrollThreshold
7752 * @propertyOf ui.grid.class:GridOptions
7753 * @description Defaults to 4
7755 baseOptions.horizontalScrollThreshold = typeof(baseOptions.horizontalScrollThreshold) !== "undefined" ? baseOptions.horizontalScrollThreshold : 2;
7760 * @name aggregationCalcThrottle
7761 * @propertyOf ui.grid.class:GridOptions
7762 * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 500ms
7764 baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 500;
7768 * @name wheelScrollThrottle
7769 * @propertyOf ui.grid.class:GridOptions
7770 * @description Default time in milliseconds to throttle scroll events to, defaults to 70ms
7772 baseOptions.wheelScrollThrottle = typeof(baseOptions.wheelScrollThrottle) !== "undefined" ? baseOptions.wheelScrollThrottle : 70;
7777 * @name scrollDebounce
7778 * @propertyOf ui.grid.class:GridOptions
7779 * @description Default time in milliseconds to debounce scroll events, defaults to 300ms
7781 baseOptions.scrollDebounce = typeof(baseOptions.scrollDebounce) !== "undefined" ? baseOptions.scrollDebounce : 300;
7785 * @name enableSorting
7786 * @propertyOf ui.grid.class:GridOptions
7787 * @description True by default. When enabled, this setting adds sort
7788 * widgets to the column headers, allowing sorting of the data for the entire grid.
7789 * Sorting can then be disabled on individual columns using the columnDefs.
7791 baseOptions.enableSorting = baseOptions.enableSorting !== false;
7795 * @name enableFiltering
7796 * @propertyOf ui.grid.class:GridOptions
7797 * @description False by default. When enabled, this setting adds filter
7798 * boxes to each column header, allowing filtering within the column for the entire grid.
7799 * Filtering can then be disabled on individual columns using the columnDefs.
7801 baseOptions.enableFiltering = baseOptions.enableFiltering === true;
7805 * @name enableColumnMenus
7806 * @propertyOf ui.grid.class:GridOptions
7807 * @description True by default. When enabled, this setting displays a column
7808 * menu within each column.
7810 baseOptions.enableColumnMenus = baseOptions.enableColumnMenus !== false;
7814 * @name enableVerticalScrollbar
7815 * @propertyOf ui.grid.class:GridOptions
7816 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the vertical scrollbar for the grid.
7817 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7819 baseOptions.enableVerticalScrollbar = typeof(baseOptions.enableVerticalScrollbar) !== "undefined" ? baseOptions.enableVerticalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7823 * @name enableHorizontalScrollbar
7824 * @propertyOf ui.grid.class:GridOptions
7825 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the horizontal scrollbar for the grid.
7826 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7828 baseOptions.enableHorizontalScrollbar = typeof(baseOptions.enableHorizontalScrollbar) !== "undefined" ? baseOptions.enableHorizontalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7832 * @name enableMinHeightCheck
7833 * @propertyOf ui.grid.class:GridOptions
7834 * @description True by default. When enabled, a newly initialized grid will check to see if it is tall enough to display
7835 * at least one row of data. If the grid is not tall enough, it will resize the DOM element to display minRowsToShow number
7838 baseOptions.enableMinHeightCheck = baseOptions.enableMinHeightCheck !== false;
7842 * @name minimumColumnSize
7843 * @propertyOf ui.grid.class:GridOptions
7844 * @description Columns can't be smaller than this, defaults to 10 pixels
7846 baseOptions.minimumColumnSize = typeof(baseOptions.minimumColumnSize) !== "undefined" ? baseOptions.minimumColumnSize : 10;
7851 * @methodOf ui.grid.class:GridOptions
7852 * @description By default, rows are compared using object equality. This option can be overridden
7853 * to compare on any data item property or function
7854 * @param {object} entityA First Data Item to compare
7855 * @param {object} entityB Second Data Item to compare
7857 baseOptions.rowEquality = baseOptions.rowEquality || function(entityA, entityB) {
7858 return entityA === entityB;
7863 * @name headerTemplate
7864 * @propertyOf ui.grid.class:GridOptions
7865 * @description Null by default. When provided, this setting uses a custom header
7866 * template, rather than the default template. Can be set to either the name of a template file:
7867 * <pre> $scope.gridOptions.headerTemplate = 'header_template.html';</pre>
7869 * <pre> $scope.gridOptions.headerTemplate = '<div class="ui-grid-top-panel" style="text-align: center">I am a Custom Grid Header</div>'</pre>
7870 * or the id of a precompiled template (TBD how to use this).
7871 * </br>Refer to the custom header tutorial for more information.
7872 * If you want no header at all, you can set to an empty div:
7873 * <pre> $scope.gridOptions.headerTemplate = '<div></div>';</pre>
7875 * If you want to only have a static header, then you can set to static content. If
7876 * you want to tailor the existing column headers, then you should look at the
7877 * current 'ui-grid-header.html' template in github as your starting point.
7880 baseOptions.headerTemplate = baseOptions.headerTemplate || null;
7884 * @name footerTemplate
7885 * @propertyOf ui.grid.class:GridOptions
7886 * @description (optional) ui-grid/ui-grid-footer by default. This footer shows the per-column
7887 * aggregation totals.
7888 * When provided, this setting uses a custom footer template. Can be set to either the name of a template file 'footer_template.html', inline html
7889 * <pre>'<div class="ui-grid-bottom-panel" style="text-align: center">I am a Custom Grid Footer</div>'</pre>, or the id
7890 * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information.
7892 baseOptions.footerTemplate = baseOptions.footerTemplate || 'ui-grid/ui-grid-footer';
7896 * @name gridFooterTemplate
7897 * @propertyOf ui.grid.class:GridOptions
7898 * @description (optional) ui-grid/ui-grid-grid-footer by default. This template by default shows the
7899 * total items at the bottom of the grid, and the selected items if selection is enabled.
7901 baseOptions.gridFooterTemplate = baseOptions.gridFooterTemplate || 'ui-grid/ui-grid-grid-footer';
7906 * @propertyOf ui.grid.class:GridOptions
7907 * @description 'ui-grid/ui-grid-row' by default. When provided, this setting uses a
7908 * custom row template. Can be set to either the name of a template file:
7909 * <pre> $scope.gridOptions.rowTemplate = 'row_template.html';</pre>
7911 * <pre> $scope.gridOptions.rowTemplate = '<div style="background-color: aquamarine" ng-click="grid.appScope.fnOne(row)" ng-repeat="col in colContainer.renderedColumns track by col.colDef.name" class="ui-grid-cell" ui-grid-cell></div>';</pre>
7912 * or the id of a precompiled template (TBD how to use this) can be provided.
7913 * </br>Refer to the custom row template tutorial for more information.
7915 baseOptions.rowTemplate = baseOptions.rowTemplate || 'ui-grid/ui-grid-row';
7919 * @name appScopeProvider
7920 * @propertyOf ui.grid.class:GridOptions
7921 * @description by default, the parent scope of the ui-grid element will be assigned to grid.appScope
7922 * this property allows you to assign any reference you want to grid.appScope
7924 baseOptions.appScopeProvider = baseOptions.appScopeProvider || null;
7937 angular.module('ui.grid')
7941 * @name ui.grid.class:GridRenderContainer
7942 * @description The grid has render containers, allowing the ability to have pinned columns. If the grid
7943 * is right-to-left then there may be a right render container, if left-to-right then there may
7944 * be a left render container. There is always a body render container.
7945 * @param {string} name The name of the render container ('body', 'left', or 'right')
7946 * @param {Grid} grid the grid the render container is in
7947 * @param {object} options the render container options
7949 .factory('GridRenderContainer', ['gridUtil', 'uiGridConstants', function(gridUtil, uiGridConstants) {
7950 function GridRenderContainer(name, grid, options) {
7953 // if (gridUtil.type(grid) !== 'Grid') {
7954 // throw new Error('Grid argument is not a Grid object');
7961 // self.rowCache = [];
7962 // self.columnCache = [];
7964 self.visibleRowCache = [];
7965 self.visibleColumnCache = [];
7967 self.renderedRows = [];
7968 self.renderedColumns = [];
7970 self.prevScrollTop = 0;
7971 self.prevScrolltopPercentage = 0;
7972 self.prevRowScrollIndex = 0;
7974 self.prevScrollLeft = 0;
7975 self.prevScrollleftPercentage = 0;
7976 self.prevColumnScrollIndex = 0;
7978 self.columnStyles = "";
7980 self.viewportAdjusters = [];
7984 * @name hasHScrollbar
7985 * @propertyOf ui.grid.class:GridRenderContainer
7986 * @description flag to signal that container has a horizontal scrollbar
7988 self.hasHScrollbar = false;
7992 * @name hasVScrollbar
7993 * @propertyOf ui.grid.class:GridRenderContainer
7994 * @description flag to signal that container has a vertical scrollbar
7996 self.hasVScrollbar = false;
8000 * @name canvasHeightShouldUpdate
8001 * @propertyOf ui.grid.class:GridRenderContainer
8002 * @description flag to signal that container should recalculate the canvas size
8004 self.canvasHeightShouldUpdate = true;
8008 * @name canvasHeight
8009 * @propertyOf ui.grid.class:GridRenderContainer
8010 * @description last calculated canvas height value
8012 self.$$canvasHeight = 0;
8014 if (options && angular.isObject(options)) {
8015 angular.extend(self, options);
8018 grid.registerStyleComputation({
8021 self.updateColumnWidths();
8022 return self.columnStyles;
8028 GridRenderContainer.prototype.reset = function reset() {
8029 // this.rowCache.length = 0;
8030 // this.columnCache.length = 0;
8032 this.visibleColumnCache.length = 0;
8033 this.visibleRowCache.length = 0;
8035 this.renderedRows.length = 0;
8036 this.renderedColumns.length = 0;
8039 // TODO(c0bra): calculate size?? Should this be in a stackable directive?
8042 GridRenderContainer.prototype.containsColumn = function (col) {
8043 return this.visibleColumnCache.indexOf(col) !== -1;
8046 GridRenderContainer.prototype.minRowsToRender = function minRowsToRender() {
8049 var rowAddedHeight = 0;
8050 var viewPortHeight = self.getViewportHeight();
8051 for (var i = self.visibleRowCache.length - 1; rowAddedHeight < viewPortHeight && i >= 0; i--) {
8052 rowAddedHeight += self.visibleRowCache[i].height;
8058 GridRenderContainer.prototype.minColumnsToRender = function minColumnsToRender() {
8060 var viewportWidth = this.getViewportWidth();
8064 // self.columns.forEach(function(col, i) {
8065 for (var i = 0; i < self.visibleColumnCache.length; i++) {
8066 var col = self.visibleColumnCache[i];
8068 if (totalWidth < viewportWidth) {
8069 totalWidth += col.drawnWidth ? col.drawnWidth : 0;
8074 for (var j = i; j >= i - min; j--) {
8075 currWidth += self.visibleColumnCache[j].drawnWidth ? self.visibleColumnCache[j].drawnWidth : 0;
8077 if (currWidth < viewportWidth) {
8086 GridRenderContainer.prototype.getVisibleRowCount = function getVisibleRowCount() {
8087 return this.visibleRowCache.length;
8092 * @name registerViewportAdjuster
8093 * @methodOf ui.grid.class:GridRenderContainer
8094 * @description Registers an adjuster to the render container's available width or height. Adjusters are used
8095 * to tell the render container that there is something else consuming space, and to adjust it's size
8097 * @param {function} func the adjuster function we want to register
8100 GridRenderContainer.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
8101 this.viewportAdjusters.push(func);
8106 * @name removeViewportAdjuster
8107 * @methodOf ui.grid.class:GridRenderContainer
8108 * @description Removes an adjuster, should be used when your element is destroyed
8109 * @param {function} func the adjuster function we want to remove
8111 GridRenderContainer.prototype.removeViewportAdjuster = function removeViewportAdjuster(func) {
8112 var idx = this.viewportAdjusters.indexOf(func);
8115 this.viewportAdjusters.splice(idx, 1);
8121 * @name getViewportAdjustment
8122 * @methodOf ui.grid.class:GridRenderContainer
8123 * @description Gets the adjustment based on the viewportAdjusters.
8124 * @returns {object} a hash of { height: x, width: y }. Usually the values will be negative
8126 GridRenderContainer.prototype.getViewportAdjustment = function getViewportAdjustment() {
8129 var adjustment = { height: 0, width: 0 };
8131 self.viewportAdjusters.forEach(function (func) {
8132 adjustment = func.call(this, adjustment);
8138 GridRenderContainer.prototype.getMargin = function getMargin(side) {
8143 self.viewportAdjusters.forEach(function (func) {
8144 var adjustment = func.call(this, { height: 0, width: 0 });
8146 if (adjustment.side && adjustment.side === side) {
8147 amount += adjustment.width * -1;
8154 GridRenderContainer.prototype.getViewportHeight = function getViewportHeight() {
8157 var headerHeight = (self.headerHeight) ? self.headerHeight : self.grid.headerHeight;
8159 var viewPortHeight = self.grid.gridHeight - headerHeight - self.grid.footerHeight;
8162 var adjustment = self.getViewportAdjustment();
8164 viewPortHeight = viewPortHeight + adjustment.height;
8166 return viewPortHeight;
8169 GridRenderContainer.prototype.getViewportWidth = function getViewportWidth() {
8172 var viewportWidth = self.grid.gridWidth;
8174 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8175 // viewPortWidth = viewPortWidth - self.grid.verticalScrollbarWidth;
8178 // var viewportWidth = 0;\
8179 // self.visibleColumnCache.forEach(function (column) {
8180 // viewportWidth += column.drawnWidth;
8183 var adjustment = self.getViewportAdjustment();
8185 viewportWidth = viewportWidth + adjustment.width;
8187 return viewportWidth;
8190 GridRenderContainer.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
8193 var viewportWidth = this.getViewportWidth();
8195 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8196 // viewPortWidth = viewPortWidth + self.grid.verticalScrollbarWidth;
8199 // var adjustment = self.getViewportAdjustment();
8200 // viewPortWidth = viewPortWidth + adjustment.width;
8202 return viewportWidth;
8208 * @name getCanvasHeight
8209 * @methodOf ui.grid.class:GridRenderContainer
8210 * @description Returns the total canvas height. Only recalculates if canvasHeightShouldUpdate = false
8211 * @returns {number} total height of all the visible rows in the container
8213 GridRenderContainer.prototype.getCanvasHeight = function getCanvasHeight() {
8216 if (!self.canvasHeightShouldUpdate) {
8217 return self.$$canvasHeight;
8220 var oldCanvasHeight = self.$$canvasHeight;
8222 self.$$canvasHeight = 0;
8224 self.visibleRowCache.forEach(function(row){
8225 self.$$canvasHeight += row.height;
8229 self.canvasHeightShouldUpdate = false;
8231 self.grid.api.core.raise.canvasHeightChanged(oldCanvasHeight, self.$$canvasHeight);
8233 return self.$$canvasHeight;
8236 GridRenderContainer.prototype.getVerticalScrollLength = function getVerticalScrollLength() {
8237 return this.getCanvasHeight() - this.getViewportHeight() + this.grid.scrollbarHeight;
8240 GridRenderContainer.prototype.getCanvasWidth = function getCanvasWidth() {
8243 var ret = self.canvasWidth;
8248 GridRenderContainer.prototype.setRenderedRows = function setRenderedRows(newRows) {
8249 this.renderedRows.length = newRows.length;
8250 for (var i = 0; i < newRows.length; i++) {
8251 this.renderedRows[i] = newRows[i];
8255 GridRenderContainer.prototype.setRenderedColumns = function setRenderedColumns(newColumns) {
8259 this.renderedColumns.length = newColumns.length;
8260 for (var i = 0; i < newColumns.length; i++) {
8261 this.renderedColumns[i] = newColumns[i];
8264 this.updateColumnOffset();
8267 GridRenderContainer.prototype.updateColumnOffset = function updateColumnOffset() {
8268 // Calculate the width of the columns on the left side that are no longer rendered.
8269 // That will be the offset for the columns as we scroll horizontally.
8270 var hiddenColumnsWidth = 0;
8271 for (var i = 0; i < this.currentFirstColumn; i++) {
8272 hiddenColumnsWidth += this.visibleColumnCache[i].drawnWidth;
8275 this.columnOffset = hiddenColumnsWidth;
8278 GridRenderContainer.prototype.scrollVertical = function (newScrollTop) {
8279 var vertScrollPercentage = -1;
8281 if (newScrollTop !== this.prevScrollTop) {
8282 var yDiff = newScrollTop - this.prevScrollTop;
8284 if (yDiff > 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; }
8285 if (yDiff < 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.UP; }
8287 var vertScrollLength = this.getVerticalScrollLength();
8289 vertScrollPercentage = newScrollTop / vertScrollLength;
8291 // console.log('vert', vertScrollPercentage, newScrollTop, vertScrollLength);
8293 if (vertScrollPercentage > 1) { vertScrollPercentage = 1; }
8294 if (vertScrollPercentage < 0) { vertScrollPercentage = 0; }
8296 this.adjustScrollVertical(newScrollTop, vertScrollPercentage);
8297 return vertScrollPercentage;
8301 GridRenderContainer.prototype.scrollHorizontal = function(newScrollLeft){
8302 var horizScrollPercentage = -1;
8306 if (newScrollLeft !== this.prevScrollLeft) {
8307 var xDiff = newScrollLeft - this.prevScrollLeft;
8309 if (xDiff > 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; }
8310 if (xDiff < 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; }
8312 var horizScrollLength = (this.canvasWidth - this.getViewportWidth());
8313 if (horizScrollLength !== 0) {
8314 horizScrollPercentage = newScrollLeft / horizScrollLength;
8317 horizScrollPercentage = 0;
8320 this.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage);
8321 return horizScrollPercentage;
8325 GridRenderContainer.prototype.adjustScrollVertical = function adjustScrollVertical(scrollTop, scrollPercentage, force) {
8326 if (this.prevScrollTop === scrollTop && !force) {
8330 if (typeof(scrollTop) === 'undefined' || scrollTop === undefined || scrollTop === null) {
8331 scrollTop = (this.getCanvasHeight() - this.getViewportHeight()) * scrollPercentage;
8334 this.adjustRows(scrollTop, scrollPercentage, false);
8336 this.prevScrollTop = scrollTop;
8337 this.prevScrolltopPercentage = scrollPercentage;
8339 this.grid.queueRefresh();
8342 GridRenderContainer.prototype.adjustScrollHorizontal = function adjustScrollHorizontal(scrollLeft, scrollPercentage, force) {
8343 if (this.prevScrollLeft === scrollLeft && !force) {
8347 if (typeof(scrollLeft) === 'undefined' || scrollLeft === undefined || scrollLeft === null) {
8348 scrollLeft = (this.getCanvasWidth() - this.getViewportWidth()) * scrollPercentage;
8351 this.adjustColumns(scrollLeft, scrollPercentage);
8353 this.prevScrollLeft = scrollLeft;
8354 this.prevScrollleftPercentage = scrollPercentage;
8356 this.grid.queueRefresh();
8359 GridRenderContainer.prototype.adjustRows = function adjustRows(scrollTop, scrollPercentage, postDataLoaded) {
8362 var minRows = self.minRowsToRender();
8364 var rowCache = self.visibleRowCache;
8366 var maxRowIndex = rowCache.length - minRows;
8368 // console.log('scroll%1', scrollPercentage);
8370 // Calculate the scroll percentage according to the scrollTop location, if no percentage was provided
8371 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollTop) {
8372 scrollPercentage = scrollTop / self.getVerticalScrollLength();
8375 var rowIndex = Math.ceil(Math.min(maxRowIndex, maxRowIndex * scrollPercentage));
8377 // console.log('maxRowIndex / scroll%', maxRowIndex, scrollPercentage, rowIndex);
8379 // Define a max row index that we can't scroll past
8380 if (rowIndex > maxRowIndex) {
8381 rowIndex = maxRowIndex;
8385 if (rowCache.length > self.grid.options.virtualizationThreshold) {
8386 if (!(typeof(scrollTop) === 'undefined' || scrollTop === null)) {
8387 // Have we hit the threshold going down?
8388 if ( !self.grid.suppressParentScrollDown && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8391 //Have we hit the threshold going up?
8392 if ( !self.grid.suppressParentScrollUp && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8396 var rangeStart = {};
8399 rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows);
8400 rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows);
8402 newRange = [rangeStart, rangeEnd];
8405 var maxLen = self.visibleRowCache.length;
8406 newRange = [0, Math.max(maxLen, minRows + self.grid.options.excessRows)];
8409 self.updateViewableRowRange(newRange);
8411 self.prevRowScrollIndex = rowIndex;
8414 GridRenderContainer.prototype.adjustColumns = function adjustColumns(scrollLeft, scrollPercentage) {
8417 var minCols = self.minColumnsToRender();
8419 var columnCache = self.visibleColumnCache;
8420 var maxColumnIndex = columnCache.length - minCols;
8422 // Calculate the scroll percentage according to the scrollLeft location, if no percentage was provided
8423 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollLeft) {
8424 var horizScrollLength = (self.getCanvasWidth() - self.getViewportWidth());
8425 scrollPercentage = scrollLeft / horizScrollLength;
8428 var colIndex = Math.ceil(Math.min(maxColumnIndex, maxColumnIndex * scrollPercentage));
8430 // Define a max row index that we can't scroll past
8431 if (colIndex > maxColumnIndex) {
8432 colIndex = maxColumnIndex;
8436 if (columnCache.length > self.grid.options.columnVirtualizationThreshold && self.getCanvasWidth() > self.getViewportWidth()) {
8437 /* Commented the following lines because otherwise the moved column wasn't visible immediately on the new position
8438 * in the case of many columns with horizontal scroll, one had to scroll left or right and then return in order to see it
8439 // Have we hit the threshold going down?
8440 if (self.prevScrollLeft < scrollLeft && colIndex < self.prevColumnScrollIndex + self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8443 //Have we hit the threshold going up?
8444 if (self.prevScrollLeft > scrollLeft && colIndex > self.prevColumnScrollIndex - self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8448 var rangeStart = Math.max(0, colIndex - self.grid.options.excessColumns);
8449 var rangeEnd = Math.min(columnCache.length, colIndex + minCols + self.grid.options.excessColumns);
8451 newRange = [rangeStart, rangeEnd];
8454 var maxLen = self.visibleColumnCache.length;
8456 newRange = [0, Math.max(maxLen, minCols + self.grid.options.excessColumns)];
8459 self.updateViewableColumnRange(newRange);
8461 self.prevColumnScrollIndex = colIndex;
8464 // Method for updating the visible rows
8465 GridRenderContainer.prototype.updateViewableRowRange = function updateViewableRowRange(renderedRange) {
8466 // Slice out the range of rows from the data
8467 // var rowArr = uiGridCtrl.grid.rows.slice(renderedRange[0], renderedRange[1]);
8468 var rowArr = this.visibleRowCache.slice(renderedRange[0], renderedRange[1]);
8470 // Define the top-most rendered row
8471 this.currentTopRow = renderedRange[0];
8473 this.setRenderedRows(rowArr);
8476 // Method for updating the visible columns
8477 GridRenderContainer.prototype.updateViewableColumnRange = function updateViewableColumnRange(renderedRange) {
8478 // Slice out the range of rows from the data
8479 // var columnArr = uiGridCtrl.grid.columns.slice(renderedRange[0], renderedRange[1]);
8480 var columnArr = this.visibleColumnCache.slice(renderedRange[0], renderedRange[1]);
8482 // Define the left-most rendered columns
8483 this.currentFirstColumn = renderedRange[0];
8485 this.setRenderedColumns(columnArr);
8488 GridRenderContainer.prototype.headerCellWrapperStyle = function () {
8491 if (self.currentFirstColumn !== 0) {
8492 var offset = self.columnOffset;
8494 if (self.grid.isRTL()) {
8495 return { 'margin-right': offset + 'px' };
8498 return { 'margin-left': offset + 'px' };
8507 * @name updateColumnWidths
8508 * @propertyOf ui.grid.class:GridRenderContainer
8509 * @description Determine the appropriate column width of each column across all render containers.
8511 * Column width is easy when each column has a specified width. When columns are variable width (i.e.
8512 * have an * or % of the viewport) then we try to calculate so that things fit in. The problem is that
8513 * we have multiple render containers, and we don't want one render container to just take the whole viewport
8514 * when it doesn't need to - we want things to balance out across the render containers.
8516 * To do this, we use this method to calculate all the renderContainers, recognising that in a given render
8517 * cycle it'll get called once per render container, so it needs to return the same values each time.
8519 * The constraints on this method are therefore:
8520 * - must return the same value when called multiple times, to do this it needs to rely on properties of the
8521 * columns, but not properties that change when this is called (so it shouldn't rely on drawnWidth)
8523 * The general logic of this method is:
8524 * - calculate our total available width
8525 * - look at all the columns across all render containers, and work out which have widths and which have
8526 * constraints such as % or * or something else
8527 * - for those with *, count the total number of * we see and add it onto a running total, add this column to an * array
8528 * - for those with a %, allocate the % as a percentage of the viewport, having consideration of min and max
8529 * - for those with manual width (in pixels) we set the drawnWidth to the specified width
8530 * - we end up with an asterisks array still to process
8531 * - we look at our remaining width. If it's greater than zero, we divide it up among the asterisk columns, then process
8532 * them for min and max width constraints
8533 * - if it's zero or less, we set the asterisk columns to their minimum widths
8534 * - we use parseInt quite a bit, as we try to make all our column widths integers
8536 GridRenderContainer.prototype.updateColumnWidths = function () {
8539 var asterisksArray = [],
8544 // Get the width of the viewport
8545 var availableWidth = self.grid.getViewportWidth() - self.grid.scrollbarWidth;
8547 // get all the columns across all render containers, we have to calculate them all or one render container
8548 // could consume the whole viewport
8549 var columnCache = [];
8550 angular.forEach(self.grid.renderContainers, function( container, name){
8551 columnCache = columnCache.concat(container.visibleColumnCache);
8554 // look at each column, process any manual values or %, put the * into an array to look at later
8555 columnCache.forEach(function(column, i) {
8557 // Skip hidden columns
8558 if (!column.visible) { return; }
8560 if (angular.isNumber(column.width)) {
8561 // pixel width, set to this value
8562 width = parseInt(column.width, 10);
8563 usedWidthSum = usedWidthSum + width;
8564 column.drawnWidth = width;
8566 } else if (gridUtil.endsWith(column.width, "%")) {
8567 // percentage width, set to percentage of the viewport
8568 width = parseInt(parseInt(column.width.replace(/%/g, ''), 10) / 100 * availableWidth);
8570 if ( width > column.maxWidth ){
8571 width = column.maxWidth;
8574 if ( width < column.minWidth ){
8575 width = column.minWidth;
8578 usedWidthSum = usedWidthSum + width;
8579 column.drawnWidth = width;
8580 } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) {
8581 // is an asterisk column, the gridColumn already checked the string consists only of '****'
8582 asteriskNum = asteriskNum + column.width.length;
8583 asterisksArray.push(column);
8587 // Get the remaining width (available width subtracted by the used widths sum)
8588 var remainingWidth = availableWidth - usedWidthSum;
8590 var i, column, colWidth;
8592 if (asterisksArray.length > 0) {
8593 // the width that each asterisk value would be assigned (this can be negative)
8594 var asteriskVal = remainingWidth / asteriskNum;
8596 asterisksArray.forEach(function( column ){
8597 var width = parseInt(column.width.length * asteriskVal, 10);
8599 if ( width > column.maxWidth ){
8600 width = column.maxWidth;
8603 if ( width < column.minWidth ){
8604 width = column.minWidth;
8607 usedWidthSum = usedWidthSum + width;
8608 column.drawnWidth = width;
8612 // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our
8613 // calculated widths would have the grid narrower than the available space,
8614 // dole the remainder out one by one to make everything fit
8615 var processColumnUpwards = function(column){
8616 if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) {
8617 column.drawnWidth++;
8620 columnsToChange = true;
8624 var leftoverWidth = availableWidth - usedWidthSum;
8625 var columnsToChange = true;
8627 while (leftoverWidth > 0 && columnsToChange) {
8628 columnsToChange = false;
8629 asterisksArray.forEach(processColumnUpwards);
8632 // We can end up with too much width even though some columns aren't at their max width, in this situation
8633 // we can trim the columns a little
8634 var processColumnDownwards = function(column){
8635 if ( column.drawnWidth > column.minWidth && excessWidth > 0) {
8636 column.drawnWidth--;
8639 columnsToChange = true;
8643 var excessWidth = usedWidthSum - availableWidth;
8644 columnsToChange = true;
8646 while (excessWidth > 0 && columnsToChange) {
8647 columnsToChange = false;
8648 asterisksArray.forEach(processColumnDownwards);
8652 // all that was across all the renderContainers, now we need to work out what that calculation decided for
8653 // our renderContainer
8654 var canvasWidth = 0;
8655 self.visibleColumnCache.forEach(function(column){
8656 if ( column.visible ){
8657 canvasWidth = canvasWidth + column.drawnWidth;
8662 columnCache.forEach(function (column) {
8663 ret = ret + column.getColClassDefinition();
8666 self.canvasWidth = canvasWidth;
8668 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
8671 // Set this render container's column styles so they can be used in style computation
8672 this.columnStyles = ret;
8675 GridRenderContainer.prototype.needsHScrollbarPlaceholder = function () {
8676 return this.grid.options.enableHorizontalScrollbar && !this.hasHScrollbar && !this.grid.disableScrolling;
8679 GridRenderContainer.prototype.getViewportStyle = function () {
8683 self.hasHScrollbar = false;
8684 self.hasVScrollbar = false;
8686 if (self.grid.disableScrolling) {
8687 styles['overflow-x'] = 'hidden';
8688 styles['overflow-y'] = 'hidden';
8692 if (self.name === 'body') {
8693 self.hasHScrollbar = self.grid.options.enableHorizontalScrollbar !== uiGridConstants.scrollbars.NEVER;
8694 if (!self.grid.isRTL()) {
8695 if (!self.grid.hasRightContainerColumns()) {
8696 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8700 if (!self.grid.hasLeftContainerColumns()) {
8701 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8705 else if (self.name === 'left') {
8706 self.hasVScrollbar = self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8709 self.hasVScrollbar = !self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8712 styles['overflow-x'] = self.hasHScrollbar ? 'scroll' : 'hidden';
8713 styles['overflow-y'] = self.hasVScrollbar ? 'scroll' : 'hidden';
8721 return GridRenderContainer;
8728 angular.module('ui.grid')
8729 .factory('GridRow', ['gridUtil', function(gridUtil) {
8733 * @name ui.grid.class:GridRow
8734 * @description GridRow is the viewModel for one logical row on the grid. A grid Row is not necessarily a one-to-one
8735 * relation to gridOptions.data.
8736 * @param {object} entity the array item from GridOptions.data
8737 * @param {number} index the current position of the row in the array
8738 * @param {Grid} reference to the parent grid
8740 function GridRow(entity, index, grid) {
8745 * @propertyOf ui.grid.class:GridRow
8746 * @description A reference back to the grid
8753 * @propertyOf ui.grid.class:GridRow
8754 * @description A reference to an item in gridOptions.data[]
8756 this.entity = entity;
8761 * @propertyOf ui.grid.class:GridRow
8762 * @description UniqueId of row
8764 this.uid = gridUtil.nextUid();
8769 * @propertyOf ui.grid.class:GridRow
8770 * @description If true, the row will be rendered
8773 this.visible = true;
8776 this.$$height = grid.options.rowHeight;
8783 * @propertyOf ui.grid.class:GridRow
8784 * @description height of each individual row. changing the height will flag all
8785 * row renderContainers to recalculate their canvas height
8787 Object.defineProperty(GridRow.prototype, 'height', {
8789 return this.$$height;
8791 set: function(height) {
8792 if (height !== this.$$height) {
8793 this.grid.updateCanvasHeight();
8794 this.$$height = height;
8801 * @name getQualifiedColField
8802 * @methodOf ui.grid.class:GridRow
8803 * @description returns the qualified field name as it exists on scope
8804 * ie: row.entity.fieldA
8805 * @param {GridCol} col column instance
8806 * @returns {string} resulting name that can be evaluated on scope
8808 GridRow.prototype.getQualifiedColField = function(col) {
8809 return 'row.' + this.getEntityQualifiedColField(col);
8814 * @name getEntityQualifiedColField
8815 * @methodOf ui.grid.class:GridRow
8816 * @description returns the qualified field name minus the row path
8818 * @param {GridCol} col column instance
8819 * @returns {string} resulting name that can be evaluated against a row
8821 GridRow.prototype.getEntityQualifiedColField = function(col) {
8822 return gridUtil.preEval('entity.' + col.field);
8828 * @name setRowInvisible
8829 * @methodOf ui.grid.class:GridRow
8830 * @description Sets an override on the row that forces it to always
8831 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8833 * This method can be called from the api, passing in the gridRow we want
8834 * altered. It should really work by calling gridRow.setRowInvisible, but that's
8835 * not the way I coded it, and too late to change now. Changed to just call
8836 * the internal function row.setThisRowInvisible().
8838 * @param {GridRow} row the row we want to set to invisible
8841 GridRow.prototype.setRowInvisible = function ( row ) {
8842 if (row && row.setThisRowInvisible){
8843 row.setThisRowInvisible( 'user' );
8850 * @name clearRowInvisible
8851 * @methodOf ui.grid.class:GridRow
8852 * @description Clears an override on the row that forces it to always
8853 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8855 * This method can be called from the api, passing in the gridRow we want
8856 * altered. It should really work by calling gridRow.clearRowInvisible, but that's
8857 * not the way I coded it, and too late to change now. Changed to just call
8858 * the internal function row.clearThisRowInvisible().
8860 * @param {GridRow} row the row we want to clear the invisible flag
8863 GridRow.prototype.clearRowInvisible = function ( row ) {
8864 if (row && row.clearThisRowInvisible){
8865 row.clearThisRowInvisible( 'user' );
8872 * @name setThisRowInvisible
8873 * @methodOf ui.grid.class:GridRow
8874 * @description Sets an override on the row that forces it to always
8875 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility
8877 * @param {string} reason the reason (usually the module) for the row to be invisible.
8878 * E.g. grouping, user, filter
8879 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8881 GridRow.prototype.setThisRowInvisible = function ( reason, fromRowsProcessor ) {
8882 if ( !this.invisibleReason ){
8883 this.invisibleReason = {};
8885 this.invisibleReason[reason] = true;
8886 this.evaluateRowVisibility( fromRowsProcessor);
8892 * @name clearRowInvisible
8893 * @methodOf ui.grid.class:GridRow
8894 * @description Clears any override on the row visibility, returning it
8895 * to normal visibility calculations. Emits the rowsVisibleChanged
8898 * @param {string} reason the reason (usually the module) for the row to be invisible.
8899 * E.g. grouping, user, filter
8900 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8902 GridRow.prototype.clearThisRowInvisible = function ( reason, fromRowsProcessor ) {
8903 if (typeof(this.invisibleReason) !== 'undefined' ) {
8904 delete this.invisibleReason[reason];
8906 this.evaluateRowVisibility( fromRowsProcessor );
8912 * @name evaluateRowVisibility
8913 * @methodOf ui.grid.class:GridRow
8914 * @description Determines whether the row should be visible based on invisibleReason,
8915 * and if it changes the row visibility, then emits the rowsVisibleChanged event.
8917 * Queues a grid refresh, but doesn't call it directly to avoid hitting lots of grid refreshes.
8918 * @param {boolean} fromRowProcessor if true, then it won't raise events or queue the refresh, the
8919 * row processor does that already
8921 GridRow.prototype.evaluateRowVisibility = function ( fromRowProcessor ) {
8922 var newVisibility = true;
8923 if ( typeof(this.invisibleReason) !== 'undefined' ){
8924 angular.forEach(this.invisibleReason, function( value, key ){
8926 newVisibility = false;
8931 if ( typeof(this.visible) === 'undefined' || this.visible !== newVisibility ){
8932 this.visible = newVisibility;
8933 if ( !fromRowProcessor ){
8934 this.grid.queueGridRefresh();
8935 this.grid.api.core.raise.rowsVisibleChanged(this);
8950 * @name ui.grid.class:GridRowColumn
8951 * @param {GridRow} row The row for this pair
8952 * @param {GridColumn} column The column for this pair
8953 * @description A row and column pair that represents the intersection of these two entities.
8954 * Must be instantiated as a constructor using the `new` keyword.
8956 angular.module('ui.grid')
8957 .factory('GridRowColumn', ['$parse', '$filter',
8958 function GridRowColumnFactory($parse, $filter){
8959 var GridRowColumn = function GridRowColumn(row, col) {
8960 if ( !(this instanceof GridRowColumn)){
8961 throw "Using GridRowColumn as a function insead of as a constructor. Must be called with `new` keyword";
8967 * @propertyOf ui.grid.class:GridRowColumn
8968 * @description {@link ui.grid.class:GridRow }
8974 * @propertyOf ui.grid.class:GridRowColumn
8975 * @description {@link ui.grid.class:GridColumn }
8982 * @name getIntersectionValueRaw
8983 * @methodOf ui.grid.class:GridRowColumn
8984 * @description Gets the intersection of where the row and column meet.
8985 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8986 * If the column has a cellFilter this will NOT return the filtered value.
8988 GridRowColumn.prototype.getIntersectionValueRaw = function(){
8989 var getter = $parse(this.row.getEntityQualifiedColField(this.col));
8990 var context = this.row;
8991 return getter(context);
8995 * @name getIntersectionValueFiltered
8996 * @methodOf ui.grid.class:GridRowColumn
8997 * @description Gets the intersection of where the row and column meet.
8998 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8999 * If the column has a cellFilter this will also apply the filter to it and return the value that the filter displays.
9001 GridRowColumn.prototype.getIntersectionValueFiltered = function(){
9002 var value = this.getIntersectionValueRaw();
9003 if (this.col.cellFilter && this.col.cellFilter !== ''){
9004 var getFilterIfExists = function(filterName){
9006 return $filter(filterName);
9011 var filter = getFilterIfExists(this.col.cellFilter);
9012 if (filter) { // Check if this is filter name or a filter string
9013 value = filter(value);
9014 } else { // We have the template version of a filter so we need to parse it apart
9015 // Get the filter params out using a regex
9016 // Test out this regex here https://regex101.com/r/rC5eR5/2
9017 var re = /([^:]*):([^:]*):?([\s\S]+)?/;
9019 if ((matches = re.exec(this.col.cellFilter)) !== null) {
9020 // View your result using the matches-variable.
9021 // eg matches[0] etc.
9022 value = $filter(matches[1])(value, matches[2], matches[3]);
9028 return GridRowColumn;
9034 angular.module('ui.grid')
9035 .factory('ScrollEvent', ['gridUtil', function (gridUtil) {
9039 * @name ui.grid.class:ScrollEvent
9040 * @description Model for all scrollEvents
9041 * @param {Grid} grid that owns the scroll event
9042 * @param {GridRenderContainer} sourceRowContainer that owns the scroll event. Can be null
9043 * @param {GridRenderContainer} sourceColContainer that owns the scroll event. Can be null
9044 * @param {string} source the source of the event - from uiGridConstants.scrollEventSources or a string value of directive/service/factory.functionName
9046 function ScrollEvent(grid, sourceRowContainer, sourceColContainer, source) {
9049 throw new Error("grid argument is required");
9055 * @propertyOf ui.grid.class:ScrollEvent
9056 * @description A reference back to the grid
9065 * @propertyOf ui.grid.class:ScrollEvent
9066 * @description the source of the scroll event. limited to values from uiGridConstants.scrollEventSources
9068 self.source = source;
9074 * @propertyOf ui.grid.class:ScrollEvent
9075 * @description most scroll events from the mouse or trackpad require delay to operate properly
9076 * set to false to eliminate delay. Useful for scroll events that the grid causes, such as scrolling to make a row visible.
9078 self.withDelay = true;
9080 self.sourceRowContainer = sourceRowContainer;
9081 self.sourceColContainer = sourceColContainer;
9083 self.newScrollLeft = null;
9084 self.newScrollTop = null;
9088 self.verticalScrollLength = -9999999;
9089 self.horizontalScrollLength = -999999;
9094 * @name fireThrottledScrollingEvent
9095 * @methodOf ui.grid.class:ScrollEvent
9096 * @description fires a throttled event using grid.api.core.raise.scrollEvent
9098 self.fireThrottledScrollingEvent = gridUtil.throttle(function(sourceContainerId) {
9099 self.grid.scrollContainers(sourceContainerId, self);
9100 }, self.grid.options.wheelScrollThrottle, {trailing: true});
9107 * @name getNewScrollLeft
9108 * @methodOf ui.grid.class:ScrollEvent
9109 * @description returns newScrollLeft property if available; calculates a new value if it isn't
9111 ScrollEvent.prototype.getNewScrollLeft = function(colContainer, viewport){
9114 if (!self.newScrollLeft){
9115 var scrollWidth = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
9117 var oldScrollLeft = gridUtil.normalizeScrollLeft(viewport, self.grid);
9119 var scrollXPercentage;
9120 if (typeof(self.x.percentage) !== 'undefined' && self.x.percentage !== undefined) {
9121 scrollXPercentage = self.x.percentage;
9123 else if (typeof(self.x.pixels) !== 'undefined' && self.x.pixels !== undefined) {
9124 scrollXPercentage = self.x.percentage = (oldScrollLeft + self.x.pixels) / scrollWidth;
9127 throw new Error("No percentage or pixel value provided for scroll event X axis");
9130 return Math.max(0, scrollXPercentage * scrollWidth);
9133 return self.newScrollLeft;
9139 * @name getNewScrollTop
9140 * @methodOf ui.grid.class:ScrollEvent
9141 * @description returns newScrollTop property if available; calculates a new value if it isn't
9143 ScrollEvent.prototype.getNewScrollTop = function(rowContainer, viewport){
9147 if (!self.newScrollTop){
9148 var scrollLength = rowContainer.getVerticalScrollLength();
9150 var oldScrollTop = viewport[0].scrollTop;
9152 var scrollYPercentage;
9153 if (typeof(self.y.percentage) !== 'undefined' && self.y.percentage !== undefined) {
9154 scrollYPercentage = self.y.percentage;
9156 else if (typeof(self.y.pixels) !== 'undefined' && self.y.pixels !== undefined) {
9157 scrollYPercentage = self.y.percentage = (oldScrollTop + self.y.pixels) / scrollLength;
9160 throw new Error("No percentage or pixel value provided for scroll event Y axis");
9163 return Math.max(0, scrollYPercentage * scrollLength);
9166 return self.newScrollTop;
9169 ScrollEvent.prototype.atTop = function(scrollTop) {
9170 return (this.y && (this.y.percentage === 0 || this.verticalScrollLength < 0) && scrollTop === 0);
9173 ScrollEvent.prototype.atBottom = function(scrollTop) {
9174 return (this.y && (this.y.percentage === 1 || this.verticalScrollLength === 0) && scrollTop > 0);
9177 ScrollEvent.prototype.atLeft = function(scrollLeft) {
9178 return (this.x && (this.x.percentage === 0 || this.horizontalScrollLength < 0) && scrollLeft === 0);
9181 ScrollEvent.prototype.atRight = function(scrollLeft) {
9182 return (this.x && (this.x.percentage === 1 || this.horizontalScrollLength ===0) && scrollLeft > 0);
9186 ScrollEvent.Sources = {
9187 ViewPortScroll: 'ViewPortScroll',
9188 RenderContainerMouseWheel: 'RenderContainerMouseWheel',
9189 RenderContainerTouchMove: 'RenderContainerTouchMove',
9204 * @name ui.grid.service:gridClassFactory
9206 * @description factory to return dom specific instances of a grid
9209 angular.module('ui.grid').service('gridClassFactory', ['gridUtil', '$q', '$compile', '$templateCache', 'uiGridConstants', 'Grid', 'GridColumn', 'GridRow',
9210 function (gridUtil, $q, $compile, $templateCache, uiGridConstants, Grid, GridColumn, GridRow) {
9216 * @methodOf ui.grid.service:gridClassFactory
9217 * @description Creates a new grid instance. Each instance will have a unique id
9218 * @param {object} options An object map of options to pass into the created grid instance.
9219 * @returns {Grid} grid
9221 createGrid : function(options) {
9222 options = (typeof(options) !== 'undefined') ? options : {};
9223 options.id = gridUtil.newId();
9224 var grid = new Grid(options);
9226 // NOTE/TODO: rowTemplate should always be defined...
9227 if (grid.options.rowTemplate) {
9228 var rowTemplateFnPromise = $q.defer();
9229 grid.getRowTemplateFn = rowTemplateFnPromise.promise;
9231 gridUtil.getTemplate(grid.options.rowTemplate)
9233 function (template) {
9234 var rowTemplateFn = $compile(template);
9235 rowTemplateFnPromise.resolve(rowTemplateFn);
9238 // Todo handle response error here?
9239 throw new Error("Couldn't fetch/use row template '" + grid.options.rowTemplate + "'");
9243 grid.registerColumnBuilder(service.defaultColumnBuilder);
9245 // Row builder for custom row templates
9246 grid.registerRowBuilder(service.rowTemplateAssigner);
9248 // Reset all rows to visible initially
9249 grid.registerRowsProcessor(function allRowsVisible(rows) {
9250 rows.forEach(function (row) {
9251 row.evaluateRowVisibility( true );
9257 grid.registerColumnsProcessor(function allColumnsVisible(columns) {
9258 columns.forEach(function (column) {
9259 column.visible = true;
9265 grid.registerColumnsProcessor(function(renderableColumns) {
9266 renderableColumns.forEach(function (column) {
9267 if (column.colDef.visible === false) {
9268 column.visible = false;
9272 return renderableColumns;
9276 grid.registerRowsProcessor(grid.searchRows, 100);
9278 // Register the default row processor, it sorts rows by selected columns
9279 if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) {
9280 grid.registerRowsProcessor(grid.options.externalSort, 200);
9283 grid.registerRowsProcessor(grid.sortByColumn, 200);
9291 * @name defaultColumnBuilder
9292 * @methodOf ui.grid.service:gridClassFactory
9293 * @description Processes designTime column definitions and applies them to col for the
9294 * core grid features
9295 * @param {object} colDef reference to column definition
9296 * @param {GridColumn} col reference to gridCol
9297 * @param {object} gridOptions reference to grid options
9299 defaultColumnBuilder: function (colDef, col, gridOptions) {
9301 var templateGetPromises = [];
9303 // Abstracts the standard template processing we do for every template type.
9304 var processTemplate = function( templateType, providedType, defaultTemplate, filterType, tooltipType ) {
9305 if ( !colDef[templateType] ){
9306 col[providedType] = defaultTemplate;
9308 col[providedType] = colDef[templateType];
9311 templateGetPromises.push(gridUtil.getTemplate(col[providedType])
9313 function (template) {
9314 if ( angular.isFunction(template) ) { template = template(); }
9315 var tooltipCall = ( tooltipType === 'cellTooltip' ) ? 'col.cellTooltip(row,col)' : 'col.headerTooltip(col)';
9316 if ( tooltipType && col[tooltipType] === false ){
9317 template = template.replace(uiGridConstants.TOOLTIP, '');
9318 } else if ( tooltipType && col[tooltipType] ){
9319 template = template.replace(uiGridConstants.TOOLTIP, 'title="{{' + tooltipCall + ' CUSTOM_FILTERS }}"');
9323 col[templateType] = template.replace(uiGridConstants.CUSTOM_FILTERS, function() {
9324 return col[filterType] ? "|" + col[filterType] : "";
9327 col[templateType] = template;
9331 throw new Error("Couldn't fetch/use colDef." + templateType + " '" + colDef[templateType] + "'");
9340 * @name cellTemplate
9341 * @propertyOf ui.grid.class:GridOptions.columnDef
9342 * @description a custom template for each cell in this column. The default
9343 * is ui-grid/uiGridCell. If you are using the cellNav feature, this template
9344 * must contain a div that can receive focus.
9347 processTemplate( 'cellTemplate', 'providedCellTemplate', 'ui-grid/uiGridCell', 'cellFilter', 'cellTooltip' );
9348 col.cellTemplatePromise = templateGetPromises[0];
9352 * @name headerCellTemplate
9353 * @propertyOf ui.grid.class:GridOptions.columnDef
9354 * @description a custom template for the header for this column. The default
9355 * is ui-grid/uiGridHeaderCell
9358 processTemplate( 'headerCellTemplate', 'providedHeaderCellTemplate', 'ui-grid/uiGridHeaderCell', 'headerCellFilter', 'headerTooltip' );
9362 * @name footerCellTemplate
9363 * @propertyOf ui.grid.class:GridOptions.columnDef
9364 * @description a custom template for the footer for this column. The default
9365 * is ui-grid/uiGridFooterCell
9368 processTemplate( 'footerCellTemplate', 'providedFooterCellTemplate', 'ui-grid/uiGridFooterCell', 'footerCellFilter' );
9372 * @name filterHeaderTemplate
9373 * @propertyOf ui.grid.class:GridOptions.columnDef
9374 * @description a custom template for the filter input. The default is ui-grid/ui-grid-filter
9377 processTemplate( 'filterHeaderTemplate', 'providedFilterHeaderTemplate', 'ui-grid/ui-grid-filter' );
9379 // Create a promise for the compiled element function
9380 col.compiledElementFnDefer = $q.defer();
9382 return $q.all(templateGetPromises);
9386 rowTemplateAssigner: function rowTemplateAssigner(row) {
9389 // Row has no template assigned to it
9390 if (!row.rowTemplate) {
9391 // Use the default row template from the grid
9392 row.rowTemplate = grid.options.rowTemplate;
9394 // Use the grid's function for fetching the compiled row template function
9395 row.getRowTemplateFn = grid.getRowTemplateFn;
9397 // Row has its own template assigned
9399 // Create a promise for the compiled row template function
9400 var perRowTemplateFnPromise = $q.defer();
9401 row.getRowTemplateFn = perRowTemplateFnPromise.promise;
9403 // Get the row template
9404 gridUtil.getTemplate(row.rowTemplate)
9405 .then(function (template) {
9406 // Compile the template
9407 var rowTemplateFn = $compile(template);
9409 // Resolve the compiled template function promise
9410 perRowTemplateFnPromise.resolve(rowTemplateFn);
9413 // Todo handle response error here?
9414 throw new Error("Couldn't fetch/use row template '" + row.rowTemplate + "'");
9418 return row.getRowTemplateFn;
9422 //class definitions (moved to separate factories)
9431 var module = angular.module('ui.grid');
9433 function escapeRegExp(str) {
9434 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
9440 * @name ui.grid.service:rowSearcher
9442 * @description Service for searching/filtering rows based on column value conditions.
9444 module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil, uiGridConstants) {
9445 var defaultCondition = uiGridConstants.filter.CONTAINS;
9447 var rowSearcher = {};
9452 * @methodOf ui.grid.service:rowSearcher
9453 * @description Get the term from a filter
9454 * Trims leading and trailing whitespace
9455 * @param {object} filter object to use
9456 * @returns {object} Parsed term
9458 rowSearcher.getTerm = function getTerm(filter) {
9459 if (typeof(filter.term) === 'undefined') { return filter.term; }
9461 var term = filter.term;
9463 // Strip leading and trailing whitespace if the term is a string
9464 if (typeof(term) === 'string') {
9474 * @methodOf ui.grid.service:rowSearcher
9475 * @description Remove leading and trailing asterisk (*) from the filter's term
9476 * @param {object} filter object to use
9477 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9479 rowSearcher.stripTerm = function stripTerm(filter) {
9480 var term = rowSearcher.getTerm(filter);
9482 if (typeof(term) === 'string') {
9483 return escapeRegExp(term.replace(/(^\*|\*$)/g, ''));
9493 * @name guessCondition
9494 * @methodOf ui.grid.service:rowSearcher
9495 * @description Guess the condition for a filter based on its term
9497 * Defaults to STARTS_WITH. Uses CONTAINS for strings beginning and ending with *s (*bob*).
9498 * Uses STARTS_WITH for strings ending with * (bo*). Uses ENDS_WITH for strings starting with * (*ob).
9499 * @param {object} filter object to use
9500 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9502 rowSearcher.guessCondition = function guessCondition(filter) {
9503 if (typeof(filter.term) === 'undefined' || !filter.term) {
9504 return defaultCondition;
9507 var term = rowSearcher.getTerm(filter);
9509 if (/\*/.test(term)) {
9510 var regexpFlags = '';
9511 if (!filter.flags || !filter.flags.caseSensitive) {
9515 var reText = term.replace(/(\\)?\*/g, function ($0, $1) { return $1 ? $0 : '[\\s\\S]*?'; });
9516 return new RegExp('^' + reText + '$', regexpFlags);
9518 // Otherwise default to default condition
9520 return defaultCondition;
9527 * @name setupFilters
9528 * @methodOf ui.grid.service:rowSearcher
9529 * @description For a given columns filters (either col.filters, or [col.filter] can be passed in),
9530 * do all the parsing and pre-processing and store that data into a new filters object. The object
9531 * has the condition, the flags, the stripped term, and a parsed reg exp if there was one.
9533 * We could use a forEach in here, since it's much less performance sensitive, but since we're using
9534 * for loops everywhere else in this module...
9536 * @param {array} filters the filters from the column (col.filters or [col.filter])
9537 * @returns {array} An array of parsed/preprocessed filters
9539 rowSearcher.setupFilters = function setupFilters( filters ){
9540 var newFilters = [];
9542 var filtersLength = filters.length;
9543 for ( var i = 0; i < filtersLength; i++ ){
9544 var filter = filters[i];
9546 if ( filter.noTerm || !gridUtil.isNullOrUndefined(filter.term) ){
9549 var regexpFlags = '';
9550 if (!filter.flags || !filter.flags.caseSensitive) {
9554 if ( !gridUtil.isNullOrUndefined(filter.term) ){
9555 // it is possible to have noTerm. We don't need to copy that across, it was just a flag to avoid
9556 // getting the filter ignored if the filter was a function that didn't use a term
9557 newFilter.term = rowSearcher.stripTerm(filter);
9560 if ( filter.condition ){
9561 newFilter.condition = filter.condition;
9563 newFilter.condition = rowSearcher.guessCondition(filter);
9566 newFilter.flags = angular.extend( { caseSensitive: false, date: false }, filter.flags );
9568 if (newFilter.condition === uiGridConstants.filter.STARTS_WITH) {
9569 newFilter.startswithRE = new RegExp('^' + newFilter.term, regexpFlags);
9572 if (newFilter.condition === uiGridConstants.filter.ENDS_WITH) {
9573 newFilter.endswithRE = new RegExp(newFilter.term + '$', regexpFlags);
9576 if (newFilter.condition === uiGridConstants.filter.CONTAINS) {
9577 newFilter.containsRE = new RegExp(newFilter.term, regexpFlags);
9580 if (newFilter.condition === uiGridConstants.filter.EXACT) {
9581 newFilter.exactRE = new RegExp('^' + newFilter.term + '$', regexpFlags);
9584 newFilters.push(newFilter);
9593 * @name runColumnFilter
9594 * @methodOf ui.grid.service:rowSearcher
9595 * @description Runs a single pre-parsed filter against a cell, returning true
9596 * if the cell matches that one filter.
9598 * @param {Grid} grid the grid we're working against
9599 * @param {GridRow} row the row we're matching against
9600 * @param {GridCol} column the column that we're working against
9601 * @param {object} filter the specific, preparsed, filter that we want to test
9602 * @returns {boolean} true if we match (row stays visible)
9604 rowSearcher.runColumnFilter = function runColumnFilter(grid, row, column, filter) {
9605 // Cache typeof condition
9606 var conditionType = typeof(filter.condition);
9608 // Term to search for.
9609 var term = filter.term;
9611 // Get the column value for this row
9613 if ( column.filterCellFiltered ){
9614 value = grid.getCellDisplayValue(row, column);
9616 value = grid.getCellValue(row, column);
9620 // If the filter's condition is a RegExp, then use it
9621 if (filter.condition instanceof RegExp) {
9622 return filter.condition.test(value);
9625 // If the filter's condition is a function, run it
9626 if (conditionType === 'function') {
9627 return filter.condition(term, value, row, column);
9630 if (filter.startswithRE) {
9631 return filter.startswithRE.test(value);
9634 if (filter.endswithRE) {
9635 return filter.endswithRE.test(value);
9638 if (filter.containsRE) {
9639 return filter.containsRE.test(value);
9642 if (filter.exactRE) {
9643 return filter.exactRE.test(value);
9646 if (filter.condition === uiGridConstants.filter.NOT_EQUAL) {
9647 var regex = new RegExp('^' + term + '$');
9648 return !regex.exec(value);
9651 if (typeof(value) === 'number' && typeof(term) === 'string' ){
9652 // if the term has a decimal in it, it comes through as '9\.4', we need to take out the \
9653 // the same for negative numbers
9654 // TODO: I suspect the right answer is to look at escapeRegExp at the top of this code file, maybe it's not needed?
9655 var tempFloat = parseFloat(term.replace(/\\\./,'.').replace(/\\\-/,'-'));
9656 if (!isNaN(tempFloat)) {
9661 if (filter.flags.date === true) {
9662 value = new Date(value);
9663 // If the term has a dash in it, it comes through as '\-' -- we need to take out the '\'.
9664 term = new Date(term.replace(/\\/g, ''));
9667 if (filter.condition === uiGridConstants.filter.GREATER_THAN) {
9668 return (value > term);
9671 if (filter.condition === uiGridConstants.filter.GREATER_THAN_OR_EQUAL) {
9672 return (value >= term);
9675 if (filter.condition === uiGridConstants.filter.LESS_THAN) {
9676 return (value < term);
9679 if (filter.condition === uiGridConstants.filter.LESS_THAN_OR_EQUAL) {
9680 return (value <= term);
9689 * @name useExternalFiltering
9690 * @propertyOf ui.grid.class:GridOptions
9691 * @description False by default. When enabled, this setting suppresses the internal filtering.
9692 * All UI logic will still operate, allowing filter conditions to be set and modified.
9694 * The external filter logic can listen for the `filterChange` event, which fires whenever
9695 * a filter has been adjusted.
9699 * @name searchColumn
9700 * @methodOf ui.grid.service:rowSearcher
9701 * @description Process provided filters on provided column against a given row. If the row meets
9702 * the conditions on all the filters, return true.
9703 * @param {Grid} grid Grid to search in
9704 * @param {GridRow} row Row to search on
9705 * @param {GridCol} column Column with the filters to use
9706 * @param {array} filters array of pre-parsed/preprocessed filters to apply
9707 * @returns {boolean} Whether the column matches or not.
9709 rowSearcher.searchColumn = function searchColumn(grid, row, column, filters) {
9710 if (grid.options.useExternalFiltering) {
9714 var filtersLength = filters.length;
9715 for (var i = 0; i < filtersLength; i++) {
9716 var filter = filters[i];
9718 var ret = rowSearcher.runColumnFilter(grid, row, column, filter);
9731 * @methodOf ui.grid.service:rowSearcher
9732 * @description Run a search across the given rows and columns, marking any rows that don't
9733 * match the stored col.filters or col.filter as invisible.
9734 * @param {Grid} grid Grid instance to search inside
9735 * @param {Array[GridRow]} rows GridRows to filter
9736 * @param {Array[GridColumn]} columns GridColumns with filters to process
9738 rowSearcher.search = function search(grid, rows, columns) {
9740 * Added performance optimisations into this code base, as this logic creates deeply nested
9741 * loops and is therefore very performance sensitive. In particular, avoiding forEach as
9742 * this impacts some browser optimisers (particularly Chrome), using iterators instead
9745 // Don't do anything if we weren't passed any rows
9750 // don't filter if filtering currently disabled
9751 if (!grid.options.enableFiltering){
9755 // Build list of filters to apply
9756 var filterData = [];
9758 var colsLength = columns.length;
9760 var hasTerm = function( filters ) {
9761 var hasTerm = false;
9763 filters.forEach( function (filter) {
9764 if ( !gridUtil.isNullOrUndefined(filter.term) && filter.term !== '' || filter.noTerm ){
9772 for (var i = 0; i < colsLength; i++) {
9773 var col = columns[i];
9775 if (typeof(col.filters) !== 'undefined' && hasTerm(col.filters) ) {
9776 filterData.push( { col: col, filters: rowSearcher.setupFilters(col.filters) } );
9780 if (filterData.length > 0) {
9781 // define functions outside the loop, performance optimisation
9782 var foreachRow = function(grid, row, col, filters){
9783 if ( row.visible && !rowSearcher.searchColumn(grid, row, col, filters) ) {
9784 row.visible = false;
9788 var foreachFilterCol = function(grid, filterData){
9789 var rowsLength = rows.length;
9790 for ( var i = 0; i < rowsLength; i++){
9791 foreachRow(grid, rows[i], filterData.col, filterData.filters);
9795 // nested loop itself - foreachFilterCol, which in turn calls foreachRow
9796 var filterDataLength = filterData.length;
9797 for ( var j = 0; j < filterDataLength; j++){
9798 foreachFilterCol( grid, filterData[j] );
9801 if (grid.api.core.raise.rowsVisibleChanged) {
9802 grid.api.core.raise.rowsVisibleChanged();
9805 // drop any invisible rows
9806 // keeping these, as needed with filtering for trees - we have to come back and make parent nodes visible if child nodes are selected in the filter
9807 // rows = rows.filter(function(row){ return row.visible; });
9821 var module = angular.module('ui.grid');
9825 * @name ui.grid.class:RowSorter
9826 * @description RowSorter provides the default sorting mechanisms,
9827 * including guessing column types and applying appropriate sort
9832 module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGridConstants) {
9833 var currencyRegexStr =
9835 uiGridConstants.CURRENCY_SYMBOLS
9836 .map(function (a) { return '\\' + a; }) // Escape all the currency symbols ($ at least will jack up this regex)
9837 .join('|') + // Join all the symbols together with |s
9840 // /^[-+]?[£$¤¥]?[\d,.]+%?$/
9841 var numberStrRegex = new RegExp('^[-+]?' + currencyRegexStr + '[\\d,.]+' + currencyRegexStr + '%?$');
9844 // Cache of sorting functions. Once we create them, we don't want to keep re-doing it
9845 // this takes a piece of data from the cell and tries to determine its type and what sorting
9846 // function to use for it
9853 * @methodOf ui.grid.class:RowSorter
9855 * @description Assigns a sort function to use based on the itemType in the column
9856 * @param {string} itemType one of 'number', 'boolean', 'string', 'date', 'object'. And
9857 * error will be thrown for any other type.
9858 * @returns {function} a sort function that will sort that type
9860 rowSorter.guessSortFn = function guessSortFn(itemType) {
9863 return rowSorter.sortNumber;
9865 return rowSorter.sortNumberStr;
9867 return rowSorter.sortBool;
9869 return rowSorter.sortAlpha;
9871 return rowSorter.sortDate;
9873 return rowSorter.basicSort;
9875 throw new Error('No sorting function found for type:' + itemType);
9882 * @methodOf ui.grid.class:RowSorter
9884 * @description Sorts nulls and undefined to the bottom (top when
9885 * descending). Called by each of the internal sorters before
9886 * attempting to sort. Note that this method is available on the core api
9887 * via gridApi.core.sortHandleNulls
9888 * @param {object} a sort value a
9889 * @param {object} b sort value b
9890 * @returns {number} null if there were no nulls/undefineds, otherwise returns
9891 * a sort value that should be passed back from the sort function
9893 rowSorter.handleNulls = function handleNulls(a, b) {
9894 // We want to allow zero values and false values to be evaluated in the sort function
9895 if ((!a && a !== 0 && a !== false) || (!b && b !== 0 && b !== false)) {
9896 // We want to force nulls and such to the bottom when we sort... which effectively is "greater than"
9897 if ((!a && a !== 0 && a !== false) && (!b && b !== 0 && b !== false)) {
9900 else if (!a && a !== 0 && a !== false) {
9903 else if (!b && b !== 0 && b !== false) {
9913 * @methodOf ui.grid.class:RowSorter
9915 * @description Sorts any values that provide the < method, including strings
9916 * or numbers. Handles nulls and undefined through calling handleNulls
9917 * @param {object} a sort value a
9918 * @param {object} b sort value b
9919 * @returns {number} normal sort function, returns -ve, 0, +ve
9921 rowSorter.basicSort = function basicSort(a, b) {
9922 var nulls = rowSorter.handleNulls(a, b);
9923 if ( nulls !== null ){
9939 * @methodOf ui.grid.class:RowSorter
9941 * @description Sorts numerical values. Handles nulls and undefined through calling handleNulls
9942 * @param {object} a sort value a
9943 * @param {object} b sort value b
9944 * @returns {number} normal sort function, returns -ve, 0, +ve
9946 rowSorter.sortNumber = function sortNumber(a, b) {
9947 var nulls = rowSorter.handleNulls(a, b);
9948 if ( nulls !== null ){
9958 * @methodOf ui.grid.class:RowSorter
9959 * @name sortNumberStr
9960 * @description Sorts numerical values that are stored in a string (i.e. parses them to numbers first).
9961 * Handles nulls and undefined through calling handleNulls
9962 * @param {object} a sort value a
9963 * @param {object} b sort value b
9964 * @returns {number} normal sort function, returns -ve, 0, +ve
9966 rowSorter.sortNumberStr = function sortNumberStr(a, b) {
9967 var nulls = rowSorter.handleNulls(a, b);
9968 if ( nulls !== null ){
9971 var numA, // The parsed number form of 'a'
9972 numB, // The parsed number form of 'b'
9976 // Try to parse 'a' to a float
9977 numA = parseFloat(a.replace(/[^0-9.-]/g, ''));
9979 // If 'a' couldn't be parsed to float, flag it as bad
9984 // Try to parse 'b' to a float
9985 numB = parseFloat(b.replace(/[^0-9.-]/g, ''));
9987 // If 'b' couldn't be parsed to float, flag it as bad
9992 // We want bad ones to get pushed to the bottom... which effectively is "greater than"
10005 return numA - numB;
10012 * @methodOf ui.grid.class:RowSorter
10014 * @description Sorts string values. Handles nulls and undefined through calling handleNulls
10015 * @param {object} a sort value a
10016 * @param {object} b sort value b
10017 * @returns {number} normal sort function, returns -ve, 0, +ve
10019 rowSorter.sortAlpha = function sortAlpha(a, b) {
10020 var nulls = rowSorter.handleNulls(a, b);
10021 if ( nulls !== null ){
10024 var strA = a.toString().toLowerCase(),
10025 strB = b.toString().toLowerCase();
10027 return strA === strB ? 0 : strA.localeCompare(strB);
10034 * @methodOf ui.grid.class:RowSorter
10036 * @description Sorts date values. Handles nulls and undefined through calling handleNulls.
10037 * Handles date strings by converting to Date object if not already an instance of Date
10038 * @param {object} a sort value a
10039 * @param {object} b sort value b
10040 * @returns {number} normal sort function, returns -ve, 0, +ve
10042 rowSorter.sortDate = function sortDate(a, b) {
10043 var nulls = rowSorter.handleNulls(a, b);
10044 if ( nulls !== null ){
10047 if (!(a instanceof Date)) {
10050 if (!(b instanceof Date)){
10053 var timeA = a.getTime(),
10054 timeB = b.getTime();
10056 return timeA === timeB ? 0 : (timeA < timeB ? -1 : 1);
10063 * @methodOf ui.grid.class:RowSorter
10065 * @description Sorts boolean values, true is considered larger than false.
10066 * Handles nulls and undefined through calling handleNulls
10067 * @param {object} a sort value a
10068 * @param {object} b sort value b
10069 * @returns {number} normal sort function, returns -ve, 0, +ve
10071 rowSorter.sortBool = function sortBool(a, b) {
10072 var nulls = rowSorter.handleNulls(a, b);
10073 if ( nulls !== null ){
10092 * @methodOf ui.grid.class:RowSorter
10094 * @description Get the sort function for the column. Looks first in
10095 * rowSorter.colSortFnCache using the column name, failing that it
10096 * looks at col.sortingAlgorithm (and puts it in the cache), failing that
10097 * it guesses the sort algorithm based on the data type.
10099 * The cache currently seems a bit pointless, as none of the work we do is
10100 * processor intensive enough to need caching. Presumably in future we might
10101 * inspect the row data itself to guess the sort function, and in that case
10102 * it would make sense to have a cache, the infrastructure is in place to allow
10105 * @param {Grid} grid the grid to consider
10106 * @param {GridCol} col the column to find a function for
10107 * @param {array} rows an array of grid rows. Currently unused, but presumably in future
10108 * we might inspect the rows themselves to decide what sort of data might be there
10109 * @returns {function} the sort function chosen for the column
10111 rowSorter.getSortFn = function getSortFn(grid, col, rows) {
10114 // See if we already figured out what to use to sort the column and have it in the cache
10115 if (rowSorter.colSortFnCache[col.colDef.name]) {
10116 sortFn = rowSorter.colSortFnCache[col.colDef.name];
10118 // If the column has its OWN sorting algorithm, use that
10119 else if (col.sortingAlgorithm !== undefined) {
10120 sortFn = col.sortingAlgorithm;
10121 rowSorter.colSortFnCache[col.colDef.name] = col.sortingAlgorithm;
10123 // Always default to sortAlpha when sorting after a cellFilter
10124 else if ( col.sortCellFiltered && col.cellFilter ){
10125 sortFn = rowSorter.sortAlpha;
10126 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10128 // Try and guess what sort function to use
10130 // Guess the sort function
10131 sortFn = rowSorter.guessSortFn(col.colDef.type);
10133 // If we found a sort function, cache it
10135 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10138 // We assign the alpha sort because anything that is null/undefined will never get passed to
10139 // the actual sorting function. It will get caught in our null check and returned to be sorted
10140 // down to the bottom
10141 sortFn = rowSorter.sortAlpha;
10152 * @methodOf ui.grid.class:RowSorter
10153 * @name prioritySort
10154 * @description Used where multiple columns are present in the sort criteria,
10155 * we determine which column should take precedence in the sort by sorting
10156 * the columns based on their sort.priority
10158 * @param {gridColumn} a column a
10159 * @param {gridColumn} b column b
10160 * @returns {number} normal sort function, returns -ve, 0, +ve
10162 rowSorter.prioritySort = function (a, b) {
10163 // Both columns have a sort priority
10164 if (a.sort.priority !== undefined && b.sort.priority !== undefined) {
10165 // A is higher priority
10166 if (a.sort.priority < b.sort.priority) {
10170 else if (a.sort.priority === b.sort.priority) {
10178 // Only A has a priority
10179 else if (a.sort.priority || a.sort.priority === undefined) {
10182 // Only B has a priority
10183 else if (b.sort.priority || b.sort.priority === undefined) {
10186 // Neither has a priority
10195 * @name useExternalSorting
10196 * @propertyOf ui.grid.class:GridOptions
10197 * @description Prevents the internal sorting from executing. Events will
10198 * still be fired when the sort changes, and the sort information on
10199 * the columns will be updated, allowing an external sorter (for example,
10200 * server sorting) to be implemented. Defaults to false.
10205 * @methodOf ui.grid.class:RowSorter
10207 * @description sorts the grid
10208 * @param {Object} grid the grid itself
10209 * @param {array} rows the rows to be sorted
10210 * @param {array} columns the columns in which to look
10211 * for sort criteria
10212 * @returns {array} sorted rows
10214 rowSorter.sort = function rowSorterSort(grid, rows, columns) {
10215 // first make sure we are even supposed to do work
10220 if (grid.options.useExternalSorting){
10224 // Build the list of columns to sort by
10226 columns.forEach(function (col) {
10227 if (col.sort && !col.sort.ignoreSort && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
10228 sortCols.push(col);
10232 // Sort the "sort columns" by their sort priority
10233 sortCols = sortCols.sort(rowSorter.prioritySort);
10235 // Now rows to sort by, maintain original order
10236 if (sortCols.length === 0) {
10240 // Re-usable variables
10241 var col, direction;
10243 // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10244 var setIndex = function( row, idx ){
10245 row.entity.$$uiGridIndex = idx;
10247 rows.forEach(setIndex);
10249 // IE9-11 HACK.... the 'rows' variable would be empty where we call rowSorter.getSortFn(...) below. We have to use a separate reference
10250 // var d = data.slice(0);
10251 var r = rows.slice(0);
10253 // Now actually sort the data
10254 var rowSortFn = function (rowA, rowB) {
10259 while (tem === 0 && idx < sortCols.length) {
10260 // grab the metadata for the rest of the logic
10261 col = sortCols[idx];
10262 direction = sortCols[idx].sort.direction;
10264 sortFn = rowSorter.getSortFn(grid, col, r);
10268 if ( col.sortCellFiltered ){
10269 propA = grid.getCellDisplayValue(rowA, col);
10270 propB = grid.getCellDisplayValue(rowB, col);
10272 propA = grid.getCellValue(rowA, col);
10273 propB = grid.getCellValue(rowB, col);
10276 tem = sortFn(propA, propB, rowA, rowB, direction);
10281 // Chrome doesn't implement a stable sort function. If our sort returns 0
10282 // (i.e. the items are equal), and we're at the last sort column in the list,
10283 // then return the previous order using our custom
10286 return rowA.entity.$$uiGridIndex - rowB.entity.$$uiGridIndex;
10289 // Made it this far, we don't have to worry about null & undefined
10290 if (direction === uiGridConstants.ASC) {
10297 var newRows = rows.sort(rowSortFn);
10299 // remove the custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10300 var clearIndex = function( row, idx ){
10301 delete row.entity.$$uiGridIndex;
10303 rows.forEach(clearIndex);
10315 var module = angular.module('ui.grid');
10318 if (typeof Function.prototype.bind !== "function") {
10319 bindPolyfill = function() {
10320 var slice = Array.prototype.slice;
10321 return function(context) {
10323 args = slice.call(arguments, 1);
10325 return function() {
10326 return arguments.length ? fn.apply(context, args.concat(slice.call(arguments))) : fn.apply(context, args);
10329 return function() {
10330 return arguments.length ? fn.apply(context, arguments) : fn.call(context);
10336 function getStyles (elem) {
10338 if (typeof(e.length) !== 'undefined' && e.length) {
10342 return e.ownerDocument.defaultView.getComputedStyle(e, null);
10345 var rnumnonpx = new RegExp( "^(" + (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source + ")(?!px)[a-z%]+$", "i" ),
10346 // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
10347 // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
10348 rdisplayswap = /^(block|none|table(?!-c[ea]).+)/,
10349 cssShow = { position: "absolute", visibility: "hidden", display: "block" };
10351 function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
10352 var i = extra === ( isBorderBox ? 'border' : 'content' ) ?
10353 // If we already have the right measurement, avoid augmentation
10355 // Otherwise initialize for horizontal or vertical properties
10356 name === 'width' ? 1 : 0,
10360 var sides = ['Top', 'Right', 'Bottom', 'Left'];
10362 for ( ; i < 4; i += 2 ) {
10363 var side = sides[i];
10364 // dump('side', side);
10366 // both box models exclude margin, so add it if we want it
10367 if ( extra === 'margin' ) {
10368 var marg = parseFloat(styles[extra + side]);
10369 if (!isNaN(marg)) {
10373 // dump('val1', val);
10375 if ( isBorderBox ) {
10376 // border-box includes padding, so remove it if we want content
10377 if ( extra === 'content' ) {
10378 var padd = parseFloat(styles['padding' + side]);
10379 if (!isNaN(padd)) {
10381 // dump('val2', val);
10385 // at this point, extra isn't border nor margin, so remove border
10386 if ( extra !== 'margin' ) {
10387 var bordermarg = parseFloat(styles['border' + side + 'Width']);
10388 if (!isNaN(bordermarg)) {
10390 // dump('val3', val);
10395 // at this point, extra isn't content, so add padding
10396 var nocontentPad = parseFloat(styles['padding' + side]);
10397 if (!isNaN(nocontentPad)) {
10398 val += nocontentPad;
10399 // dump('val4', val);
10402 // at this point, extra isn't content nor padding, so add border
10403 if ( extra !== 'padding') {
10404 var nocontentnopad = parseFloat(styles['border' + side + 'Width']);
10405 if (!isNaN(nocontentnopad)) {
10406 val += nocontentnopad;
10407 // dump('val5', val);
10413 // dump('augVal', val);
10418 function getWidthOrHeight( elem, name, extra ) {
10419 // Start with offset property, which is equivalent to the border-box value
10420 var valueIsBorderBox = true,
10421 val, // = name === 'width' ? elem.offsetWidth : elem.offsetHeight,
10422 styles = getStyles(elem),
10423 isBorderBox = styles['boxSizing'] === 'border-box';
10425 // some non-html elements return undefined for offsetWidth, so check for null/undefined
10426 // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
10427 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
10428 if ( val <= 0 || val == null ) {
10429 // Fall back to computed then uncomputed css if necessary
10430 val = styles[name];
10431 if ( val < 0 || val == null ) {
10432 val = elem.style[ name ];
10435 // Computed unit is not pixels. Stop here and return.
10436 if ( rnumnonpx.test(val) ) {
10440 // we need the check for style in case a browser which returns unreliable values
10441 // for getComputedStyle silently falls back to the reliable elem.style
10442 valueIsBorderBox = isBorderBox &&
10443 ( true || val === elem.style[ name ] ); // use 'true' instead of 'support.boxSizingReliable()'
10445 // Normalize "", auto, and prepare for extra
10446 val = parseFloat( val ) || 0;
10449 // use the active box-sizing model to add/subtract irrelevant styles
10451 augmentWidthOrHeight(
10454 extra || ( isBorderBox ? "border" : "content" ),
10460 // dump('ret', ret, val);
10464 function getLineHeight(elm) {
10465 elm = angular.element(elm)[0];
10466 var parent = elm.parentElement;
10469 parent = document.getElementsByTagName('body')[0];
10472 return parseInt( getStyles(parent).fontSize ) || parseInt( getStyles(elm).fontSize ) || 16;
10475 var uid = ['0', '0', '0', '0'];
10476 var uidPrefix = 'uiGrid-';
10480 * @name ui.grid.service:GridUtil
10482 * @description Grid utility functions
10484 module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateCache', '$timeout', '$interval', '$injector', '$q', '$interpolate', 'uiGridConstants',
10485 function ($log, $window, $document, $http, $templateCache, $timeout, $interval, $injector, $q, $interpolate, uiGridConstants) {
10488 augmentWidthOrHeight: augmentWidthOrHeight,
10490 getStyles: getStyles,
10494 * @name createBoundedWrapper
10495 * @methodOf ui.grid.service:GridUtil
10497 * @param {object} Object to bind 'this' to
10498 * @param {method} Method to bind
10499 * @returns {Function} The wrapper that performs the binding
10502 * Binds given method to given object.
10504 * By means of a wrapper, ensures that ``method`` is always bound to
10505 * ``object`` regardless of its calling environment.
10506 * Iow, inside ``method``, ``this`` always points to ``object``.
10508 * See http://alistapart.com/article/getoutbindingsituations
10511 createBoundedWrapper: function(object, method) {
10512 return function() {
10513 return method.apply(object, arguments);
10520 * @name readableColumnName
10521 * @methodOf ui.grid.service:GridUtil
10523 * @param {string} columnName Column name as a string
10524 * @returns {string} Column name appropriately capitalized and split apart
10527 <example module="app">
10528 <file name="app.js">
10529 var app = angular.module('app', ['ui.grid']);
10531 app.controller('MainCtrl', ['$scope', 'gridUtil', function ($scope, gridUtil) {
10532 $scope.name = 'firstName';
10533 $scope.columnName = function(name) {
10534 return gridUtil.readableColumnName(name);
10538 <file name="index.html">
10539 <div ng-controller="MainCtrl">
10540 <strong>Column name:</strong> <input ng-model="name" />
10542 <strong>Output:</strong> <span ng-bind="columnName(name)"></span>
10547 readableColumnName: function (columnName) {
10548 // Convert underscores to spaces
10549 if (typeof(columnName) === 'undefined' || columnName === undefined || columnName === null) { return columnName; }
10551 if (typeof(columnName) !== 'string') {
10552 columnName = String(columnName);
10555 return columnName.replace(/_+/g, ' ')
10556 // Replace a completely all-capsed word with a first-letter-capitalized version
10557 .replace(/^[A-Z]+$/, function (match) {
10558 return angular.lowercase(angular.uppercase(match.charAt(0)) + match.slice(1));
10560 // Capitalize the first letter of words
10561 .replace(/([\w\u00C0-\u017F]+)/g, function (match) {
10562 return angular.uppercase(match.charAt(0)) + match.slice(1);
10564 // Put a space in between words that have partial capilizations (i.e. 'firstName' becomes 'First Name')
10565 // .replace(/([A-Z]|[A-Z]\w+)([A-Z])/g, "$1 $2");
10566 // .replace(/(\w+?|\w)([A-Z])/g, "$1 $2");
10567 .replace(/(\w+?(?=[A-Z]))/g, '$1 ');
10572 * @name getColumnsFromData
10573 * @methodOf ui.grid.service:GridUtil
10574 * @description Return a list of column names, given a data set
10576 * @param {string} data Data array for grid
10577 * @returns {Object} Column definitions with field accessor and column name
10582 { firstName: 'Bob', lastName: 'Jones' },
10583 { firstName: 'Frank', lastName: 'Smith' }
10586 var columnDefs = GridUtil.getColumnsFromData(data, excludeProperties);
10590 field: 'firstName',
10600 getColumnsFromData: function (data, excludeProperties) {
10601 var columnDefs = [];
10603 if (!data || typeof(data[0]) === 'undefined' || data[0] === undefined) { return []; }
10604 if (angular.isUndefined(excludeProperties)) { excludeProperties = []; }
10606 var item = data[0];
10608 angular.forEach(item,function (prop, propName) {
10609 if ( excludeProperties.indexOf(propName) === -1){
10622 * @methodOf ui.grid.service:GridUtil
10623 * @description Return a unique ID string
10625 * @returns {string} Unique string
10629 var id = GridUtil.newId();
10634 newId: (function() {
10635 var seedId = new Date().getTime();
10636 return function() {
10637 return seedId += 1;
10644 * @name getTemplate
10645 * @methodOf ui.grid.service:GridUtil
10646 * @description Get's template from cache / element / url
10648 * @param {string|element|promise} Either a string representing the template id, a string representing the template url,
10649 * an jQuery/Angualr element, or a promise that returns the template contents to use.
10650 * @returns {object} a promise resolving to template contents
10654 GridUtil.getTemplate(url).then(function (contents) {
10659 getTemplate: function (template) {
10660 // Try to fetch the template out of the templateCache
10661 if ($templateCache.get(template)) {
10662 return s.postProcessTemplate($templateCache.get(template));
10665 // See if the template is itself a promise
10666 if (template.hasOwnProperty('then')) {
10667 return template.then(s.postProcessTemplate);
10670 // If the template is an element, return the element
10672 if (angular.element(template).length > 0) {
10673 return $q.when(template).then(s.postProcessTemplate);
10677 //do nothing; not valid html
10680 s.logDebug('fetching url', template);
10682 // Default to trying to fetch the template as a url with $http
10683 return $http({ method: 'GET', url: template})
10685 function (result) {
10686 var templateHtml = result.data.trim();
10687 //put in templateCache for next call
10688 $templateCache.put(template, templateHtml);
10689 return templateHtml;
10692 throw new Error("Could not get template " + template + ": " + err);
10695 .then(s.postProcessTemplate);
10699 postProcessTemplate: function (template) {
10700 var startSym = $interpolate.startSymbol(),
10701 endSym = $interpolate.endSymbol();
10703 // If either of the interpolation symbols have been changed, we need to alter this template
10704 if (startSym !== '{{' || endSym !== '}}') {
10705 template = template.replace(/\{\{/g, startSym);
10706 template = template.replace(/\}\}/g, endSym);
10709 return $q.when(template);
10715 * @methodOf ui.grid.service:GridUtil
10716 * @description guesses the type of an argument
10718 * @param {string/number/bool/object} item variable to examine
10719 * @returns {string} one of the following
10726 guessType : function (item) {
10727 var itemType = typeof(item);
10729 // Check for numbers and booleans
10730 switch (itemType) {
10736 if (angular.isDate(item)) {
10746 * @name elementWidth
10747 * @methodOf ui.grid.service:GridUtil
10749 * @param {element} element DOM element
10750 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10752 * @returns {number} Element width in pixels, accounting for any borders, etc.
10754 elementWidth: function (elem) {
10760 * @name elementHeight
10761 * @methodOf ui.grid.service:GridUtil
10763 * @param {element} element DOM element
10764 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10766 * @returns {number} Element height in pixels, accounting for any borders, etc.
10768 elementHeight: function (elem) {
10772 // Thanks to http://stackoverflow.com/a/13382873/888165
10773 getScrollbarWidth: function() {
10774 var outer = document.createElement("div");
10775 outer.style.visibility = "hidden";
10776 outer.style.width = "100px";
10777 outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
10779 document.body.appendChild(outer);
10781 var widthNoScroll = outer.offsetWidth;
10782 // force scrollbars
10783 outer.style.overflow = "scroll";
10786 var inner = document.createElement("div");
10787 inner.style.width = "100%";
10788 outer.appendChild(inner);
10790 var widthWithScroll = inner.offsetWidth;
10793 outer.parentNode.removeChild(outer);
10795 return widthNoScroll - widthWithScroll;
10798 swap: function( elem, options, callback, args ) {
10802 // Remember the old values, and insert the new ones
10803 for ( name in options ) {
10804 old[ name ] = elem.style[ name ];
10805 elem.style[ name ] = options[ name ];
10808 ret = callback.apply( elem, args || [] );
10810 // Revert the old values
10811 for ( name in options ) {
10812 elem.style[ name ] = old[ name ];
10818 fakeElement: function( elem, options, callback, args ) {
10820 newElement = angular.element(elem).clone()[0];
10822 for ( name in options ) {
10823 newElement.style[ name ] = options[ name ];
10826 angular.element(document.body).append(newElement);
10828 ret = callback.call( newElement, newElement );
10830 angular.element(newElement).remove();
10837 * @name normalizeWheelEvent
10838 * @methodOf ui.grid.service:GridUtil
10840 * @param {event} event A mouse wheel event
10842 * @returns {event} A normalized event
10845 * Given an event from this list:
10847 * `wheel, mousewheel, DomMouseScroll, MozMousePixelScroll`
10850 * so that it stays consistent no matter what browser it comes from (i.e. scale it correctly and make sure the direction is right.)
10852 normalizeWheelEvent: function (event) {
10853 // var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];
10854 // var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
10855 var lowestDelta, lowestDeltaXY;
10857 var orgEvent = event || window.event,
10858 args = [].slice.call(arguments, 1),
10866 // event = $.event.fix(orgEvent);
10867 // event.type = 'mousewheel';
10869 // NOTE: jQuery masks the event and stores it in the event as originalEvent
10870 if (orgEvent.originalEvent) {
10871 orgEvent = orgEvent.originalEvent;
10874 // Old school scrollwheel delta
10875 if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; }
10876 if ( orgEvent.detail ) { delta = orgEvent.detail * -1; }
10878 // At a minimum, setup the deltaY to be delta
10881 // Firefox < 17 related to DOMMouseScroll event
10882 if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
10884 deltaX = delta * -1;
10887 // New school wheel delta (wheel event)
10888 if ( orgEvent.deltaY ) {
10889 deltaY = orgEvent.deltaY * -1;
10892 if ( orgEvent.deltaX ) {
10893 deltaX = orgEvent.deltaX;
10894 delta = deltaX * -1;
10898 if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; }
10899 if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX; }
10901 // Look for lowest delta to normalize the delta values
10902 absDelta = Math.abs(delta);
10903 if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; }
10904 absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX));
10905 if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; }
10907 // Get a whole value for the deltas
10908 fn = delta > 0 ? 'floor' : 'ceil';
10909 delta = Math[fn](delta / lowestDelta);
10910 deltaX = Math[fn](deltaX / lowestDeltaXY);
10911 deltaY = Math[fn](deltaY / lowestDeltaXY);
10920 // Stolen from Modernizr
10921 // TODO: make this, and everythign that flows from it, robust
10922 //http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
10923 isTouchEnabled: function() {
10926 if (('ontouchstart' in $window) || $window.DocumentTouch && $document instanceof DocumentTouch) {
10933 isNullOrUndefined: function(obj) {
10934 if (obj === undefined || obj === null) {
10940 endsWith: function(str, suffix) {
10941 if (!str || !suffix || typeof str !== "string") {
10944 return str.indexOf(suffix, str.length - suffix.length) !== -1;
10947 arrayContainsObjectWithProperty: function(array, propertyName, propertyValue) {
10949 angular.forEach(array, function (object) {
10950 if (object[propertyName] === propertyValue) {
10957 //// Shim requestAnimationFrame
10958 //requestAnimationFrame: $window.requestAnimationFrame && $window.requestAnimationFrame.bind($window) ||
10959 // $window.webkitRequestAnimationFrame && $window.webkitRequestAnimationFrame.bind($window) ||
10961 // return $timeout(fn, 10, false);
10964 numericAndNullSort: function (a, b) {
10965 if (a === null) { return 1; }
10966 if (b === null) { return -1; }
10967 if (a === null && b === null) { return 0; }
10971 // Disable ngAnimate animations on an element
10972 disableAnimations: function (element) {
10975 $animate = $injector.get('$animate');
10976 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10977 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10978 $animate.enabled(element, false);
10980 $animate.enabled(false, element);
10986 enableAnimations: function (element) {
10989 $animate = $injector.get('$animate');
10990 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10991 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10992 $animate.enabled(element, true);
10994 $animate.enabled(true, element);
11001 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
11002 nextUid: function nextUid() {
11003 var index = uid.length;
11008 digit = uid[index].charCodeAt(0);
11009 if (digit === 57 /*'9'*/) {
11011 return uidPrefix + uid.join('');
11013 if (digit === 90 /*'Z'*/) {
11016 uid[index] = String.fromCharCode(digit + 1);
11017 return uidPrefix + uid.join('');
11022 return uidPrefix + uid.join('');
11025 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
11026 hashKey: function hashKey(obj) {
11027 var objType = typeof obj,
11030 if (objType === 'object' && obj !== null) {
11031 if (typeof (key = obj.$$hashKey) === 'function') {
11032 // must invoke on object to keep the right this
11033 key = obj.$$hashKey();
11035 else if (typeof(obj.$$hashKey) !== 'undefined' && obj.$$hashKey) {
11036 key = obj.$$hashKey;
11038 else if (key === undefined) {
11039 key = obj.$$hashKey = s.nextUid();
11046 return objType + ':' + key;
11049 resetUids: function () {
11050 uid = ['0', '0', '0'];
11055 * @methodOf ui.grid.service:GridUtil
11057 * @description wraps the $log method, allowing us to choose different
11058 * treatment within ui-grid if we so desired. At present we only log
11059 * error messages if uiGridConstants.LOG_ERROR_MESSAGES is set to true
11060 * @param {string} logMessage message to be logged to the console
11063 logError: function( logMessage ){
11064 if ( uiGridConstants.LOG_ERROR_MESSAGES ){
11065 $log.error( logMessage );
11071 * @methodOf ui.grid.service:GridUtil
11073 * @description wraps the $log method, allowing us to choose different
11074 * treatment within ui-grid if we so desired. At present we only log
11075 * warning messages if uiGridConstants.LOG_WARN_MESSAGES is set to true
11076 * @param {string} logMessage message to be logged to the console
11079 logWarn: function( logMessage ){
11080 if ( uiGridConstants.LOG_WARN_MESSAGES ){
11081 $log.warn( logMessage );
11087 * @methodOf ui.grid.service:GridUtil
11089 * @description wraps the $log method, allowing us to choose different
11090 * treatment within ui-grid if we so desired. At present we only log
11091 * debug messages if uiGridConstants.LOG_DEBUG_MESSAGES is set to true
11094 logDebug: function() {
11095 if ( uiGridConstants.LOG_DEBUG_MESSAGES ){
11096 $log.debug.apply($log, arguments);
11105 * @propertyOf ui.grid.service:GridUtil
11106 * @description Provies a set of methods to set the document focus inside the grid.
11107 * See {@link ui.grid.service:GridUtil.focus} for more information.
11112 * @name ui.grid.service:GridUtil.focus
11113 * @description Provies a set of methods to set the document focus inside the grid.
11114 * Timeouts are utilized to ensure that the focus is invoked after any other event has been triggered.
11115 * e.g. click events that need to run before the focus or
11116 * inputs elements that are in a disabled state but are enabled when those events
11121 //http://stackoverflow.com/questions/25596399/set-element-focus-in-angular-way
11124 * @methodOf ui.grid.service:GridUtil.focus
11126 * @description Sets the focus of the document to the given id value.
11127 * If provided with the grid object it will automatically append the grid id.
11128 * This is done to encourage unique dom id's as it allows for multiple grids on a
11130 * @param {String} id the id of the dom element to set the focus on
11131 * @param {Object=} Grid the grid object for this grid instance. See: {@link ui.grid.class:Grid}
11132 * @param {Number} Grid.id the unique id for this grid. Already set on an initialized grid object.
11133 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11134 * then the promise will fail with the `'canceled'` reason.
11136 byId: function (id, Grid) {
11137 this._purgeQueue();
11138 var promise = $timeout(function() {
11139 var elementID = (Grid && Grid.id ? Grid.id + '-' : '') + id;
11140 var element = $window.document.getElementById(elementID);
11144 s.logWarn('[focus.byId] Element id ' + elementID + ' was not found.');
11147 this.queue.push(promise);
11153 * @methodOf ui.grid.service:GridUtil.focus
11155 * @description Sets the focus of the document to the given dom element.
11156 * @param {(element|angular.element)} element the DOM element to set the focus on
11157 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11158 * then the promise will fail with the `'canceled'` reason.
11160 byElement: function(element){
11161 if (!angular.isElement(element)){
11162 s.logWarn("Trying to focus on an element that isn\'t an element.");
11163 return $q.reject('not-element');
11165 element = angular.element(element);
11166 this._purgeQueue();
11167 var promise = $timeout(function(){
11169 element[0].focus();
11172 this.queue.push(promise);
11177 * @methodOf ui.grid.service:GridUtil.focus
11179 * @description Sets the focus of the document to the given dom element.
11180 * @param {(element|angular.element)} parentElement the parent/ancestor of the dom element that you are selecting using the query selector
11181 * @param {String} querySelector finds the dom element using the {@link http://www.w3schools.com/jsref/met_document_queryselector.asp querySelector}
11182 * @param {boolean} [aSync=false] If true then the selector will be querried inside of a timeout. Otherwise the selector will be querried imidately
11183 * then the focus will be called.
11184 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11185 * then the promise will fail with the `'canceled'` reason.
11187 bySelector: function(parentElement, querySelector, aSync){
11189 if (!angular.isElement(parentElement)){
11190 throw new Error("The parent element is not an element.");
11192 // Ensure that this is an angular element.
11193 // It is fine if this is already an angular element.
11194 parentElement = angular.element(parentElement);
11195 var focusBySelector = function(){
11196 var element = parentElement[0].querySelector(querySelector);
11197 return self.byElement(element);
11199 this._purgeQueue();
11200 if (aSync){ //Do this asynchronysly
11201 var promise = $timeout(focusBySelector);
11202 this.queue.push($timeout(focusBySelector));
11205 return focusBySelector();
11208 _purgeQueue: function(){
11209 this.queue.forEach(function(element){
11210 $timeout.cancel(element);
11217 ['width', 'height'].forEach(function (name) {
11218 var capsName = angular.uppercase(name.charAt(0)) + name.substr(1);
11219 s['element' + capsName] = function (elem, extra) {
11221 if (e && typeof(e.length) !== 'undefined' && e.length) {
11226 var styles = getStyles(e);
11227 return e.offsetWidth === 0 && rdisplayswap.test(styles.display) ?
11228 s.swap(e, cssShow, function() {
11229 return getWidthOrHeight(e, name, extra );
11231 getWidthOrHeight( e, name, extra );
11238 s['outerElement' + capsName] = function (elem, margin) {
11239 return elem ? s['element' + capsName].call(this, elem, margin ? 'margin' : 'border') : null;
11243 // http://stackoverflow.com/a/24107550/888165
11244 s.closestElm = function closestElm(el, selector) {
11245 if (typeof(el.length) !== 'undefined' && el.length) {
11251 // find vendor prefix
11252 ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) {
11253 if (typeof document.body[fn] === 'function') {
11260 // traverse parents
11262 while (el !== null) {
11263 parent = el.parentElement;
11264 if (parent !== null && parent[matchesFn](selector)) {
11273 s.type = function (obj) {
11274 var text = Function.prototype.toString.call(obj.constructor);
11275 return text.match(/function (.*?)\(/)[1];
11278 s.getBorderSize = function getBorderSize(elem, borderType) {
11279 if (typeof(elem.length) !== 'undefined' && elem.length) {
11283 var styles = getStyles(elem);
11285 // If a specific border is supplied, like 'top', read the 'borderTop' style property
11287 borderType = 'border' + borderType.charAt(0).toUpperCase() + borderType.slice(1);
11290 borderType = 'border';
11293 borderType += 'Width';
11295 var val = parseInt(styles[borderType], 10);
11305 // http://stackoverflow.com/a/22948274/888165
11306 // TODO: Opera? Mobile?
11307 s.detectBrowser = function detectBrowser() {
11308 var userAgent = $window.navigator.userAgent;
11310 var browsers = {chrome: /chrome/i, safari: /safari/i, firefox: /firefox/i, ie: /internet explorer|trident\//i};
11312 for (var key in browsers) {
11313 if (browsers[key].test(userAgent)) {
11321 // Borrowed from https://github.com/othree/jquery.rtl-scroll-type
11322 // Determine the scroll "type" this browser is using for RTL
11323 s.rtlScrollType = function rtlScrollType() {
11324 if (rtlScrollType.type) {
11325 return rtlScrollType.type;
11328 var definer = angular.element('<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>')[0],
11331 document.body.appendChild(definer);
11333 if (definer.scrollLeft > 0) {
11337 definer.scrollLeft = 1;
11338 if (definer.scrollLeft === 0) {
11343 angular.element(definer).remove();
11344 rtlScrollType.type = type;
11351 * @name normalizeScrollLeft
11352 * @methodOf ui.grid.service:GridUtil
11354 * @param {element} element The element to get the `scrollLeft` from.
11355 * @param {grid} grid - grid used to normalize (uses the rtl property)
11357 * @returns {number} A normalized scrollLeft value for the current browser.
11360 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method normalizes them
11362 s.normalizeScrollLeft = function normalizeScrollLeft(element, grid) {
11363 if (typeof(element.length) !== 'undefined' && element.length) {
11364 element = element[0];
11367 var scrollLeft = element.scrollLeft;
11369 if (grid.isRTL()) {
11370 switch (s.rtlScrollType()) {
11372 return element.scrollWidth - scrollLeft - element.clientWidth;
11374 return Math.abs(scrollLeft);
11385 * @name denormalizeScrollLeft
11386 * @methodOf ui.grid.service:GridUtil
11388 * @param {element} element The element to normalize the `scrollLeft` value for
11389 * @param {number} scrollLeft The `scrollLeft` value to denormalize.
11390 * @param {grid} grid The grid that owns the scroll event.
11392 * @returns {number} A normalized scrollLeft value for the current browser.
11395 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method denormalizes a value for the current browser.
11397 s.denormalizeScrollLeft = function denormalizeScrollLeft(element, scrollLeft, grid) {
11398 if (typeof(element.length) !== 'undefined' && element.length) {
11399 element = element[0];
11402 if (grid.isRTL()) {
11403 switch (s.rtlScrollType()) {
11405 // Get the max scroll for the element
11406 var maxScrollLeft = element.scrollWidth - element.clientWidth;
11408 // Subtract the current scroll amount from the max scroll
11409 return maxScrollLeft - scrollLeft;
11411 return scrollLeft * -1;
11423 * @methodOf ui.grid.service:GridUtil
11425 * @param {string} path Path to evaluate
11427 * @returns {string} A path that is normalized.
11430 * Takes a field path and converts it to bracket notation to allow for special characters in path
11433 * gridUtil.preEval('property') == 'property'
11434 * gridUtil.preEval('nested.deep.prop-erty') = "nested['deep']['prop-erty']"
11437 s.preEval = function (path) {
11438 var m = uiGridConstants.BRACKET_REGEXP.exec(path);
11440 return (m[1] ? s.preEval(m[1]) : m[1]) + m[2] + (m[3] ? s.preEval(m[3]) : m[3]);
11442 path = path.replace(uiGridConstants.APOS_REGEXP, '\\\'');
11443 var parts = path.split(uiGridConstants.DOT_REGEXP);
11444 var preparsed = [parts.shift()]; // first item must be var notation, thus skip
11445 angular.forEach(parts, function (part) {
11446 preparsed.push(part.replace(uiGridConstants.FUNC_REGEXP, '\']$1'));
11448 return preparsed.join('[\'');
11455 * @methodOf ui.grid.service:GridUtil
11457 * @param {function} func function to debounce
11458 * @param {number} wait milliseconds to delay
11459 * @param {boolean} immediate execute before delay
11461 * @returns {function} A function that can be executed as debounced function
11464 * Copied from https://github.com/shahata/angular-debounce
11465 * Takes a function, decorates it to execute only 1 time after multiple calls, and returns the decorated function
11468 * var debouncedFunc = gridUtil.debounce(function(){alert('debounced');}, 500);
11474 s.debounce = function (func, wait, immediate) {
11475 var timeout, args, context, result;
11476 function debounce() {
11477 /* jshint validthis:true */
11480 var later = function () {
11483 result = func.apply(context, args);
11486 var callNow = immediate && !timeout;
11488 $timeout.cancel(timeout);
11490 timeout = $timeout(later, wait, false);
11492 result = func.apply(context, args);
11496 debounce.cancel = function () {
11497 $timeout.cancel(timeout);
11506 * @methodOf ui.grid.service:GridUtil
11508 * @param {function} func function to throttle
11509 * @param {number} wait milliseconds to delay after first trigger
11510 * @param {Object} params to use in throttle.
11512 * @returns {function} A function that can be executed as throttled function
11515 * Adapted from debounce function (above)
11516 * Potential keys for Params Object are:
11517 * trailing (bool) - whether to trigger after throttle time ends if called multiple times
11518 * Updated to use $interval rather than $timeout, as protractor (e2e tests) is able to work with $interval,
11519 * but not with $timeout
11521 * Note that when using throttle, you need to use throttle to create a new function upfront, then use the function
11522 * return from that call each time you need to call throttle. If you call throttle itself repeatedly, the lastCall
11523 * variable will get overwritten and the throttling won't work
11527 * var throttledFunc = gridUtil.throttle(function(){console.log('throttled');}, 500, {trailing: true});
11528 * throttledFunc(); //=> logs throttled
11529 * throttledFunc(); //=> queues attempt to log throttled for ~500ms (since trailing param is truthy)
11530 * throttledFunc(); //=> updates arguments to keep most-recent request, but does not do anything else.
11533 s.throttle = function(func, wait, options){
11534 options = options || {};
11535 var lastCall = 0, queued = null, context, args;
11537 function runFunc(endDate){
11538 lastCall = +new Date();
11539 func.apply(context, args);
11540 $interval(function(){ queued = null; }, 0, 1, false);
11544 /* jshint validthis:true */
11547 if (queued === null){
11548 var sinceLast = +new Date() - lastCall;
11549 if (sinceLast > wait){
11552 else if (options.trailing){
11553 queued = $interval(runFunc, wait - sinceLast, 1, false);
11563 s.addOff = function (eventName) {
11564 s.off[eventName] = function (elm, fn) {
11565 var idx = s._events[eventName].indexOf(fn);
11567 s._events[eventName].removeAt(idx);
11572 var mouseWheeltoBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
11573 nullLowestDeltaTimeout,
11576 s.on.mousewheel = function (elm, fn) {
11577 if (!elm || !fn) { return; }
11579 var $elm = angular.element(elm);
11581 // Store the line height and page height for this particular element
11582 $elm.data('mousewheel-line-height', getLineHeight($elm));
11583 $elm.data('mousewheel-page-height', s.elementHeight($elm));
11584 if (!$elm.data('mousewheel-callbacks')) { $elm.data('mousewheel-callbacks', {}); }
11586 var cbs = $elm.data('mousewheel-callbacks');
11587 cbs[fn] = (Function.prototype.bind || bindPolyfill).call(mousewheelHandler, $elm[0], fn);
11589 // Bind all the mousew heel events
11590 for ( var i = mouseWheeltoBind.length; i; ) {
11591 $elm.on(mouseWheeltoBind[--i], cbs[fn]);
11594 s.off.mousewheel = function (elm, fn) {
11595 var $elm = angular.element(elm);
11597 var cbs = $elm.data('mousewheel-callbacks');
11598 var handler = cbs[fn];
11601 for ( var i = mouseWheeltoBind.length; i; ) {
11602 $elm.off(mouseWheeltoBind[--i], handler);
11608 if (Object.keys(cbs).length === 0) {
11609 $elm.removeData('mousewheel-line-height');
11610 $elm.removeData('mousewheel-page-height');
11611 $elm.removeData('mousewheel-callbacks');
11615 function mousewheelHandler(fn, event) {
11616 var $elm = angular.element(this);
11625 // jQuery masks events
11626 if (event.originalEvent) { event = event.originalEvent; }
11628 if ( 'detail' in event ) { deltaY = event.detail * -1; }
11629 if ( 'wheelDelta' in event ) { deltaY = event.wheelDelta; }
11630 if ( 'wheelDeltaY' in event ) { deltaY = event.wheelDeltaY; }
11631 if ( 'wheelDeltaX' in event ) { deltaX = event.wheelDeltaX * -1; }
11633 // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
11634 if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
11635 deltaX = deltaY * -1;
11639 // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
11640 delta = deltaY === 0 ? deltaX : deltaY;
11642 // New school wheel delta (wheel event)
11643 if ( 'deltaY' in event ) {
11644 deltaY = event.deltaY * -1;
11647 if ( 'deltaX' in event ) {
11648 deltaX = event.deltaX;
11649 if ( deltaY === 0 ) { delta = deltaX * -1; }
11652 // No change actually happened, no reason to go any further
11653 if ( deltaY === 0 && deltaX === 0 ) { return; }
11655 // Need to convert lines and pages to pixels if we aren't already in pixels
11656 // There are three delta modes:
11657 // * deltaMode 0 is by pixels, nothing to do
11658 // * deltaMode 1 is by lines
11659 // * deltaMode 2 is by pages
11660 if ( event.deltaMode === 1 ) {
11661 var lineHeight = $elm.data('mousewheel-line-height');
11662 delta *= lineHeight;
11663 deltaY *= lineHeight;
11664 deltaX *= lineHeight;
11666 else if ( event.deltaMode === 2 ) {
11667 var pageHeight = $elm.data('mousewheel-page-height');
11668 delta *= pageHeight;
11669 deltaY *= pageHeight;
11670 deltaX *= pageHeight;
11673 // Store lowest absolute delta to normalize the delta values
11674 absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );
11676 if ( !lowestDelta || absDelta < lowestDelta ) {
11677 lowestDelta = absDelta;
11679 // Adjust older deltas if necessary
11680 if ( shouldAdjustOldDeltas(event, absDelta) ) {
11685 // Get a whole, normalized value for the deltas
11686 delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta);
11687 deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
11688 deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);
11690 event.deltaMode = 0;
11692 // Normalise offsetX and offsetY properties
11693 // if ($elm[0].getBoundingClientRect ) {
11694 // var boundingRect = $(elm)[0].getBoundingClientRect();
11695 // offsetX = event.clientX - boundingRect.left;
11696 // offsetY = event.clientY - boundingRect.top;
11699 // event.deltaX = deltaX;
11700 // event.deltaY = deltaY;
11701 // event.deltaFactor = lowestDelta;
11704 originalEvent: event,
11707 deltaFactor: lowestDelta,
11708 preventDefault: function () { event.preventDefault(); },
11709 stopPropagation: function () { event.stopPropagation(); }
11712 // Clearout lowestDelta after sometime to better
11713 // handle multiple device types that give
11714 // a different lowestDelta
11715 // Ex: trackpad = 3 and mouse wheel = 120
11716 if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
11717 nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);
11719 fn.call($elm[0], newEvent);
11722 function nullLowestDelta() {
11723 lowestDelta = null;
11726 function shouldAdjustOldDeltas(orgEvent, absDelta) {
11727 // If this is an older event and the delta is divisable by 120,
11728 // then we are assuming that the browser is treating this as an
11729 // older mouse wheel event and that we should divide the deltas
11730 // by 40 to try and get a more usable deltaFactor.
11731 // Side note, this actually impacts the reported scroll distance
11732 // in older browsers and can cause scrolling to be slower than native.
11733 // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
11734 return orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
11740 // Add 'px' to the end of a number string if it doesn't have it already
11741 module.filter('px', function() {
11742 return function(str) {
11743 if (str.match(/^[\d\.]+$/)) {
11755 angular.module('ui.grid').config(['$provide', function($provide) {
11756 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11762 description: 'Přesuňte záhlaví zde pro vytvoření skupiny dle sloupce.'
11765 placeholder: 'Hledat...',
11766 showingItems: 'Zobrazuji položky:',
11767 selectedItems: 'Vybrané položky:',
11768 totalItems: 'Celkem položek:',
11769 size: 'Velikost strany:',
11770 first: 'První strana',
11771 next: 'Další strana',
11772 previous: 'Předchozí strana',
11773 last: 'Poslední strana'
11776 text: 'Vyberte sloupec:'
11779 ascending: 'Seřadit od A-Z',
11780 descending: 'Seřadit od Z-A',
11781 remove: 'Odebrat seřazení'
11784 hide: 'Schovat sloupec'
11787 count: 'celkem řádků: ',
11794 pinLeft: 'Zamknout vlevo',
11795 pinRight: 'Zamknout vpravo',
11799 columns: 'Sloupce:',
11800 importerTitle: 'Importovat soubor',
11801 exporterAllAsCsv: 'Exportovat všechna data do csv',
11802 exporterVisibleAsCsv: 'Exportovat viditelná data do csv',
11803 exporterSelectedAsCsv: 'Exportovat vybraná data do csv',
11804 exporterAllAsPdf: 'Exportovat všechna data do pdf',
11805 exporterVisibleAsPdf: 'Exportovat viditelná data do pdf',
11806 exporterSelectedAsPdf: 'Exportovat vybraná data do pdf',
11807 clearAllFilters: 'Odstranit všechny filtry'
11810 noHeaders: 'Názvy sloupců se nepodařilo získat, obsahuje soubor záhlaví?',
11811 noObjects: 'Data se nepodařilo zpracovat, obsahuje soubor řádky mimo záhlaví?',
11812 invalidCsv: 'Soubor nelze zpracovat, jedná se o CSV?',
11813 invalidJson: 'Soubor nelze zpracovat, je to JSON?',
11814 jsonNotArray: 'Soubor musí obsahovat json. Ukončuji..'
11817 sizes: 'položek na stránku',
11818 totalItems: 'položek'
11822 ungroup: 'Odebrat seskupení',
11823 aggregate_count: 'Agregace: Count',
11824 aggregate_sum: 'Agregace: Sum',
11825 aggregate_max: 'Agregace: Max',
11826 aggregate_min: 'Agregace: Min',
11827 aggregate_avg: 'Agregace: Avg',
11828 aggregate_remove: 'Agregace: Odebrat'
11832 // support varianty of different czech keys.
11833 $delegate.add('cs', lang);
11834 $delegate.add('cz', lang);
11835 $delegate.add('cs-cz', lang);
11836 $delegate.add('cs-CZ', lang);
11843 angular.module('ui.grid').config(['$provide', function($provide) {
11844 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11845 $delegate.add('da', {
11850 description: 'Grupér rækker udfra en kolonne ved at trække dens overskift hertil.'
11853 placeholder: 'Søg...',
11854 showingItems: 'Viste rækker:',
11855 selectedItems: 'Valgte rækker:',
11856 totalItems: 'Rækker totalt:',
11857 size: 'Side størrelse:',
11858 first: 'Første side',
11859 next: 'Næste side',
11860 previous: 'Forrige side',
11861 last: 'Sidste side'
11864 text: 'Vælg kolonner:'
11867 ascending: 'Sorter stigende',
11868 descending: 'Sorter faldende',
11869 none: 'Sorter ingen',
11870 remove: 'Fjern sortering'
11873 hide: 'Skjul kolonne'
11876 count: 'antal rækker: ',
11883 columns: 'Columns:',
11884 importerTitle: 'Import file',
11885 exporterAllAsCsv: 'Export all data as csv',
11886 exporterVisibleAsCsv: 'Export visible data as csv',
11887 exporterSelectedAsCsv: 'Export selected data as csv',
11888 exporterAllAsPdf: 'Export all data as pdf',
11889 exporterVisibleAsPdf: 'Export visible data as pdf',
11890 exporterSelectedAsPdf: 'Export selected data as pdf',
11891 clearAllFilters: 'Clear all filters'
11894 noHeaders: 'Column names were unable to be derived, does the file have a header?',
11895 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
11896 invalidCsv: 'File was unable to be processed, is it valid CSV?',
11897 invalidJson: 'File was unable to be processed, is it valid Json?',
11898 jsonNotArray: 'Imported json file must contain an array, aborting.'
11907 angular.module('ui.grid').config(['$provide', function ($provide) {
11908 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
11909 $delegate.add('de', {
11912 defaultFilterLabel: 'Filter für Spalte',
11913 removeFilter: 'Filter löschen',
11914 columnMenuButtonLabel: 'Spaltenmenü'
11916 priority: 'Priorität:',
11917 filterLabel: "Filter für Spalte: "
11923 description: 'Ziehen Sie eine Spaltenüberschrift hierhin, um nach dieser Spalte zu gruppieren.'
11926 placeholder: 'Suche...',
11927 showingItems: 'Zeige Einträge:',
11928 selectedItems: 'Ausgewählte Einträge:',
11929 totalItems: 'Einträge gesamt:',
11930 size: 'Einträge pro Seite:',
11931 first: 'Erste Seite',
11932 next: 'Nächste Seite',
11933 previous: 'Vorherige Seite',
11934 last: 'Letzte Seite'
11937 text: 'Spalten auswählen:'
11940 ascending: 'aufsteigend sortieren',
11941 descending: 'absteigend sortieren',
11942 none: 'keine Sortierung',
11943 remove: 'Sortierung zurücksetzen'
11946 hide: 'Spalte ausblenden'
11949 count: 'Zeilen insgesamt: ',
11951 avg: 'Durchschnitt: ',
11956 pinLeft: 'Links anheften',
11957 pinRight: 'Rechts anheften',
11965 buttonLabel: 'Tabellenmenü'
11967 columns: 'Spalten:',
11968 importerTitle: 'Datei importieren',
11969 exporterAllAsCsv: 'Alle Daten als CSV exportieren',
11970 exporterVisibleAsCsv: 'sichtbare Daten als CSV exportieren',
11971 exporterSelectedAsCsv: 'markierte Daten als CSV exportieren',
11972 exporterAllAsPdf: 'Alle Daten als PDF exportieren',
11973 exporterVisibleAsPdf: 'sichtbare Daten als PDF exportieren',
11974 exporterSelectedAsPdf: 'markierte Daten als CSV exportieren',
11975 clearAllFilters: 'Alle Filter zurücksetzen'
11978 noHeaders: 'Es konnten keine Spaltennamen ermittelt werden. Sind in der Datei Spaltendefinitionen enthalten?',
11979 noObjects: 'Es konnten keine Zeileninformationen gelesen werden, Sind in der Datei außer den Spaltendefinitionen auch Daten enthalten?',
11980 invalidCsv: 'Die Datei konnte nicht eingelesen werden, ist es eine gültige CSV-Datei?',
11981 invalidJson: 'Die Datei konnte nicht eingelesen werden. Enthält sie gültiges JSON?',
11982 jsonNotArray: 'Die importierte JSON-Datei muß ein Array enthalten. Breche Import ab.'
11986 pageToFirst: 'Zum Anfang',
11987 pageBack: 'Seite zurück',
11988 pageSelected: 'Ausgwählte Seite',
11989 pageForward: 'Seite vor',
11990 pageToLast: 'Zum Ende'
11992 sizes: 'Einträge pro Seite',
11993 totalItems: 'Einträge',
11998 group: 'Gruppieren',
11999 ungroup: 'Gruppierung aufheben',
12000 aggregate_count: 'Agg: Anzahl',
12001 aggregate_sum: 'Agg: Summe',
12002 aggregate_max: 'Agg: Maximum',
12003 aggregate_min: 'Agg: Minimum',
12004 aggregate_avg: 'Agg: Mittelwert',
12005 aggregate_remove: 'Aggregation entfernen'
12014 angular.module('ui.grid').config(['$provide', function($provide) {
12015 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12016 $delegate.add('en', {
12019 defaultFilterLabel: 'Filter for column',
12020 removeFilter: 'Remove Filter',
12021 columnMenuButtonLabel: 'Column Menu'
12023 priority: 'Priority:',
12024 filterLabel: "Filter for column: "
12030 description: 'Drag a column header here and drop it to group by that column.'
12033 placeholder: 'Search...',
12034 showingItems: 'Showing Items:',
12035 selectedItems: 'Selected Items:',
12036 totalItems: 'Total Items:',
12037 size: 'Page Size:',
12038 first: 'First Page',
12040 previous: 'Previous Page',
12044 text: 'Choose Columns:'
12047 ascending: 'Sort Ascending',
12048 descending: 'Sort Descending',
12050 remove: 'Remove Sort'
12053 hide: 'Hide Column'
12056 count: 'total rows: ',
12063 pinLeft: 'Pin Left',
12064 pinRight: 'Pin Right',
12072 buttonLabel: 'Grid Menu'
12074 columns: 'Columns:',
12075 importerTitle: 'Import file',
12076 exporterAllAsCsv: 'Export all data as csv',
12077 exporterVisibleAsCsv: 'Export visible data as csv',
12078 exporterSelectedAsCsv: 'Export selected data as csv',
12079 exporterAllAsPdf: 'Export all data as pdf',
12080 exporterVisibleAsPdf: 'Export visible data as pdf',
12081 exporterSelectedAsPdf: 'Export selected data as pdf',
12082 clearAllFilters: 'Clear all filters'
12085 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12086 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12087 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12088 invalidJson: 'File was unable to be processed, is it valid Json?',
12089 jsonNotArray: 'Imported json file must contain an array, aborting.'
12093 pageToFirst: 'Page to first',
12094 pageBack: 'Page back',
12095 pageSelected: 'Selected page',
12096 pageForward: 'Page forward',
12097 pageToLast: 'Page to last'
12099 sizes: 'items per page',
12100 totalItems: 'items',
12101 through: 'through',
12106 ungroup: 'Ungroup',
12107 aggregate_count: 'Agg: Count',
12108 aggregate_sum: 'Agg: Sum',
12109 aggregate_max: 'Agg: Max',
12110 aggregate_min: 'Agg: Min',
12111 aggregate_avg: 'Agg: Avg',
12112 aggregate_remove: 'Agg: Remove'
12116 minLength: 'Value should be at least THRESHOLD characters long.',
12117 maxLength: 'Value should be at most THRESHOLD characters long.',
12118 required: 'A value is needed.'
12127 angular.module('ui.grid').config(['$provide', function($provide) {
12128 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12129 $delegate.add('es', {
12134 description: 'Arrastre un encabezado de columna aquí y suéltelo para agrupar por esa columna.'
12137 placeholder: 'Buscar...',
12138 showingItems: 'Artículos Mostrados:',
12139 selectedItems: 'Artículos Seleccionados:',
12140 totalItems: 'Artículos Totales:',
12141 size: 'Tamaño de Página:',
12142 first: 'Primera Página',
12143 next: 'Página Siguiente',
12144 previous: 'Página Anterior',
12145 last: 'Última Página'
12148 text: 'Elegir columnas:'
12151 ascending: 'Orden Ascendente',
12152 descending: 'Orden Descendente',
12153 remove: 'Sin Ordenar'
12156 hide: 'Ocultar la columna'
12159 count: 'filas totales: ',
12166 pinLeft: 'Fijar a la Izquierda',
12167 pinRight: 'Fijar a la Derecha',
12168 unpin: 'Quitar Fijación'
12171 columns: 'Columnas:',
12172 importerTitle: 'Importar archivo',
12173 exporterAllAsCsv: 'Exportar todo como csv',
12174 exporterVisibleAsCsv: 'Exportar vista como csv',
12175 exporterSelectedAsCsv: 'Exportar selección como csv',
12176 exporterAllAsPdf: 'Exportar todo como pdf',
12177 exporterVisibleAsPdf: 'Exportar vista como pdf',
12178 exporterSelectedAsPdf: 'Exportar selección como pdf',
12179 clearAllFilters: 'Limpiar todos los filtros'
12182 noHeaders: 'No fue posible derivar los nombres de las columnas, ¿tiene encabezados el archivo?',
12183 noObjects: 'No fue posible obtener registros, ¿contiene datos el archivo, aparte de los encabezados?',
12184 invalidCsv: 'No fue posible procesar el archivo, ¿es un CSV válido?',
12185 invalidJson: 'No fue posible procesar el archivo, ¿es un Json válido?',
12186 jsonNotArray: 'El archivo json importado debe contener un array, abortando.'
12189 sizes: 'registros por página',
12190 totalItems: 'registros',
12195 ungroup: 'Desagrupar',
12196 aggregate_count: 'Agr: Cont',
12197 aggregate_sum: 'Agr: Sum',
12198 aggregate_max: 'Agr: Máx',
12199 aggregate_min: 'Agr: Min',
12200 aggregate_avg: 'Agr: Prom',
12201 aggregate_remove: 'Agr: Quitar'
12210 * Translated by: R. Salarmehr
12212 * Using Vajje.com online dictionary.
12215 angular.module('ui.grid').config(['$provide', function ($provide) {
12216 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12217 $delegate.add('fa', {
12222 description: 'عنوان یک ستون را بگیر و به گروهی از آن ستون رها کن.'
12225 placeholder: 'جستجو...',
12226 showingItems: 'نمایش اقلام:',
12227 selectedItems: 'قلم\u200cهای انتخاب شده:',
12228 totalItems: 'مجموع اقلام:',
12229 size: 'اندازه\u200cی صفحه:',
12230 first: 'اولین صفحه',
12231 next: 'صفحه\u200cی\u200cبعدی',
12232 previous: 'صفحه\u200cی\u200c قبلی',
12236 text: 'ستون\u200cهای انتخابی:'
12239 ascending: 'ترتیب صعودی',
12240 descending: 'ترتیب نزولی',
12241 remove: 'حذف مرتب کردن'
12244 hide: 'پنهان\u200cکردن ستون'
12254 pinLeft: 'پین کردن سمت چپ',
12255 pinRight: 'پین کردن سمت راست',
12259 columns: 'ستون\u200cها:',
12260 importerTitle: 'وارد کردن فایل',
12261 exporterAllAsCsv: 'خروجی تمام داده\u200cها در فایل csv',
12262 exporterVisibleAsCsv: 'خروجی داده\u200cهای قابل مشاهده در فایل csv',
12263 exporterSelectedAsCsv: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل csv',
12264 exporterAllAsPdf: 'خروجی تمام داده\u200cها در فایل pdf',
12265 exporterVisibleAsPdf: 'خروجی داده\u200cهای قابل مشاهده در فایل pdf',
12266 exporterSelectedAsPdf: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل pdf',
12267 clearAllFilters: 'پاک کردن تمام فیلتر'
12270 noHeaders: 'نام ستون قابل استخراج نیست. آیا فایل عنوان دارد؟',
12271 noObjects: 'اشیا قابل استخراج نیستند. آیا به جز عنوان\u200cها در فایل داده وجود دارد؟',
12272 invalidCsv: 'فایل قابل پردازش نیست. آیا فرمت csv معتبر است؟',
12273 invalidJson: 'فایل قابل پردازش نیست. آیا فرمت json معتبر است؟',
12274 jsonNotArray: 'فایل json وارد شده باید حاوی آرایه باشد. عملیات ساقط شد.'
12277 sizes: 'اقلام در هر صفحه',
12278 totalItems: 'اقلام',
12282 group: 'گروه\u200cبندی',
12283 ungroup: 'حذف گروه\u200cبندی',
12284 aggregate_count: 'Agg: تعداد',
12285 aggregate_sum: 'Agg: جمع',
12286 aggregate_max: 'Agg: بیشینه',
12287 aggregate_min: 'Agg: کمینه',
12288 aggregate_avg: 'Agg: میانگین',
12289 aggregate_remove: 'Agg: حذف'
12298 angular.module('ui.grid').config(['$provide', function($provide) {
12299 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12300 $delegate.add('fi', {
12305 description: 'Raahaa ja pudota otsikko tähän ryhmittääksesi sarakkeen mukaan.'
12308 placeholder: 'Hae...',
12309 showingItems: 'Näytetään rivejä:',
12310 selectedItems: 'Valitut rivit:',
12311 totalItems: 'Rivejä yht.:',
12313 first: 'Ensimmäinen sivu',
12314 next: 'Seuraava sivu',
12315 previous: 'Edellinen sivu',
12316 last: 'Viimeinen sivu'
12319 text: 'Valitse sarakkeet:'
12322 ascending: 'Järjestä nouseva',
12323 descending: 'Järjestä laskeva',
12324 remove: 'Poista järjestys'
12327 hide: 'Piilota sarake'
12330 count: 'Rivejä yht.: ',
12337 pinLeft: 'Lukitse vasemmalle',
12338 pinRight: 'Lukitse oikealle',
12339 unpin: 'Poista lukitus'
12342 columns: 'Sarakkeet:',
12343 importerTitle: 'Tuo tiedosto',
12344 exporterAllAsCsv: 'Vie tiedot csv-muodossa',
12345 exporterVisibleAsCsv: 'Vie näkyvä tieto csv-muodossa',
12346 exporterSelectedAsCsv: 'Vie valittu tieto csv-muodossa',
12347 exporterAllAsPdf: 'Vie tiedot pdf-muodossa',
12348 exporterVisibleAsPdf: 'Vie näkyvä tieto pdf-muodossa',
12349 exporterSelectedAsPdf: 'Vie valittu tieto pdf-muodossa',
12350 clearAllFilters: 'Puhdista kaikki suodattimet'
12353 noHeaders: 'Sarakkeen nimiä ei voitu päätellä, onko tiedostossa otsikkoriviä?',
12354 noObjects: 'Tietoja ei voitu lukea, onko tiedostossa muuta kuin otsikkot?',
12355 invalidCsv: 'Tiedostoa ei voitu käsitellä, oliko se CSV-muodossa?',
12356 invalidJson: 'Tiedostoa ei voitu käsitellä, oliko se JSON-muodossa?',
12357 jsonNotArray: 'Tiedosto ei sisältänyt taulukkoa, lopetetaan.'
12366 angular.module('ui.grid').config(['$provide', function($provide) {
12367 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12368 $delegate.add('fr', {
12373 description: 'Faites glisser une en-tête de colonne ici pour créer un groupe de colonnes.'
12376 placeholder: 'Recherche...',
12377 showingItems: 'Affichage des éléments :',
12378 selectedItems: 'Éléments sélectionnés :',
12379 totalItems: 'Nombre total d\'éléments:',
12380 size: 'Taille de page:',
12381 first: 'Première page',
12382 next: 'Page Suivante',
12383 previous: 'Page précédente',
12384 last: 'Dernière page'
12387 text: 'Choisir des colonnes :'
12390 ascending: 'Trier par ordre croissant',
12391 descending: 'Trier par ordre décroissant',
12392 remove: 'Enlever le tri'
12395 hide: 'Cacher la colonne'
12398 count: 'lignes totales: ',
12405 pinLeft: 'Épingler à gauche',
12406 pinRight: 'Épingler à droite',
12410 columns: 'Colonnes:',
12411 importerTitle: 'Importer un fichier',
12412 exporterAllAsCsv: 'Exporter toutes les données en CSV',
12413 exporterVisibleAsCsv: 'Exporter les données visibles en CSV',
12414 exporterSelectedAsCsv: 'Exporter les données sélectionnées en CSV',
12415 exporterAllAsPdf: 'Exporter toutes les données en PDF',
12416 exporterVisibleAsPdf: 'Exporter les données visibles en PDF',
12417 exporterSelectedAsPdf: 'Exporter les données sélectionnées en PDF',
12418 clearAllFilters: 'Nettoyez tous les filtres'
12421 noHeaders: 'Impossible de déterminer le nom des colonnes, le fichier possède-t-il une en-tête ?',
12422 noObjects: 'Aucun objet trouvé, le fichier possède-t-il des données autres que l\'en-tête ?',
12423 invalidCsv: 'Le fichier n\'a pas pu être traité, le CSV est-il valide ?',
12424 invalidJson: 'Le fichier n\'a pas pu être traité, le JSON est-il valide ?',
12425 jsonNotArray: 'Le fichier JSON importé doit contenir un tableau, abandon.'
12428 sizes: 'éléments par page',
12429 totalItems: 'éléments',
12434 ungroup: 'Dégrouper',
12435 aggregate_count: 'Agg: Compte',
12436 aggregate_sum: 'Agg: Somme',
12437 aggregate_max: 'Agg: Max',
12438 aggregate_min: 'Agg: Min',
12439 aggregate_avg: 'Agg: Moy',
12440 aggregate_remove: 'Agg: Retirer'
12449 angular.module('ui.grid').config(['$provide', function ($provide) {
12450 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12451 $delegate.add('he', {
12456 description: 'גרור עמודה לכאן ושחרר בכדי לקבץ עמודה זו.'
12459 placeholder: 'חפש...',
12460 showingItems: 'מציג:',
12461 selectedItems: 'סה"כ נבחרו:',
12462 totalItems: 'סה"כ רשומות:',
12463 size: 'תוצאות בדף:',
12466 previous: 'דף קודם',
12470 text: 'בחר עמודות:'
12473 ascending: 'סדר עולה',
12474 descending: 'סדר יורד',
12481 count: 'total rows: ',
12488 columns: 'Columns:',
12489 importerTitle: 'Import file',
12490 exporterAllAsCsv: 'Export all data as csv',
12491 exporterVisibleAsCsv: 'Export visible data as csv',
12492 exporterSelectedAsCsv: 'Export selected data as csv',
12493 exporterAllAsPdf: 'Export all data as pdf',
12494 exporterVisibleAsPdf: 'Export visible data as pdf',
12495 exporterSelectedAsPdf: 'Export selected data as pdf',
12496 clearAllFilters: 'Clean all filters'
12499 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12500 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12501 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12502 invalidJson: 'File was unable to be processed, is it valid Json?',
12503 jsonNotArray: 'Imported json file must contain an array, aborting.'
12512 angular.module('ui.grid').config(['$provide', function($provide) {
12513 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12514 $delegate.add('hy', {
12519 description: 'Ըստ սյան խմբավորելու համար քաշեք և գցեք վերնագիրն այստեղ։'
12522 placeholder: 'Փնտրում...',
12523 showingItems: 'Ցուցադրված տվյալներ՝',
12524 selectedItems: 'Ընտրված:',
12525 totalItems: 'Ընդամենը՝',
12526 size: 'Տողերի քանակը էջում՝',
12527 first: 'Առաջին էջ',
12529 previous: 'Նախորդ էջ',
12533 text: 'Ընտրել սյուները:'
12536 ascending: 'Աճման կարգով',
12537 descending: 'Նվազման կարգով',
12541 hide: 'Թաքցնել սյունը'
12544 count: 'ընդամենը տող՝ ',
12551 pinLeft: 'Կպցնել ձախ կողմում',
12552 pinRight: 'Կպցնել աջ կողմում',
12556 columns: 'Սյուներ:',
12557 importerTitle: 'Ներմուծել ֆայլ',
12558 exporterAllAsCsv: 'Արտահանել ամբողջը CSV',
12559 exporterVisibleAsCsv: 'Արտահանել երևացող տվյալները CSV',
12560 exporterSelectedAsCsv: 'Արտահանել ընտրված տվյալները CSV',
12561 exporterAllAsPdf: 'Արտահանել PDF',
12562 exporterVisibleAsPdf: 'Արտահանել երևացող տվյալները PDF',
12563 exporterSelectedAsPdf: 'Արտահանել ընտրված տվյալները PDF',
12564 clearAllFilters: 'Մաքրել բոլոր ֆիլտրերը'
12567 noHeaders: 'Հնարավոր չեղավ որոշել սյան վերնագրերը։ Արդյո՞ք ֆայլը ունի վերնագրեր։',
12568 noObjects: 'Հնարավոր չեղավ կարդալ տվյալները։ Արդյո՞ք ֆայլում կան տվյալներ։',
12569 invalidCsv: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր CSV է։',
12570 invalidJson: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր Json է։',
12571 jsonNotArray: 'Ներմուծված json ֆայլը պետք է պարունակի զանգված, կասեցվում է։'
12580 angular.module('ui.grid').config(['$provide', function($provide) {
12581 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12582 $delegate.add('it', {
12587 description: 'Trascina un\'intestazione all\'interno del gruppo della colonna.'
12590 placeholder: 'Ricerca...',
12591 showingItems: 'Mostra:',
12592 selectedItems: 'Selezionati:',
12593 totalItems: 'Totali:',
12594 size: 'Tot Pagine:',
12597 previous: 'Precedente',
12601 text: 'Scegli le colonne:'
12605 descending: 'Desc.',
12606 remove: 'Annulla ordinamento'
12612 count: 'righe totali: ',
12619 pinLeft: 'Blocca a sx',
12620 pinRight: 'Blocca a dx',
12621 unpin: 'Blocca in alto'
12624 columns: 'Colonne:',
12625 importerTitle: 'Importa',
12626 exporterAllAsCsv: 'Esporta tutti i dati in CSV',
12627 exporterVisibleAsCsv: 'Esporta i dati visibili in CSV',
12628 exporterSelectedAsCsv: 'Esporta i dati selezionati in CSV',
12629 exporterAllAsPdf: 'Esporta tutti i dati in PDF',
12630 exporterVisibleAsPdf: 'Esporta i dati visibili in PDF',
12631 exporterSelectedAsPdf: 'Esporta i dati selezionati in PDF',
12632 clearAllFilters: 'Pulire tutti i filtri'
12635 noHeaders: 'Impossibile reperire i nomi delle colonne, sicuro che siano indicati all\'interno del file?',
12636 noObjects: 'Impossibile reperire gli oggetti, sicuro che siano indicati all\'interno del file?',
12637 invalidCsv: 'Impossibile elaborare il file, sicuro che sia un CSV?',
12638 invalidJson: 'Impossibile elaborare il file, sicuro che sia un JSON valido?',
12639 jsonNotArray: 'Errore! Il file JSON da importare deve contenere un array.'
12642 group: 'Raggruppa',
12644 aggregate_count: 'Agg: N. Elem.',
12645 aggregate_sum: 'Agg: Somma',
12646 aggregate_max: 'Agg: Massimo',
12647 aggregate_min: 'Agg: Minimo',
12648 aggregate_avg: 'Agg: Media',
12649 aggregate_remove: 'Agg: Rimuovi'
12653 minLength: 'Lunghezza minima pari a THRESHOLD caratteri.',
12654 maxLength: 'Lunghezza massima pari a THRESHOLD caratteri.',
12655 required: 'Necessario inserire un valore.'
12664 angular.module('ui.grid').config(['$provide', function($provide) {
12665 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12666 $delegate.add('ja', {
12671 description: 'ここに列ヘッダをドラッグアンドドロップして、その列でグループ化します。'
12674 placeholder: '検索...',
12675 showingItems: '表示中の項目:',
12676 selectedItems: '選択した項目:',
12677 totalItems: '項目の総数:',
12688 ascending: '昇順に並べ替え',
12689 descending: '降順に並べ替え',
12709 importerTitle: 'ファイルのインポート',
12710 exporterAllAsCsv: 'すべてのデータをCSV形式でエクスポート',
12711 exporterVisibleAsCsv: '表示中のデータをCSV形式でエクスポート',
12712 exporterSelectedAsCsv: '選択したデータをCSV形式でエクスポート',
12713 exporterAllAsPdf: 'すべてのデータをPDF形式でエクスポート',
12714 exporterVisibleAsPdf: '表示中のデータをPDF形式でエクスポート',
12715 exporterSelectedAsPdf: '選択したデータをPDF形式でエクスポート',
12716 clearAllFilters: 'すべてのフィルタを清掃してください'
12719 noHeaders: '列名を取得できません。ファイルにヘッダが含まれていることを確認してください。',
12720 noObjects: 'オブジェクトを取得できません。ファイルにヘッダ以外のデータが含まれていることを確認してください。',
12721 invalidCsv: 'ファイルを処理できません。ファイルが有効なCSV形式であることを確認してください。',
12722 invalidJson: 'ファイルを処理できません。ファイルが有効なJSON形式であることを確認してください。',
12723 jsonNotArray: 'インポートしたJSONファイルには配列が含まれている必要があります。処理を中止します。'
12727 pageToFirst: '最初のページ',
12729 pageSelected: '現在のページ',
12730 pageForward: '次のページ',
12731 pageToLast: '最後のページ'
12745 angular.module('ui.grid').config(['$provide', function($provide) {
12746 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12747 $delegate.add('ko', {
12752 description: '컬럼으로 그룹핑하기 위해서는 컬럼 헤더를 끌어 떨어뜨려 주세요.'
12755 placeholder: '검색...',
12756 showingItems: '항목 보여주기:',
12757 selectedItems: '선택 항목:',
12758 totalItems: '전체 항목:',
12762 previous: '이전 페이지',
12769 ascending: '오름차순 정렬',
12770 descending: '내림차순 정렬',
12790 importerTitle: '파일 가져오기',
12791 exporterAllAsCsv: 'csv로 모든 데이터 내보내기',
12792 exporterVisibleAsCsv: 'csv로 보이는 데이터 내보내기',
12793 exporterSelectedAsCsv: 'csv로 선택된 데이터 내보내기',
12794 exporterAllAsPdf: 'pdf로 모든 데이터 내보내기',
12795 exporterVisibleAsPdf: 'pdf로 보이는 데이터 내보내기',
12796 exporterSelectedAsPdf: 'pdf로 선택 데이터 내보내기',
12797 clearAllFilters: '모든 필터를 청소'
12800 noHeaders: '컬럼명이 지정되어 있지 않습니다. 파일에 헤더가 명시되어 있는지 확인해 주세요.',
12801 noObjects: '데이터가 지정되어 있지 않습니다. 데이터가 파일에 있는지 확인해 주세요.',
12802 invalidCsv: '파일을 처리할 수 없습니다. 올바른 csv인지 확인해 주세요.',
12803 invalidJson: '파일을 처리할 수 없습니다. 올바른 json인지 확인해 주세요.',
12804 jsonNotArray: 'json 파일은 배열을 포함해야 합니다.'
12808 totalItems: '전체 항목'
12817 angular.module('ui.grid').config(['$provide', function($provide) {
12818 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12819 $delegate.add('nl', {
12824 description: 'Sleep hier een kolomnaam heen om op te groeperen.'
12827 placeholder: 'Zoeken...',
12828 showingItems: 'Getoonde items:',
12829 selectedItems: 'Geselecteerde items:',
12830 totalItems: 'Totaal aantal items:',
12831 size: 'Items per pagina:',
12832 first: 'Eerste pagina',
12833 next: 'Volgende pagina',
12834 previous: 'Vorige pagina',
12835 last: 'Laatste pagina'
12838 text: 'Kies kolommen:'
12841 ascending: 'Sorteer oplopend',
12842 descending: 'Sorteer aflopend',
12843 remove: 'Verwijder sortering'
12846 hide: 'Verberg kolom'
12849 count: 'Aantal rijen: ',
12851 avg: 'Gemiddelde: ',
12856 pinLeft: 'Zet links vast',
12857 pinRight: 'Zet rechts vast',
12861 columns: 'Kolommen:',
12862 importerTitle: 'Importeer bestand',
12863 exporterAllAsCsv: 'Exporteer alle data als csv',
12864 exporterVisibleAsCsv: 'Exporteer zichtbare data als csv',
12865 exporterSelectedAsCsv: 'Exporteer geselecteerde data als csv',
12866 exporterAllAsPdf: 'Exporteer alle data als pdf',
12867 exporterVisibleAsPdf: 'Exporteer zichtbare data als pdf',
12868 exporterSelectedAsPdf: 'Exporteer geselecteerde data als pdf',
12869 clearAllFilters: 'Reinig alle filters'
12872 noHeaders: 'Kolomnamen kunnen niet worden afgeleid. Heeft het bestand een header?',
12873 noObjects: 'Objecten kunnen niet worden afgeleid. Bevat het bestand data naast de headers?',
12874 invalidCsv: 'Het bestand kan niet verwerkt worden. Is het een valide csv bestand?',
12875 invalidJson: 'Het bestand kan niet verwerkt worden. Is het valide json?',
12876 jsonNotArray: 'Het json bestand moet een array bevatten. De actie wordt geannuleerd.'
12879 sizes: 'items per pagina',
12880 totalItems: 'items',
12885 ungroup: 'Groepering opheffen',
12886 aggregate_count: 'Agg: Aantal',
12887 aggregate_sum: 'Agg: Som',
12888 aggregate_max: 'Agg: Max',
12889 aggregate_min: 'Agg: Min',
12890 aggregate_avg: 'Agg: Gem',
12891 aggregate_remove: 'Agg: Verwijder'
12900 angular.module('ui.grid').config(['$provide', function($provide) {
12901 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12902 $delegate.add('pl', {
12905 defaultFilterLabel: 'Filter dla kolumny',
12906 removeFilter: 'Usuń filter',
12907 columnMenuButtonLabel: 'Menu kolumny'
12909 priority: 'Prioritet:',
12910 filterLabel: "Filtr dla kolumny: "
12916 description: 'Przeciągnij nagłówek kolumny tutaj, aby pogrupować według niej.'
12919 placeholder: 'Szukaj...',
12920 showingItems: 'Widoczne pozycje:',
12921 selectedItems: 'Zaznaczone pozycje:',
12922 totalItems: 'Wszystkich pozycji:',
12923 size: 'Rozmiar strony:',
12924 first: 'Pierwsza strona',
12925 next: 'Następna strona',
12926 previous: 'Poprzednia strona',
12927 last: 'Ostatnia strona'
12930 text: 'Wybierz kolumny:'
12933 ascending: 'Sortuj rosnąco',
12934 descending: 'Sortuj malejąco',
12935 none: 'Brak sortowania',
12936 remove: 'Wyłącz sortowanie'
12939 hide: 'Ukryj kolumne'
12942 count: 'Razem pozycji: ',
12949 pinLeft: 'Przypnij do lewej',
12950 pinRight: 'Przypnij do prawej',
12958 buttonLabel: 'Menu Grida'
12960 columns: 'Kolumny:',
12961 importerTitle: 'Importuj plik',
12962 exporterAllAsCsv: 'Eksportuj wszystkie dane do csv',
12963 exporterVisibleAsCsv: 'Eksportuj widoczne dane do csv',
12964 exporterSelectedAsCsv: 'Eksportuj zaznaczone dane do csv',
12965 exporterAllAsPdf: 'Eksportuj wszystkie dane do pdf',
12966 exporterVisibleAsPdf: 'Eksportuj widoczne dane do pdf',
12967 exporterSelectedAsPdf: 'Eksportuj zaznaczone dane do pdf',
12968 clearAllFilters: 'Wyczyść filtry'
12971 noHeaders: 'Nie udało się wczytać nazw kolumn. Czy plik posiada nagłówek?',
12972 noObjects: 'Nie udalo się wczytać pozycji. Czy plik zawiera dane??',
12973 invalidCsv: 'Nie udało się przetworzyć pliku, jest to prawidlowy plik CSV??',
12974 invalidJson: 'Nie udało się przetworzyć pliku, jest to prawidlowy plik Json?',
12975 jsonNotArray: 'Importowany plik json musi zawierać tablicę, importowanie przerwane.'
12979 pageToFirst: 'Pierwsza strona',
12980 pageBack: 'Poprzednia strona',
12981 pageSelected: 'Wybrana strona',
12982 pageForward: 'Następna strona',
12983 pageToLast: 'Ostatnia strona'
12985 sizes: 'pozycji na stronę',
12986 totalItems: 'pozycji',
12992 ungroup: 'Rozgrupuj',
12993 aggregate_count: 'Zbiorczo: Razem',
12994 aggregate_sum: 'Zbiorczo: Suma',
12995 aggregate_max: 'Zbiorczo: Max',
12996 aggregate_min: 'Zbiorczo: Min',
12997 aggregate_avg: 'Zbiorczo: Średnia',
12998 aggregate_remove: 'Zbiorczo: Usuń'
13007 angular.module('ui.grid').config(['$provide', function($provide) {
13008 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13009 $delegate.add('pt-br', {
13012 defaultFilterLabel: 'Filtro por coluna',
13013 removeFilter: 'Remover filtro',
13014 columnMenuButtonLabel: 'Menu coluna'
13016 priority: 'Prioridade:',
13017 filterLabel: "Filtro por coluna: "
13023 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
13026 placeholder: 'Procurar...',
13027 showingItems: 'Mostrando os Itens:',
13028 selectedItems: 'Items Selecionados:',
13029 totalItems: 'Total de Itens:',
13030 size: 'Tamanho da Página:',
13031 first: 'Primeira Página',
13032 next: 'Próxima Página',
13033 previous: 'Página Anterior',
13034 last: 'Última Página'
13037 text: 'Selecione as colunas:'
13040 ascending: 'Ordenar Ascendente',
13041 descending: 'Ordenar Descendente',
13042 none: 'Nenhuma Ordem',
13043 remove: 'Remover Ordenação'
13046 hide: 'Esconder coluna'
13049 count: 'total de linhas: ',
13056 pinLeft: 'Fixar Esquerda',
13057 pinRight: 'Fixar Direita',
13058 unpin: 'Desprender'
13065 buttonLabel: 'Menu Grid'
13067 columns: 'Colunas:',
13068 importerTitle: 'Importar arquivo',
13069 exporterAllAsCsv: 'Exportar todos os dados como csv',
13070 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
13071 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
13072 exporterAllAsPdf: 'Exportar todos os dados como pdf',
13073 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
13074 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
13075 clearAllFilters: 'Limpar todos os filtros'
13078 noHeaders: 'Nomes de colunas não puderam ser derivados. O arquivo tem um cabeçalho?',
13079 noObjects: 'Objetos não puderam ser derivados. Havia dados no arquivo, além dos cabeçalhos?',
13080 invalidCsv: 'Arquivo não pode ser processado. É um CSV válido?',
13081 invalidJson: 'Arquivo não pode ser processado. É um Json válido?',
13082 jsonNotArray: 'Arquivo json importado tem que conter um array. Abortando.'
13086 pageToFirst: 'Primeira página',
13087 pageBack: 'Página anterior',
13088 pageSelected: 'Página Selecionada',
13089 pageForward: 'Proxima',
13090 pageToLast: 'Anterior'
13092 sizes: 'itens por página',
13093 totalItems: 'itens',
13094 through: 'através dos',
13099 ungroup: 'Desagrupar',
13100 aggregate_count: 'Agr: Contar',
13101 aggregate_sum: 'Agr: Soma',
13102 aggregate_max: 'Agr: Max',
13103 aggregate_min: 'Agr: Min',
13104 aggregate_avg: 'Agr: Med',
13105 aggregate_remove: 'Agr: Remover'
13114 angular.module('ui.grid').config(['$provide', function($provide) {
13115 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13116 $delegate.add('pt', {
13119 defaultFilterLabel: 'Filtro por coluna',
13120 removeFilter: 'Remover filtro',
13121 columnMenuButtonLabel: 'Menu coluna'
13123 priority: 'Prioridade:',
13124 filterLabel: "Filtro por coluna: "
13130 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
13133 placeholder: 'Procurar...',
13134 showingItems: 'Mostrando os Itens:',
13135 selectedItems: 'Itens Selecionados:',
13136 totalItems: 'Total de Itens:',
13137 size: 'Tamanho da Página:',
13138 first: 'Primeira Página',
13139 next: 'Próxima Página',
13140 previous: 'Página Anterior',
13141 last: 'Última Página'
13144 text: 'Selecione as colunas:'
13147 ascending: 'Ordenar Ascendente',
13148 descending: 'Ordenar Descendente',
13149 none: 'Nenhuma Ordem',
13150 remove: 'Remover Ordenação'
13153 hide: 'Esconder coluna'
13156 count: 'total de linhas: ',
13163 pinLeft: 'Fixar Esquerda',
13164 pinRight: 'Fixar Direita',
13165 unpin: 'Desprender'
13172 buttonLabel: 'Menu Grid'
13174 columns: 'Colunas:',
13175 importerTitle: 'Importar ficheiro',
13176 exporterAllAsCsv: 'Exportar todos os dados como csv',
13177 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
13178 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
13179 exporterAllAsPdf: 'Exportar todos os dados como pdf',
13180 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
13181 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
13182 clearAllFilters: 'Limpar todos os filtros'
13185 noHeaders: 'Nomes de colunas não puderam ser derivados. O ficheiro tem um cabeçalho?',
13186 noObjects: 'Objetos não puderam ser derivados. Havia dados no ficheiro, além dos cabeçalhos?',
13187 invalidCsv: 'Ficheiro não pode ser processado. É um CSV válido?',
13188 invalidJson: 'Ficheiro não pode ser processado. É um Json válido?',
13189 jsonNotArray: 'Ficheiro json importado tem que conter um array. Interrompendo.'
13193 pageToFirst: 'Primeira página',
13194 pageBack: 'Página anterior',
13195 pageSelected: 'Página Selecionada',
13196 pageForward: 'Próxima',
13197 pageToLast: 'Anterior'
13199 sizes: 'itens por página',
13200 totalItems: 'itens',
13201 through: 'através dos',
13206 ungroup: 'Desagrupar',
13207 aggregate_count: 'Agr: Contar',
13208 aggregate_sum: 'Agr: Soma',
13209 aggregate_max: 'Agr: Max',
13210 aggregate_min: 'Agr: Min',
13211 aggregate_avg: 'Agr: Med',
13212 aggregate_remove: 'Agr: Remover'
13221 angular.module('ui.grid').config(['$provide', function($provide) {
13222 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13223 $delegate.add('ro', {
13226 defaultFilterLabel: 'Filtru pentru coloana',
13227 removeFilter: 'Sterge filtru',
13228 columnMenuButtonLabel: 'Column Menu'
13230 priority: 'Prioritate:',
13231 filterLabel: "Filtru pentru coloana:"
13237 description: 'Trage un cap de coloana aici pentru a grupa elementele dupa coloana respectiva'
13240 placeholder: 'Cauta...',
13241 showingItems: 'Arata elementele:',
13242 selectedItems: 'Elementele selectate:',
13243 totalItems: 'Total elemente:',
13244 size: 'Marime pagina:',
13245 first: 'Prima pagina',
13246 next: 'Pagina urmatoare',
13247 previous: 'Pagina anterioara',
13248 last: 'Ultima pagina'
13251 text: 'Alege coloane:'
13254 ascending: 'Ordoneaza crescator',
13255 descending: 'Ordoneaza descrescator',
13256 none: 'Fara ordonare',
13257 remove: 'Sterge ordonarea'
13260 hide: 'Ascunde coloana'
13263 count: 'total linii: ',
13270 pinLeft: 'Pin la stanga',
13271 pinRight: 'Pin la dreapta',
13272 unpin: 'Sterge pinul'
13279 buttonLabel: 'Grid Menu'
13281 columns: 'Coloane:',
13282 importerTitle: 'Incarca fisier',
13283 exporterAllAsCsv: 'Exporta toate datele ca csv',
13284 exporterVisibleAsCsv: 'Exporta datele vizibile ca csv',
13285 exporterSelectedAsCsv: 'Exporta datele selectate ca csv',
13286 exporterAllAsPdf: 'Exporta toate datele ca pdf',
13287 exporterVisibleAsPdf: 'Exporta datele vizibile ca pdf',
13288 exporterSelectedAsPdf: 'Exporta datele selectate ca csv pdf',
13289 clearAllFilters: 'Sterge toate filtrele'
13292 noHeaders: 'Numele coloanelor nu a putut fi incarcat, acest fisier are un header?',
13293 noObjects: 'Datele nu au putut fi incarcate, exista date in fisier in afara numelor de coloane?',
13294 invalidCsv: 'Fisierul nu a putut fi procesat, ati incarcat un CSV valid ?',
13295 invalidJson: 'Fisierul nu a putut fi procesat, ati incarcat un Json valid?',
13296 jsonNotArray: 'Json-ul incarcat trebuie sa contina un array, inchidere.'
13300 pageToFirst: 'Prima pagina',
13301 pageBack: 'O pagina inapoi',
13302 pageSelected: 'Pagina selectata',
13303 pageForward: 'O pagina inainte',
13304 pageToLast: 'Ultima pagina'
13306 sizes: 'Elemente per pagina',
13307 totalItems: 'elemente',
13313 ungroup: 'Opreste gruparea',
13314 aggregate_count: 'Agg: Count',
13315 aggregate_sum: 'Agg: Sum',
13316 aggregate_max: 'Agg: Max',
13317 aggregate_min: 'Agg: Min',
13318 aggregate_avg: 'Agg: Avg',
13319 aggregate_remove: 'Agg: Remove'
13328 angular.module('ui.grid').config(['$provide', function($provide) {
13329 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13330 $delegate.add('ru', {
13333 defaultFilterLabel: 'Фильтр столбца',
13334 removeFilter: 'Удалить фильтр',
13335 columnMenuButtonLabel: 'Меню столбца'
13337 priority: 'Приоритет:',
13338 filterLabel: "Фильтр столбца: "
13344 description: 'Для группировки по столбцу перетащите сюда его название.'
13347 placeholder: 'Поиск...',
13348 showingItems: 'Показать элементы:',
13349 selectedItems: 'Выбранные элементы:',
13350 totalItems: 'Всего элементов:',
13351 size: 'Размер страницы:',
13352 first: 'Первая страница',
13353 next: 'Следующая страница',
13354 previous: 'Предыдущая страница',
13355 last: 'Последняя страница'
13358 text: 'Выбрать столбцы:'
13361 ascending: 'По возрастанию',
13362 descending: 'По убыванию',
13363 none: 'Без сортировки',
13364 remove: 'Убрать сортировку'
13367 hide: 'Спрятать столбец'
13370 count: 'всего строк: ',
13377 pinLeft: 'Закрепить слева',
13378 pinRight: 'Закрепить справа',
13386 buttonLabel: 'Меню'
13388 columns: 'Столбцы:',
13389 importerTitle: 'Импортировать файл',
13390 exporterAllAsCsv: 'Экспортировать всё в CSV',
13391 exporterVisibleAsCsv: 'Экспортировать видимые данные в CSV',
13392 exporterSelectedAsCsv: 'Экспортировать выбранные данные в CSV',
13393 exporterAllAsPdf: 'Экспортировать всё в PDF',
13394 exporterVisibleAsPdf: 'Экспортировать видимые данные в PDF',
13395 exporterSelectedAsPdf: 'Экспортировать выбранные данные в PDF',
13396 clearAllFilters: 'Очистите все фильтры'
13399 noHeaders: 'Не удалось получить названия столбцов, есть ли в файле заголовок?',
13400 noObjects: 'Не удалось получить данные, есть ли в файле строки кроме заголовка?',
13401 invalidCsv: 'Не удалось обработать файл, это правильный CSV-файл?',
13402 invalidJson: 'Не удалось обработать файл, это правильный JSON?',
13403 jsonNotArray: 'Импортируемый JSON-файл должен содержать массив, операция отменена.'
13407 pageToFirst: 'Первая страница',
13408 pageBack: 'Предыдущая страница',
13409 pageSelected: 'Выбранная страница',
13410 pageForward: 'Следующая страница',
13411 pageToLast: 'Последняя страница'
13413 sizes: 'строк на страницу',
13414 totalItems: 'строк',
13419 group: 'Группировать',
13420 ungroup: 'Разгруппировать',
13421 aggregate_count: 'Группировать: Count',
13422 aggregate_sum: 'Для группы: Сумма',
13423 aggregate_max: 'Для группы: Максимум',
13424 aggregate_min: 'Для группы: Минимум',
13425 aggregate_avg: 'Для группы: Среднее',
13426 aggregate_remove: 'Для группы: Пусто'
13435 angular.module('ui.grid').config(['$provide', function($provide) {
13436 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13437 $delegate.add('sk', {
13442 description: 'Pretiahni sem názov stĺpca pre zoskupenie podľa toho stĺpca.'
13445 placeholder: 'Hľadaj...',
13446 showingItems: 'Zobrazujem položky:',
13447 selectedItems: 'Vybraté položky:',
13448 totalItems: 'Počet položiek:',
13450 first: 'Prvá strana',
13451 next: 'Ďalšia strana',
13452 previous: 'Predchádzajúca strana',
13453 last: 'Posledná strana'
13456 text: 'Vyberte stĺpce:'
13459 ascending: 'Zotriediť vzostupne',
13460 descending: 'Zotriediť zostupne',
13461 remove: 'Vymazať triedenie'
13464 count: 'total rows: ',
13471 columns: 'Columns:',
13472 importerTitle: 'Import file',
13473 exporterAllAsCsv: 'Export all data as csv',
13474 exporterVisibleAsCsv: 'Export visible data as csv',
13475 exporterSelectedAsCsv: 'Export selected data as csv',
13476 exporterAllAsPdf: 'Export all data as pdf',
13477 exporterVisibleAsPdf: 'Export visible data as pdf',
13478 exporterSelectedAsPdf: 'Export selected data as pdf',
13479 clearAllFilters: 'Clear all filters'
13482 noHeaders: 'Column names were unable to be derived, does the file have a header?',
13483 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
13484 invalidCsv: 'File was unable to be processed, is it valid CSV?',
13485 invalidJson: 'File was unable to be processed, is it valid Json?',
13486 jsonNotArray: 'Imported json file must contain an array, aborting.'
13495 angular.module('ui.grid').config(['$provide', function($provide) {
13496 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13497 $delegate.add('sv', {
13502 description: 'Dra en kolumnrubrik hit och släpp den för att gruppera efter den kolumnen.'
13505 placeholder: 'Sök...',
13506 showingItems: 'Visar artiklar:',
13507 selectedItems: 'Valda artiklar:',
13508 totalItems: 'Antal artiklar:',
13509 size: 'Sidstorlek:',
13510 first: 'Första sidan',
13511 next: 'Nästa sida',
13512 previous: 'Föregående sida',
13513 last: 'Sista sidan'
13516 text: 'Välj kolumner:'
13519 ascending: 'Sortera stigande',
13520 descending: 'Sortera fallande',
13521 remove: 'Inaktivera sortering'
13527 count: 'Antal rader: ',
13529 avg: 'Genomsnitt: ',
13534 pinLeft: 'Fäst vänster',
13535 pinRight: 'Fäst höger',
13539 columns: 'Kolumner:',
13540 importerTitle: 'Importera fil',
13541 exporterAllAsCsv: 'Exportera all data som CSV',
13542 exporterVisibleAsCsv: 'Exportera synlig data som CSV',
13543 exporterSelectedAsCsv: 'Exportera markerad data som CSV',
13544 exporterAllAsPdf: 'Exportera all data som PDF',
13545 exporterVisibleAsPdf: 'Exportera synlig data som PDF',
13546 exporterSelectedAsPdf: 'Exportera markerad data som PDF',
13547 clearAllFilters: 'Rengör alla filter'
13550 noHeaders: 'Kolumnnamn kunde inte härledas. Har filen ett sidhuvud?',
13551 noObjects: 'Objekt kunde inte härledas. Har filen data undantaget sidhuvud?',
13552 invalidCsv: 'Filen kunde inte behandlas, är den en giltig CSV?',
13553 invalidJson: 'Filen kunde inte behandlas, är den en giltig JSON?',
13554 jsonNotArray: 'Importerad JSON-fil måste innehålla ett fält. Import avbruten.'
13557 sizes: 'Artiklar per sida',
13558 totalItems: 'Artiklar'
13567 angular.module('ui.grid').config(['$provide', function($provide) {
13568 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13569 $delegate.add('ta', {
13571 label: 'உருப்படிகள்'
13574 description: 'ஒரு பத்தியை குழுவாக அமைக்க அப்பத்தியின் தலைப்பை இங்கே இழுத்து வரவும் '
13577 placeholder: 'தேடல் ...',
13578 showingItems: 'உருப்படிகளை காண்பித்தல்:',
13579 selectedItems: 'தேர்ந்தெடுக்கப்பட்ட உருப்படிகள்:',
13580 totalItems: 'மொத்த உருப்படிகள்:',
13581 size: 'பக்க அளவு: ',
13582 first: 'முதல் பக்கம்',
13583 next: 'அடுத்த பக்கம்',
13584 previous: 'முந்தைய பக்கம் ',
13585 last: 'இறுதி பக்கம்'
13588 text: 'பத்திகளை தேர்ந்தெடு:'
13591 ascending: 'மேலிருந்து கீழாக',
13592 descending: 'கீழிருந்து மேலாக',
13593 remove: 'வரிசையை நீக்கு'
13596 hide: 'பத்தியை மறைத்து வை '
13599 count: 'மொத்த வரிகள்:',
13602 min: 'குறைந்தபட்ச: ',
13606 pinLeft: 'இடதுபுறமாக தைக்க ',
13607 pinRight: 'வலதுபுறமாக தைக்க',
13611 columns: 'பத்திகள்:',
13612 importerTitle: 'கோப்பு : படித்தல்',
13613 exporterAllAsCsv: 'எல்லா தரவுகளையும் கோப்பாக்கு: csv',
13614 exporterVisibleAsCsv: 'இருக்கும் தரவுகளை கோப்பாக்கு: csv',
13615 exporterSelectedAsCsv: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: csv',
13616 exporterAllAsPdf: 'எல்லா தரவுகளையும் கோப்பாக்கு: pdf',
13617 exporterVisibleAsPdf: 'இருக்கும் தரவுகளை கோப்பாக்கு: pdf',
13618 exporterSelectedAsPdf: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: pdf',
13619 clearAllFilters: 'Clear all filters'
13622 noHeaders: 'பத்தியின் தலைப்புகளை பெற இயலவில்லை, கோப்பிற்கு தலைப்பு உள்ளதா?',
13623 noObjects: 'இலக்குகளை உருவாக்க முடியவில்லை, கோப்பில் தலைப்புகளை தவிர தரவு ஏதேனும் உள்ளதா? ',
13624 invalidCsv: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - csv',
13625 invalidJson: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - json',
13626 jsonNotArray: 'படித்த கோப்பில் வரிசைகள் உள்ளது, நடைமுறை ரத்து செய் : json'
13629 sizes : 'உருப்படிகள் / பக்கம்',
13630 totalItems : 'உருப்படிகள் '
13635 aggregate_count : 'மதிப்பீட்டு : எண்ணு',
13636 aggregate_sum : 'மதிப்பீட்டு : கூட்டல்',
13637 aggregate_max : 'மதிப்பீட்டு : அதிகபட்சம்',
13638 aggregate_min : 'மதிப்பீட்டு : குறைந்தபட்சம்',
13639 aggregate_avg : 'மதிப்பீட்டு : சராசரி',
13640 aggregate_remove : 'மதிப்பீட்டு : நீக்கு'
13649 angular.module('ui.grid').config(['$provide', function($provide) {
13650 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13651 $delegate.add('tr', {
13654 defaultFilterLabel: 'Sütun için filtre',
13655 removeFilter: 'Filtreyi Kaldır',
13656 columnMenuButtonLabel: 'Sütun Menüsü'
13658 priority: 'Öncelik:',
13659 filterLabel: "Sütun için filtre: "
13665 description: 'Sütuna göre gruplamak için sütun başlığını buraya sürükleyin ve bırakın.'
13668 placeholder: 'Arama...',
13669 showingItems: 'Gösterilen Kayıt:',
13670 selectedItems: 'Seçili Kayıt:',
13671 totalItems: 'Toplam Kayıt:',
13672 size: 'Sayfa Boyutu:',
13673 first: 'İlk Sayfa',
13674 next: 'Sonraki Sayfa',
13675 previous: 'Önceki Sayfa',
13679 text: 'Sütunları Seç:'
13682 ascending: 'Artan Sırada Sırala',
13683 descending: 'Azalan Sırada Sırala',
13684 none: 'Sıralama Yapma',
13685 remove: 'Sıralamayı Kaldır'
13688 hide: 'Sütunu Gizle'
13691 count: 'toplam satır: ',
13698 pinLeft: 'Sola Sabitle',
13699 pinRight: 'Sağa Sabitle',
13700 unpin: 'Sabitlemeyi Kaldır'
13707 buttonLabel: 'Tablo Menü'
13709 columns: 'Sütunlar:',
13710 importerTitle: 'Dosya içeri aktar',
13711 exporterAllAsCsv: 'Bütün veriyi CSV olarak dışarı aktar',
13712 exporterVisibleAsCsv: 'Görünen veriyi CSV olarak dışarı aktar',
13713 exporterSelectedAsCsv: 'Seçili veriyi CSV olarak dışarı aktar',
13714 exporterAllAsPdf: 'Bütün veriyi PDF olarak dışarı aktar',
13715 exporterVisibleAsPdf: 'Görünen veriyi PDF olarak dışarı aktar',
13716 exporterSelectedAsPdf: 'Seçili veriyi PDF olarak dışarı aktar',
13717 clearAllFilters: 'Bütün filtreleri kaldır'
13720 noHeaders: 'Sütun isimleri üretilemiyor, dosyanın bir başlığı var mı?',
13721 noObjects: 'Nesneler üretilemiyor, dosyada başlıktan başka bir veri var mı?',
13722 invalidCsv: 'Dosya işlenemedi, geçerli bir CSV dosyası mı?',
13723 invalidJson: 'Dosya işlenemedi, geçerli bir Json dosyası mı?',
13724 jsonNotArray: 'Alınan Json dosyasında bir dizi bulunmalıdır, işlem iptal ediliyor.'
13728 pageToFirst: 'İlk sayfaya',
13729 pageBack: 'Geri git',
13730 pageSelected: 'Seçili sayfa',
13731 pageForward: 'İleri git',
13732 pageToLast: 'Sona git'
13734 sizes: 'Sayfadaki nesne sayısı',
13735 totalItems: 'kayıtlar',
13736 through: '', //note(fsw) : turkish dont have this preposition
13737 of: '' //note(fsw) : turkish dont have this preposition
13741 ungroup: 'Gruplama',
13742 aggregate_count: 'Yekun: Sayı',
13743 aggregate_sum: 'Yekun: Toplam',
13744 aggregate_max: 'Yekun: Maks',
13745 aggregate_min: 'Yekun: Min',
13746 aggregate_avg: 'Yekun: Ort',
13747 aggregate_remove: 'Yekun: Sil'
13756 * @name ui.grid.i18n
13760 * This module provides i18n functions to ui.grid and any application that wants to use it
13763 * <div doc-module-components="ui.grid.i18n"></div>
13767 var DIRECTIVE_ALIASES = ['uiT', 'uiTranslate'];
13768 var FILTER_ALIASES = ['t', 'uiTranslate'];
13770 var module = angular.module('ui.grid.i18n');
13775 * @name ui.grid.i18n.constant:i18nConstants
13777 * @description constants available in i18n module
13779 module.constant('i18nConstants', {
13780 MISSING: '[MISSING]',
13781 UPDATE_EVENT: '$uiI18n',
13783 LOCALE_DIRECTIVE_ALIAS: 'uiI18n',
13784 // default to english
13788 // module.config(['$provide', function($provide) {
13789 // $provide.decorator('i18nService', ['$delegate', function($delegate) {}])}]);
13793 * @name ui.grid.i18n.service:i18nService
13795 * @description Services for i18n
13797 module.service('i18nService', ['$log', 'i18nConstants', '$rootScope',
13798 function ($log, i18nConstants, $rootScope) {
13803 get: function (lang) {
13804 return this._langs[lang.toLowerCase()];
13806 add: function (lang, strings) {
13807 var lower = lang.toLowerCase();
13808 if (!this._langs[lower]) {
13809 this._langs[lower] = {};
13811 angular.extend(this._langs[lower], strings);
13813 getAllLangs: function () {
13815 if (!this._langs) {
13819 for (var key in this._langs) {
13825 setCurrent: function (lang) {
13826 this.current = lang.toLowerCase();
13828 getCurrentLang: function () {
13829 return this.current;
13838 * @methodOf ui.grid.i18n.service:i18nService
13839 * @description Adds the languages and strings to the cache. Decorate this service to
13840 * add more translation strings
13841 * @param {string} lang language to add
13842 * @param {object} stringMaps of strings to add grouped by property names
13845 * i18nService.add('en', {
13848 * label2: 'some more items'
13852 * description: 'Drag a column header here and drop it to group by that column.'
13857 add: function (langs, stringMaps) {
13858 if (typeof(langs) === 'object') {
13859 angular.forEach(langs, function (lang) {
13861 langCache.add(lang, stringMaps);
13865 langCache.add(langs, stringMaps);
13871 * @name getAllLangs
13872 * @methodOf ui.grid.i18n.service:i18nService
13873 * @description return all currently loaded languages
13874 * @returns {array} string
13876 getAllLangs: function () {
13877 return langCache.getAllLangs();
13883 * @methodOf ui.grid.i18n.service:i18nService
13884 * @description return all currently loaded languages
13885 * @param {string} lang to return. If not specified, returns current language
13886 * @returns {object} the translation string maps for the language
13888 get: function (lang) {
13889 var language = lang ? lang : service.getCurrentLang();
13890 return langCache.get(language);
13895 * @name getSafeText
13896 * @methodOf ui.grid.i18n.service:i18nService
13897 * @description returns the text specified in the path or a Missing text if text is not found
13898 * @param {string} path property path to use for retrieving text from string map
13899 * @param {string} lang to return. If not specified, returns current language
13900 * @returns {object} the translation for the path
13903 * i18nService.getSafeText('sort.ascending')
13906 getSafeText: function (path, lang) {
13907 var language = lang ? lang : service.getCurrentLang();
13908 var trans = langCache.get(language);
13911 return i18nConstants.MISSING;
13914 var paths = path.split('.');
13915 var current = trans;
13917 for (var i = 0; i < paths.length; ++i) {
13918 if (current[paths[i]] === undefined || current[paths[i]] === null) {
13919 return i18nConstants.MISSING;
13921 current = current[paths[i]];
13931 * @name setCurrentLang
13932 * @methodOf ui.grid.i18n.service:i18nService
13933 * @description sets the current language to use in the application
13934 * $broadcasts the Update_Event on the $rootScope
13935 * @param {string} lang to set
13938 * i18nService.setCurrentLang('fr');
13942 setCurrentLang: function (lang) {
13944 langCache.setCurrent(lang);
13945 $rootScope.$broadcast(i18nConstants.UPDATE_EVENT);
13951 * @name getCurrentLang
13952 * @methodOf ui.grid.i18n.service:i18nService
13953 * @description returns the current language used in the application
13955 getCurrentLang: function () {
13956 var lang = langCache.getCurrentLang();
13958 lang = i18nConstants.DEFAULT_LANG;
13959 langCache.setCurrent(lang);
13970 var localeDirective = function (i18nService, i18nConstants) {
13972 compile: function () {
13974 pre: function ($scope, $elm, $attrs) {
13975 var alias = i18nConstants.LOCALE_DIRECTIVE_ALIAS;
13976 // check for watchable property
13977 var lang = $scope.$eval($attrs[alias]);
13979 $scope.$watch($attrs[alias], function () {
13980 i18nService.setCurrentLang(lang);
13982 } else if ($attrs.$$observers) {
13983 $attrs.$observe(alias, function () {
13984 i18nService.setCurrentLang($attrs[alias] || i18nConstants.DEFAULT_LANG);
13993 module.directive('uiI18n', ['i18nService', 'i18nConstants', localeDirective]);
13995 // directive syntax
13996 var uitDirective = function ($parse, i18nService, i18nConstants) {
13999 compile: function () {
14001 pre: function ($scope, $elm, $attrs) {
14002 var alias1 = DIRECTIVE_ALIASES[0],
14003 alias2 = DIRECTIVE_ALIASES[1];
14004 var token = $attrs[alias1] || $attrs[alias2] || $elm.html();
14005 var missing = i18nConstants.MISSING + token;
14007 if ($attrs.$$observers) {
14008 var prop = $attrs[alias1] ? alias1 : alias2;
14009 observer = $attrs.$observe(prop, function (result) {
14011 $elm.html($parse(result)(i18nService.getCurrentLang()) || missing);
14015 var getter = $parse(token);
14016 var listener = $scope.$on(i18nConstants.UPDATE_EVENT, function (evt) {
14018 observer($attrs[alias1] || $attrs[alias2]);
14020 // set text based on i18n current language
14021 $elm.html(getter(i18nService.get()) || missing);
14024 $scope.$on('$destroy', listener);
14026 $elm.html(getter(i18nService.get()) || missing);
14033 angular.forEach( DIRECTIVE_ALIASES, function ( alias ) {
14034 module.directive( alias, ['$parse', 'i18nService', 'i18nConstants', uitDirective] );
14037 // optional filter syntax
14038 var uitFilter = function ($parse, i18nService, i18nConstants) {
14039 return function (data) {
14040 var getter = $parse(data);
14041 // set text based on i18n current language
14042 return getter(i18nService.get()) || i18nConstants.MISSING + data;
14046 angular.forEach( FILTER_ALIASES, function ( alias ) {
14047 module.filter( alias, ['$parse', 'i18nService', 'i18nConstants', uitFilter] );
14053 angular.module('ui.grid').config(['$provide', function($provide) {
14054 $provide.decorator('i18nService', ['$delegate', function($delegate) {
14055 $delegate.add('zh-cn', {
14058 defaultFilterLabel: '列过滤器',
14059 removeFilter: '移除过滤器',
14060 columnMenuButtonLabel: '列菜单'
14063 filterLabel: "列过滤器: "
14069 description: '拖曳表头到此处进行分组'
14073 showingItems: '已显示行数:',
14074 selectedItems: '已选择行数:',
14075 totalItems: '总行数:',
14111 buttonLabel: '表格菜单'
14114 importerTitle: '导入文件',
14115 exporterAllAsCsv: '导出全部数据到CSV',
14116 exporterVisibleAsCsv: '导出可见数据到CSV',
14117 exporterSelectedAsCsv: '导出已选数据到CSV',
14118 exporterAllAsPdf: '导出全部数据到PDF',
14119 exporterVisibleAsPdf: '导出可见数据到PDF',
14120 exporterSelectedAsPdf: '导出已选数据到PDF',
14121 clearAllFilters: '清除所有过滤器'
14124 noHeaders: '无法获取列名,确定文件包含表头?',
14125 noObjects: '无法获取数据,确定文件包含数据?',
14126 invalidCsv: '无法处理文件,确定是合法的CSV文件?',
14127 invalidJson: '无法处理文件,确定是合法的JSON文件?',
14128 jsonNotArray: '导入的文件不是JSON数组!'
14132 pageToFirst: '第一页',
14134 pageSelected: '当前页',
14135 pageForward: '下一页',
14146 aggregate_count: '合计: 计数',
14147 aggregate_sum: '合计: 求和',
14148 aggregate_max: '合计: 最大',
14149 aggregate_min: '合计: 最小',
14150 aggregate_avg: '合计: 平均',
14151 aggregate_remove: '合计: 移除'
14160 angular.module('ui.grid').config(['$provide', function($provide) {
14161 $provide.decorator('i18nService', ['$delegate', function($delegate) {
14162 $delegate.add('zh-tw', {
14167 description: '拖曳表頭到此處進行分組'
14171 showingItems: '已顯示行數:',
14172 selectedItems: '已選擇行數:',
14173 totalItems: '總行數:',
14205 importerTitle: '導入文件',
14206 exporterAllAsCsv: '導出全部數據到CSV',
14207 exporterVisibleAsCsv: '導出可見數據到CSV',
14208 exporterSelectedAsCsv: '導出已選數據到CSV',
14209 exporterAllAsPdf: '導出全部數據到PDF',
14210 exporterVisibleAsPdf: '導出可見數據到PDF',
14211 exporterSelectedAsPdf: '導出已選數據到PDF',
14212 clearAllFilters: '清除所有过滤器'
14215 noHeaders: '無法獲取列名,確定文件包含表頭?',
14216 noObjects: '無法獲取數據,確定文件包含數據?',
14217 invalidCsv: '無法處理文件,確定是合法的CSV文件?',
14218 invalidJson: '無法處理文件,確定是合法的JSON文件?',
14219 jsonNotArray: '導入的文件不是JSON數組!'
14235 * @name ui.grid.autoResize
14239 * #ui.grid.autoResize
14241 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
14243 * This module provides auto-resizing functionality to UI-Grid.
14245 var module = angular.module('ui.grid.autoResize', ['ui.grid']);
14248 module.directive('uiGridAutoResize', ['$timeout', 'gridUtil', function ($timeout, gridUtil) {
14252 link: function ($scope, $elm, $attrs, uiGridCtrl) {
14253 var prevGridWidth, prevGridHeight;
14255 function getDimensions() {
14256 prevGridHeight = gridUtil.elementHeight($elm);
14257 prevGridWidth = gridUtil.elementWidth($elm);
14260 // Initialize the dimensions
14263 var resizeTimeoutId;
14264 function startTimeout() {
14265 clearTimeout(resizeTimeoutId);
14267 resizeTimeoutId = setTimeout(function () {
14268 var newGridHeight = gridUtil.elementHeight($elm);
14269 var newGridWidth = gridUtil.elementWidth($elm);
14271 if (newGridHeight !== prevGridHeight || newGridWidth !== prevGridWidth) {
14272 uiGridCtrl.grid.gridHeight = newGridHeight;
14273 uiGridCtrl.grid.gridWidth = newGridWidth;
14275 $scope.$apply(function () {
14276 uiGridCtrl.grid.refresh()
14277 .then(function () {
14292 $scope.$on('$destroy', function() {
14293 clearTimeout(resizeTimeoutId);
14305 * @name ui.grid.cellNav
14311 <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
14313 This module provides auto-resizing functionality to UI-Grid.
14315 var module = angular.module('ui.grid.cellNav', ['ui.grid']);
14319 * @name ui.grid.cellNav.constant:uiGridCellNavConstants
14321 * @description constants available in cellNav
14323 module.constant('uiGridCellNavConstants', {
14324 FEATURE_NAME: 'gridCellNav',
14325 CELL_NAV_EVENT: 'cellNav',
14326 direction: {LEFT: 0, RIGHT: 1, UP: 2, DOWN: 3, PG_UP: 4, PG_DOWN: 5},
14335 module.factory('uiGridCellNavFactory', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', 'GridRowColumn', '$q',
14336 function (gridUtil, uiGridConstants, uiGridCellNavConstants, GridRowColumn, $q) {
14339 * @name ui.grid.cellNav.object:CellNav
14340 * @description returns a CellNav prototype function
14341 * @param {object} rowContainer container for rows
14342 * @param {object} colContainer parent column container
14343 * @param {object} leftColContainer column container to the left of parent
14344 * @param {object} rightColContainer column container to the right of parent
14346 var UiGridCellNav = function UiGridCellNav(rowContainer, colContainer, leftColContainer, rightColContainer) {
14347 this.rows = rowContainer.visibleRowCache;
14348 this.columns = colContainer.visibleColumnCache;
14349 this.leftColumns = leftColContainer ? leftColContainer.visibleColumnCache : [];
14350 this.rightColumns = rightColContainer ? rightColContainer.visibleColumnCache : [];
14351 this.bodyContainer = rowContainer;
14354 /** returns focusable columns of all containers */
14355 UiGridCellNav.prototype.getFocusableCols = function () {
14356 var allColumns = this.leftColumns.concat(this.columns, this.rightColumns);
14358 return allColumns.filter(function (col) {
14359 return col.colDef.allowCellFocus;
14365 * @name ui.grid.cellNav.api:GridRow
14367 * @description GridRow settings for cellNav feature, these are available to be
14368 * set only internally (for example, by other features)
14373 * @name allowCellFocus
14374 * @propertyOf ui.grid.cellNav.api:GridRow
14375 * @description Enable focus on a cell within this row. If set to false then no cells
14376 * in this row can be focused - group header rows as an example would set this to false.
14377 * <br/>Defaults to true
14379 /** returns focusable rows */
14380 UiGridCellNav.prototype.getFocusableRows = function () {
14381 return this.rows.filter(function(row) {
14382 return row.allowCellFocus !== false;
14386 UiGridCellNav.prototype.getNextRowCol = function (direction, curRow, curCol) {
14387 switch (direction) {
14388 case uiGridCellNavConstants.direction.LEFT:
14389 return this.getRowColLeft(curRow, curCol);
14390 case uiGridCellNavConstants.direction.RIGHT:
14391 return this.getRowColRight(curRow, curCol);
14392 case uiGridCellNavConstants.direction.UP:
14393 return this.getRowColUp(curRow, curCol);
14394 case uiGridCellNavConstants.direction.DOWN:
14395 return this.getRowColDown(curRow, curCol);
14396 case uiGridCellNavConstants.direction.PG_UP:
14397 return this.getRowColPageUp(curRow, curCol);
14398 case uiGridCellNavConstants.direction.PG_DOWN:
14399 return this.getRowColPageDown(curRow, curCol);
14404 UiGridCellNav.prototype.initializeSelection = function () {
14405 var focusableCols = this.getFocusableCols();
14406 var focusableRows = this.getFocusableRows();
14407 if (focusableCols.length === 0 || focusableRows.length === 0) {
14411 var curRowIndex = 0;
14412 var curColIndex = 0;
14413 return new GridRowColumn(focusableRows[0], focusableCols[0]); //return same row
14416 UiGridCellNav.prototype.getRowColLeft = function (curRow, curCol) {
14417 var focusableCols = this.getFocusableCols();
14418 var focusableRows = this.getFocusableRows();
14419 var curColIndex = focusableCols.indexOf(curCol);
14420 var curRowIndex = focusableRows.indexOf(curRow);
14422 //could not find column in focusable Columns so set it to 1
14423 if (curColIndex === -1) {
14427 var nextColIndex = curColIndex === 0 ? focusableCols.length - 1 : curColIndex - 1;
14429 //get column to left
14430 if (nextColIndex > curColIndex) {
14431 // On the first row
14432 // if (curRowIndex === 0 && curColIndex === 0) {
14435 if (curRowIndex === 0) {
14436 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
14439 //up one row and far right column
14440 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[nextColIndex]);
14444 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
14450 UiGridCellNav.prototype.getRowColRight = function (curRow, curCol) {
14451 var focusableCols = this.getFocusableCols();
14452 var focusableRows = this.getFocusableRows();
14453 var curColIndex = focusableCols.indexOf(curCol);
14454 var curRowIndex = focusableRows.indexOf(curRow);
14456 //could not find column in focusable Columns so set it to 0
14457 if (curColIndex === -1) {
14460 var nextColIndex = curColIndex === focusableCols.length - 1 ? 0 : curColIndex + 1;
14462 if (nextColIndex < curColIndex) {
14463 if (curRowIndex === focusableRows.length - 1) {
14464 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
14467 //down one row and far left column
14468 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[nextColIndex]);
14472 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
14476 UiGridCellNav.prototype.getRowColDown = function (curRow, curCol) {
14477 var focusableCols = this.getFocusableCols();
14478 var focusableRows = this.getFocusableRows();
14479 var curColIndex = focusableCols.indexOf(curCol);
14480 var curRowIndex = focusableRows.indexOf(curRow);
14482 //could not find column in focusable Columns so set it to 0
14483 if (curColIndex === -1) {
14487 if (curRowIndex === focusableRows.length - 1) {
14488 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
14492 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[curColIndex]);
14496 UiGridCellNav.prototype.getRowColPageDown = function (curRow, curCol) {
14497 var focusableCols = this.getFocusableCols();
14498 var focusableRows = this.getFocusableRows();
14499 var curColIndex = focusableCols.indexOf(curCol);
14500 var curRowIndex = focusableRows.indexOf(curRow);
14502 //could not find column in focusable Columns so set it to 0
14503 if (curColIndex === -1) {
14507 var pageSize = this.bodyContainer.minRowsToRender();
14508 if (curRowIndex >= focusableRows.length - pageSize) {
14509 return new GridRowColumn(focusableRows[focusableRows.length - 1], focusableCols[curColIndex]); //return last row
14513 return new GridRowColumn(focusableRows[curRowIndex + pageSize], focusableCols[curColIndex]);
14517 UiGridCellNav.prototype.getRowColUp = function (curRow, curCol) {
14518 var focusableCols = this.getFocusableCols();
14519 var focusableRows = this.getFocusableRows();
14520 var curColIndex = focusableCols.indexOf(curCol);
14521 var curRowIndex = focusableRows.indexOf(curRow);
14523 //could not find column in focusable Columns so set it to 0
14524 if (curColIndex === -1) {
14528 if (curRowIndex === 0) {
14529 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
14533 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[curColIndex]);
14537 UiGridCellNav.prototype.getRowColPageUp = function (curRow, curCol) {
14538 var focusableCols = this.getFocusableCols();
14539 var focusableRows = this.getFocusableRows();
14540 var curColIndex = focusableCols.indexOf(curCol);
14541 var curRowIndex = focusableRows.indexOf(curRow);
14543 //could not find column in focusable Columns so set it to 0
14544 if (curColIndex === -1) {
14548 var pageSize = this.bodyContainer.minRowsToRender();
14549 if (curRowIndex - pageSize < 0) {
14550 return new GridRowColumn(focusableRows[0], focusableCols[curColIndex]); //return first row
14554 return new GridRowColumn(focusableRows[curRowIndex - pageSize], focusableCols[curColIndex]);
14557 return UiGridCellNav;
14562 * @name ui.grid.cellNav.service:uiGridCellNavService
14564 * @description Services for cell navigation features. If you don't like the key maps we use,
14565 * or the direction cells navigation, override with a service decorator (see angular docs)
14567 module.service('uiGridCellNavService', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q', 'uiGridCellNavFactory', 'GridRowColumn', 'ScrollEvent',
14568 function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q, UiGridCellNav, GridRowColumn, ScrollEvent) {
14572 initializeGrid: function (grid) {
14573 grid.registerColumnBuilder(service.cellNavColumnBuilder);
14578 * @name ui.grid.cellNav:Grid.cellNav
14579 * @description cellNav properties added to grid class
14582 grid.cellNav.lastRowCol = null;
14583 grid.cellNav.focusedCells = [];
14585 service.defaultGridOptions(grid.options);
14589 * @name ui.grid.cellNav.api:PublicApi
14591 * @description Public Api for cellNav feature
14599 * @eventOf ui.grid.cellNav.api:PublicApi
14600 * @description raised when the active cell is changed
14602 * gridApi.cellNav.on.navigate(scope,function(newRowcol, oldRowCol){})
14604 * @param {object} newRowCol new position
14605 * @param {object} oldRowCol old position
14607 navigate: function (newRowCol, oldRowCol) {},
14610 * @name viewPortKeyDown
14611 * @eventOf ui.grid.cellNav.api:PublicApi
14612 * @description is raised when the viewPort receives a keyDown event. Cells never get focus in uiGrid
14613 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14614 * event whenever you need a keydown event on a cell
14616 * @param {object} event keydown event
14617 * @param {object} rowCol current rowCol position
14619 viewPortKeyDown: function (event, rowCol) {},
14623 * @name viewPortKeyPress
14624 * @eventOf ui.grid.cellNav.api:PublicApi
14625 * @description is raised when the viewPort receives a keyPress event. Cells never get focus in uiGrid
14626 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14627 * event whenever you need a keypress event on a cell
14629 * @param {object} event keypress event
14630 * @param {object} rowCol current rowCol position
14632 viewPortKeyPress: function (event, rowCol) {}
14639 * @name scrollToFocus
14640 * @methodOf ui.grid.cellNav.api:PublicApi
14641 * @description brings the specified row and column into view, and sets focus
14643 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus
14644 * @param {object} colDef to make visible and set focus
14645 * @returns {promise} a promise that is resolved after any scrolling is finished
14647 scrollToFocus: function (rowEntity, colDef) {
14648 return service.scrollToFocus(grid, rowEntity, colDef);
14653 * @name getFocusedCell
14654 * @methodOf ui.grid.cellNav.api:PublicApi
14655 * @description returns the current (or last if Grid does not have focus) focused row and column
14656 * <br> value is null if no selection has occurred
14658 getFocusedCell: function () {
14659 return grid.cellNav.lastRowCol;
14664 * @name getCurrentSelection
14665 * @methodOf ui.grid.cellNav.api:PublicApi
14666 * @description returns an array containing the current selection
14667 * <br> array is empty if no selection has occurred
14669 getCurrentSelection: function () {
14670 return grid.cellNav.focusedCells;
14675 * @name rowColSelectIndex
14676 * @methodOf ui.grid.cellNav.api:PublicApi
14677 * @description returns the index in the order in which the GridRowColumn was selected, returns -1 if the GridRowColumn
14679 * @param {object} rowCol the rowCol to evaluate
14681 rowColSelectIndex: function (rowCol) {
14682 //return gridUtil.arrayContainsObjectWithProperty(grid.cellNav.focusedCells, 'col.uid', rowCol.col.uid) &&
14684 for (var i = 0; i < grid.cellNav.focusedCells.length; i++) {
14685 if (grid.cellNav.focusedCells[i].col.uid === rowCol.col.uid &&
14686 grid.cellNav.focusedCells[i].row.uid === rowCol.row.uid) {
14697 grid.api.registerEventsFromObject(publicApi.events);
14699 grid.api.registerMethodsFromObject(publicApi.methods);
14703 defaultGridOptions: function (gridOptions) {
14706 * @name ui.grid.cellNav.api:GridOptions
14708 * @description GridOptions for cellNav feature, these are available to be
14709 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
14714 * @name modifierKeysToMultiSelectCells
14715 * @propertyOf ui.grid.cellNav.api:GridOptions
14716 * @description Enable multiple cell selection only when using the ctrlKey or shiftKey.
14717 * <br/>Defaults to false
14719 gridOptions.modifierKeysToMultiSelectCells = gridOptions.modifierKeysToMultiSelectCells === true;
14725 * @name decorateRenderContainers
14726 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14727 * @description decorates grid renderContainers with cellNav functions
14729 decorateRenderContainers: function (grid) {
14731 var rightContainer = grid.hasRightContainer() ? grid.renderContainers.right : null;
14732 var leftContainer = grid.hasLeftContainer() ? grid.renderContainers.left : null;
14734 if (leftContainer !== null) {
14735 grid.renderContainers.left.cellNav = new UiGridCellNav(grid.renderContainers.body, leftContainer, rightContainer, grid.renderContainers.body);
14737 if (rightContainer !== null) {
14738 grid.renderContainers.right.cellNav = new UiGridCellNav(grid.renderContainers.body, rightContainer, grid.renderContainers.body, leftContainer);
14741 grid.renderContainers.body.cellNav = new UiGridCellNav(grid.renderContainers.body, grid.renderContainers.body, leftContainer, rightContainer);
14746 * @name getDirection
14747 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14748 * @description determines which direction to for a given keyDown event
14749 * @returns {uiGridCellNavConstants.direction} direction
14751 getDirection: function (evt) {
14752 if (evt.keyCode === uiGridConstants.keymap.LEFT ||
14753 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey)) {
14754 return uiGridCellNavConstants.direction.LEFT;
14756 if (evt.keyCode === uiGridConstants.keymap.RIGHT ||
14757 evt.keyCode === uiGridConstants.keymap.TAB) {
14758 return uiGridCellNavConstants.direction.RIGHT;
14761 if (evt.keyCode === uiGridConstants.keymap.UP ||
14762 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ) {
14763 return uiGridCellNavConstants.direction.UP;
14766 if (evt.keyCode === uiGridConstants.keymap.PG_UP){
14767 return uiGridCellNavConstants.direction.PG_UP;
14770 if (evt.keyCode === uiGridConstants.keymap.DOWN ||
14771 evt.keyCode === uiGridConstants.keymap.ENTER && !(evt.ctrlKey || evt.altKey)) {
14772 return uiGridCellNavConstants.direction.DOWN;
14775 if (evt.keyCode === uiGridConstants.keymap.PG_DOWN){
14776 return uiGridCellNavConstants.direction.PG_DOWN;
14784 * @name cellNavColumnBuilder
14785 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14786 * @description columnBuilder function that adds cell navigation properties to grid column
14787 * @returns {promise} promise that will load any needed templates when resolved
14789 cellNavColumnBuilder: function (colDef, col, gridOptions) {
14794 * @name ui.grid.cellNav.api:ColumnDef
14796 * @description Column Definitions for cellNav feature, these are available to be
14797 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
14802 * @name allowCellFocus
14803 * @propertyOf ui.grid.cellNav.api:ColumnDef
14804 * @description Enable focus on a cell within this column.
14805 * <br/>Defaults to true
14807 colDef.allowCellFocus = colDef.allowCellFocus === undefined ? true : colDef.allowCellFocus;
14809 return $q.all(promises);
14814 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14815 * @name scrollToFocus
14816 * @description Scroll the grid such that the specified
14817 * row and column is in view, and set focus to the cell in that row and column
14818 * @param {Grid} grid the grid you'd like to act upon, usually available
14819 * from gridApi.grid
14820 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus to
14821 * @param {object} colDef to make visible and set focus to
14822 * @returns {promise} a promise that is resolved after any scrolling is finished
14824 scrollToFocus: function (grid, rowEntity, colDef) {
14825 var gridRow = null, gridCol = null;
14827 if (typeof(rowEntity) !== 'undefined' && rowEntity !== null) {
14828 gridRow = grid.getRow(rowEntity);
14831 if (typeof(colDef) !== 'undefined' && colDef !== null) {
14832 gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field);
14834 return grid.api.core.scrollToIfNecessary(gridRow, gridCol).then(function () {
14835 var rowCol = { row: gridRow, col: gridCol };
14837 // Broadcast the navigation
14838 if (gridRow !== null && gridCol !== null) {
14839 grid.cellNav.broadcastCellNav(rowCol);
14850 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14851 * @name getLeftWidth
14852 * @description Get the current drawn width of the columns in the
14853 * grid up to the numbered column, and add an apportionment for the
14854 * column that we're on. So if we are on column 0, we want to scroll
14855 * 0% (i.e. exclude this column from calc). If we're on the last column
14856 * we want to scroll to 100% (i.e. include this column in the calc). So
14857 * we include (thisColIndex / totalNumberCols) % of this column width
14858 * @param {Grid} grid the grid you'd like to act upon, usually available
14859 * from gridApi.grid
14860 * @param {gridCol} upToCol the column to total up to and including
14862 getLeftWidth: function (grid, upToCol) {
14869 var lastIndex = grid.renderContainers.body.visibleColumnCache.indexOf( upToCol );
14871 // total column widths up-to but not including the passed in column
14872 grid.renderContainers.body.visibleColumnCache.forEach( function( col, index ) {
14873 if ( index < lastIndex ){
14874 width += col.drawnWidth;
14878 // pro-rata the final column based on % of total columns.
14879 var percentage = lastIndex === 0 ? 0 : (lastIndex + 1) / grid.renderContainers.body.visibleColumnCache.length;
14880 width += upToCol.drawnWidth * percentage;
14891 * @name ui.grid.cellNav.directive:uiCellNav
14895 * @description Adds cell navigation features to the grid columns
14898 <example module="app">
14899 <file name="app.js">
14900 var app = angular.module('app', ['ui.grid', 'ui.grid.cellNav']);
14902 app.controller('MainCtrl', ['$scope', function ($scope) {
14904 { name: 'Bob', title: 'CEO' },
14905 { name: 'Frank', title: 'Lowly Developer' }
14908 $scope.columnDefs = [
14914 <file name="index.html">
14915 <div ng-controller="MainCtrl">
14916 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-cellnav></div>
14921 module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn', '$timeout', '$compile',
14922 function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, GridRowColumn, $timeout, $compile) {
14926 require: '^uiGrid',
14928 controller: function () {},
14929 compile: function () {
14931 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
14932 var _scope = $scope;
14934 var grid = uiGridCtrl.grid;
14935 uiGridCellNavService.initializeGrid(grid);
14937 uiGridCtrl.cellNav = {};
14939 //Ensure that the object has all of the methods we expect it to
14940 uiGridCtrl.cellNav.makeRowCol = function (obj) {
14941 if (!(obj instanceof GridRowColumn)) {
14942 obj = new GridRowColumn(obj.row, obj.col);
14947 uiGridCtrl.cellNav.getActiveCell = function () {
14948 var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus');
14949 if (elms.length > 0){
14956 uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown, originEvt) {
14957 modifierDown = !(modifierDown === undefined || !modifierDown);
14959 newRowCol = uiGridCtrl.cellNav.makeRowCol(newRowCol);
14961 uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown, originEvt);
14962 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, newRowCol, modifierDown, originEvt);
14965 uiGridCtrl.cellNav.clearFocus = grid.cellNav.clearFocus = function () {
14966 grid.cellNav.focusedCells = [];
14967 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT);
14970 uiGridCtrl.cellNav.broadcastFocus = function (rowCol, modifierDown, originEvt) {
14971 modifierDown = !(modifierDown === undefined || !modifierDown);
14973 rowCol = uiGridCtrl.cellNav.makeRowCol(rowCol);
14975 var row = rowCol.row,
14978 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14980 if (grid.cellNav.lastRowCol === null || rowColSelectIndex === -1) {
14981 var newRowCol = new GridRowColumn(row, col);
14983 if (grid.cellNav.lastRowCol === null || grid.cellNav.lastRowCol.row !== newRowCol.row || grid.cellNav.lastRowCol.col !== newRowCol.col){
14984 grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol);
14985 grid.cellNav.lastRowCol = newRowCol;
14987 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) {
14988 grid.cellNav.focusedCells.push(rowCol);
14990 grid.cellNav.focusedCells = [rowCol];
14992 } else if (grid.options.modifierKeysToMultiSelectCells && modifierDown &&
14993 rowColSelectIndex >= 0) {
14995 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14999 uiGridCtrl.cellNav.handleKeyDown = function (evt) {
15000 var direction = uiGridCellNavService.getDirection(evt);
15001 if (direction === null) {
15005 var containerId = 'body';
15006 if (evt.uiGridTargetRenderContainerId) {
15007 containerId = evt.uiGridTargetRenderContainerId;
15010 // Get the last-focused row+col combo
15011 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15013 // Figure out which new row+combo we're navigating to
15014 var rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(direction, lastRowCol.row, lastRowCol.col);
15015 var focusableCols = uiGridCtrl.grid.renderContainers[containerId].cellNav.getFocusableCols();
15016 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
15017 // Shift+tab on top-left cell should exit cellnav on render container
15020 direction === uiGridCellNavConstants.direction.LEFT &&
15021 // New col is last col (i.e. wrap around)
15022 rowCol.col === focusableCols[focusableCols.length - 1] &&
15023 // Staying on same row, which means we're at first row
15024 rowCol.row === lastRowCol.row &&
15025 evt.keyCode === uiGridConstants.keymap.TAB &&
15028 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
15029 uiGridCtrl.cellNav.clearFocus();
15032 // Tab on bottom-right cell should exit cellnav on render container
15034 direction === uiGridCellNavConstants.direction.RIGHT &&
15035 // New col is first col (i.e. wrap around)
15036 rowCol.col === focusableCols[0] &&
15037 // Staying on same row, which means we're at first row
15038 rowCol.row === lastRowCol.row &&
15039 evt.keyCode === uiGridConstants.keymap.TAB &&
15042 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
15043 uiGridCtrl.cellNav.clearFocus();
15047 // Scroll to the new cell, if it's not completely visible within the render container's viewport
15048 grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () {
15049 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
15053 evt.stopPropagation();
15054 evt.preventDefault();
15060 post: function ($scope, $elm, $attrs, uiGridCtrl) {
15061 var _scope = $scope;
15062 var grid = uiGridCtrl.grid;
15064 function addAriaLiveRegion(){
15065 // Thanks to google docs for the inspiration behind how to do this
15066 // XXX: Why is this entire mess nessasary?
15067 // Because browsers take a lot of coercing to get them to read out live regions
15068 //http://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
15069 var ariaNotifierDomElt = '<div ' +
15070 'id="' + grid.id +'-aria-speakable" ' +
15071 'class="ui-grid-a11y-ariascreenreader-speakable ui-grid-offscreen" ' +
15072 'aria-live="assertive" ' +
15074 'aria-atomic="true" ' +
15075 'aria-hidden="false" ' +
15076 'aria-relevant="additions" ' +
15081 var ariaNotifier = $compile(ariaNotifierDomElt)($scope);
15082 $elm.prepend(ariaNotifier);
15083 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown, originEvt) {
15085 * If the cell nav event was because of a focus event then we don't want to
15086 * change the notifier text.
15087 * Reasoning: Voice Over fires a focus events when moving arround the grid.
15088 * If the screen reader is handing the grid nav properly then we don't need to
15089 * use the alert to notify the user of the movement.
15090 * In all other cases we do want a notification event.
15092 if (originEvt && originEvt.type === 'focus'){return;}
15094 function setNotifyText(text){
15095 if (text === ariaNotifier.text()){return;}
15096 ariaNotifier[0].style.clip = 'rect(0px,0px,0px,0px)';
15098 * This is how google docs handles clearing the div. Seems to work better than setting the text of the div to ''
15100 ariaNotifier[0].innerHTML = "";
15101 ariaNotifier[0].style.visibility = 'hidden';
15102 ariaNotifier[0].style.visibility = 'visible';
15104 ariaNotifier[0].style.clip = 'auto';
15106 * The space after the text is something that google docs does.
15108 ariaNotifier[0].appendChild(document.createTextNode(text + " "));
15109 ariaNotifier[0].style.visibility = 'hidden';
15110 ariaNotifier[0].style.visibility = 'visible';
15115 var currentSelection = grid.api.cellNav.getCurrentSelection();
15116 for (var i = 0; i < currentSelection.length; i++) {
15117 values.push(currentSelection[i].getIntersectionValueFiltered());
15119 var cellText = values.toString();
15120 setNotifyText(cellText);
15124 addAriaLiveRegion();
15131 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', '$compile','uiGridCellNavConstants',
15132 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, $compile, uiGridCellNavConstants) {
15135 priority: -99999, //this needs to run very last
15136 require: ['^uiGrid', 'uiGridRenderContainer', '?^uiGridCellnav'],
15138 compile: function () {
15140 post: function ($scope, $elm, $attrs, controllers) {
15141 var uiGridCtrl = controllers[0],
15142 renderContainerCtrl = controllers[1],
15143 uiGridCellnavCtrl = controllers[2];
15145 // Skip attaching cell-nav specific logic if the directive is not attached above us
15146 if (!uiGridCtrl.grid.api.cellNav) { return; }
15148 var containerId = renderContainerCtrl.containerId;
15150 var grid = uiGridCtrl.grid;
15152 //run each time a render container is created
15153 uiGridCellNavService.decorateRenderContainers(grid);
15155 // focusser only created for body
15156 if (containerId !== 'body') {
15162 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells){
15163 $elm.attr('aria-multiselectable', true);
15165 $elm.attr('aria-multiselectable', false);
15168 //add an element with no dimensions that can be used to set focus and capture keystrokes
15169 var focuser = $compile('<div class="ui-grid-focuser" role="region" aria-live="assertive" aria-atomic="false" tabindex="0" aria-controls="' + grid.id +'-aria-speakable '+ grid.id + '-grid-container' +'" aria-owns="' + grid.id + '-grid-container' + '"></div>')($scope);
15170 $elm.append(focuser);
15172 focuser.on('focus', function (evt) {
15173 evt.uiGridTargetRenderContainerId = containerId;
15174 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15175 if (rowCol === null) {
15176 rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(uiGridCellNavConstants.direction.DOWN, null, null);
15177 if (rowCol.row && rowCol.col) {
15178 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
15183 uiGridCellnavCtrl.setAriaActivedescendant = function(id){
15184 $elm.attr('aria-activedescendant', id);
15187 uiGridCellnavCtrl.removeAriaActivedescendant = function(id){
15188 if ($elm.attr('aria-activedescendant') === id){
15189 $elm.attr('aria-activedescendant', '');
15194 uiGridCtrl.focus = function () {
15195 gridUtil.focus.byElement(focuser[0]);
15196 //allow for first time grid focus
15199 var viewPortKeyDownWasRaisedForRowCol = null;
15200 // Bind to keydown events in the render container
15201 focuser.on('keydown', function (evt) {
15202 evt.uiGridTargetRenderContainerId = containerId;
15203 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15204 var result = uiGridCtrl.cellNav.handleKeyDown(evt);
15205 if (result === null) {
15206 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyDown(evt, rowCol);
15207 viewPortKeyDownWasRaisedForRowCol = rowCol;
15210 //Bind to keypress events in the render container
15211 //keypress events are needed by edit function so the key press
15212 //that initiated an edit is not lost
15213 //must fire the event in a timeout so the editor can
15214 //initialize and subscribe to the event on another event loop
15215 focuser.on('keypress', function (evt) {
15216 if (viewPortKeyDownWasRaisedForRowCol) {
15217 $timeout(function () {
15218 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyPress(evt, viewPortKeyDownWasRaisedForRowCol);
15221 viewPortKeyDownWasRaisedForRowCol = null;
15225 $scope.$on('$destroy', function(){
15226 //Remove all event handlers associated with this focuser.
15236 module.directive('uiGridViewport', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log','$compile',
15237 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log, $compile) {
15240 priority: -99999, //this needs to run very last
15241 require: ['^uiGrid', '^uiGridRenderContainer', '?^uiGridCellnav'],
15243 compile: function () {
15245 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
15247 post: function ($scope, $elm, $attrs, controllers) {
15248 var uiGridCtrl = controllers[0],
15249 renderContainerCtrl = controllers[1];
15251 // Skip attaching cell-nav specific logic if the directive is not attached above us
15252 if (!uiGridCtrl.grid.api.cellNav) { return; }
15254 var containerId = renderContainerCtrl.containerId;
15255 //no need to process for other containers
15256 if (containerId !== 'body') {
15260 var grid = uiGridCtrl.grid;
15262 grid.api.core.on.scrollBegin($scope, function (args) {
15264 // Skip if there's no currently-focused cell
15265 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15266 if (lastRowCol === null) {
15270 //if not in my container, move on
15271 //todo: worry about horiz scroll
15272 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
15276 uiGridCtrl.cellNav.clearFocus();
15280 grid.api.core.on.scrollEnd($scope, function (args) {
15281 // Skip if there's no currently-focused cell
15282 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15283 if (lastRowCol === null) {
15287 //if not in my container, move on
15288 //todo: worry about horiz scroll
15289 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
15293 uiGridCtrl.cellNav.broadcastCellNav(lastRowCol);
15297 grid.api.cellNav.on.navigate($scope, function () {
15298 //focus again because it can be lost
15299 uiGridCtrl.focus();
15310 * @name ui.grid.cellNav.directive:uiGridCell
15313 * @description Stacks on top of ui.grid.uiGridCell to provide cell navigation
15315 module.directive('uiGridCell', ['$timeout', '$document', 'uiGridCellNavService', 'gridUtil', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn',
15316 function ($timeout, $document, uiGridCellNavService, gridUtil, uiGridCellNavConstants, uiGridConstants, GridRowColumn) {
15318 priority: -150, // run after default uiGridCell directive and ui.grid.edit uiGridCell
15320 require: ['^uiGrid', '?^uiGridCellnav'],
15322 link: function ($scope, $elm, $attrs, controllers) {
15323 var uiGridCtrl = controllers[0],
15324 uiGridCellnavCtrl = controllers[1];
15325 // Skip attaching cell-nav specific logic if the directive is not attached above us
15326 if (!uiGridCtrl.grid.api.cellNav) { return; }
15328 if (!$scope.col.colDef.allowCellFocus) {
15332 //Convinience local variables
15333 var grid = uiGridCtrl.grid;
15334 $scope.focused = false;
15336 // Make this cell focusable but only with javascript/a mouse click
15337 $elm.attr('tabindex', -1);
15339 // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused
15340 $elm.find('div').on('click', function (evt) {
15341 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), evt.ctrlKey || evt.metaKey, evt);
15343 evt.stopPropagation();
15349 * XXX Hack for screen readers.
15350 * This allows the grid to focus using only the screen reader cursor.
15351 * Since the focus event doesn't include key press information we can't use it
15352 * as our primary source of the event.
15354 $elm.on('mousedown', preventMouseDown);
15356 //turn on and off for edit events
15357 if (uiGridCtrl.grid.api.edit) {
15358 uiGridCtrl.grid.api.edit.on.beginCellEdit($scope, function () {
15359 $elm.off('mousedown', preventMouseDown);
15362 uiGridCtrl.grid.api.edit.on.afterCellEdit($scope, function () {
15363 $elm.on('mousedown', preventMouseDown);
15366 uiGridCtrl.grid.api.edit.on.cancelCellEdit($scope, function () {
15367 $elm.on('mousedown', preventMouseDown);
15371 function preventMouseDown(evt) {
15372 //Prevents the foucus event from firing if the click event is already going to fire.
15373 //If both events fire it will cause bouncing behavior.
15374 evt.preventDefault();
15377 //You can only focus on elements with a tabindex value
15378 $elm.on('focus', function (evt) {
15379 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), false, evt);
15380 evt.stopPropagation();
15384 // This event is fired for all cells. If the cell matches, then focus is set
15385 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown) {
15386 var isFocused = grid.cellNav.focusedCells.some(function(focusedRowCol, index){
15387 return (focusedRowCol.row === $scope.row && focusedRowCol.col === $scope.col);
15396 function setFocused() {
15397 if (!$scope.focused){
15398 var div = $elm.find('div');
15399 div.addClass('ui-grid-cell-focus');
15400 $elm.attr('aria-selected', true);
15401 uiGridCellnavCtrl.setAriaActivedescendant($elm.attr('id'));
15402 $scope.focused = true;
15406 function clearFocus() {
15407 if ($scope.focused){
15408 var div = $elm.find('div');
15409 div.removeClass('ui-grid-cell-focus');
15410 $elm.attr('aria-selected', false);
15411 uiGridCellnavCtrl.removeAriaActivedescendant($elm.attr('id'));
15412 $scope.focused = false;
15416 $scope.$on('$destroy', function () {
15417 //.off withouth paramaters removes all handlers
15418 $elm.find('div').off();
15432 * @name ui.grid.edit
15437 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
15439 * This module provides cell editing capability to ui.grid. The goal was to emulate keying data in a spreadsheet via
15443 * To really get the full spreadsheet-like data entry, the ui.grid.cellNav module should be used. This will allow the
15444 * user to key data and then tab, arrow, or enter to the cells beside or below.
15446 * <div doc-module-components="ui.grid.edit"></div>
15449 var module = angular.module('ui.grid.edit', ['ui.grid']);
15453 * @name ui.grid.edit.constant:uiGridEditConstants
15455 * @description constants available in edit module
15457 module.constant('uiGridEditConstants', {
15458 EDITABLE_CELL_TEMPLATE: /EDITABLE_CELL_TEMPLATE/g,
15459 //must be lowercase because template bulder converts to lower
15460 EDITABLE_CELL_DIRECTIVE: /editable_cell_directive/g,
15462 BEGIN_CELL_EDIT: 'uiGridEventBeginCellEdit',
15463 END_CELL_EDIT: 'uiGridEventEndCellEdit',
15464 CANCEL_CELL_EDIT: 'uiGridEventCancelCellEdit'
15470 * @name ui.grid.edit.service:uiGridEditService
15472 * @description Services for editing features
15474 module.service('uiGridEditService', ['$q', 'uiGridConstants', 'gridUtil',
15475 function ($q, uiGridConstants, gridUtil) {
15479 initializeGrid: function (grid) {
15481 service.defaultGridOptions(grid.options);
15483 grid.registerColumnBuilder(service.editColumnBuilder);
15488 * @name ui.grid.edit.api:PublicApi
15490 * @description Public Api for edit feature
15497 * @name afterCellEdit
15498 * @eventOf ui.grid.edit.api:PublicApi
15499 * @description raised when cell editing is complete
15501 * gridApi.edit.on.afterCellEdit(scope,function(rowEntity, colDef){})
15503 * @param {object} rowEntity the options.data element that was edited
15504 * @param {object} colDef the column that was edited
15505 * @param {object} newValue new value
15506 * @param {object} oldValue old value
15508 afterCellEdit: function (rowEntity, colDef, newValue, oldValue) {
15512 * @name beginCellEdit
15513 * @eventOf ui.grid.edit.api:PublicApi
15514 * @description raised when cell editing starts on a cell
15516 * gridApi.edit.on.beginCellEdit(scope,function(rowEntity, colDef){})
15518 * @param {object} rowEntity the options.data element that was edited
15519 * @param {object} colDef the column that was edited
15520 * @param {object} triggerEvent the event that triggered the edit. Useful to prevent losing keystrokes on some
15523 beginCellEdit: function (rowEntity, colDef, triggerEvent) {
15527 * @name cancelCellEdit
15528 * @eventOf ui.grid.edit.api:PublicApi
15529 * @description raised when cell editing is cancelled on a cell
15531 * gridApi.edit.on.cancelCellEdit(scope,function(rowEntity, colDef){})
15533 * @param {object} rowEntity the options.data element that was edited
15534 * @param {object} colDef the column that was edited
15536 cancelCellEdit: function (rowEntity, colDef) {
15545 grid.api.registerEventsFromObject(publicApi.events);
15546 //grid.api.registerMethodsFromObject(publicApi.methods);
15550 defaultGridOptions: function (gridOptions) {
15554 * @name ui.grid.edit.api:GridOptions
15556 * @description Options for configuring the edit feature, these are available to be
15557 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
15562 * @name enableCellEdit
15563 * @propertyOf ui.grid.edit.api:GridOptions
15564 * @description If defined, sets the default value for the editable flag on each individual colDefs
15565 * if their individual enableCellEdit configuration is not defined. Defaults to undefined.
15570 * @name cellEditableCondition
15571 * @propertyOf ui.grid.edit.api:GridOptions
15572 * @description If specified, either a value or function to be used by all columns before editing.
15573 * If falsy, then editing of cell is not allowed.
15576 * function($scope){
15577 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15582 gridOptions.cellEditableCondition = gridOptions.cellEditableCondition === undefined ? true : gridOptions.cellEditableCondition;
15586 * @name editableCellTemplate
15587 * @propertyOf ui.grid.edit.api:GridOptions
15588 * @description If specified, cellTemplate to use as the editor for all columns.
15589 * <br/> defaults to 'ui-grid/cellTextEditor'
15594 * @name enableCellEditOnFocus
15595 * @propertyOf ui.grid.edit.api:GridOptions
15596 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15597 * <br/>_requires cellNav feature and the edit feature to be enabled_
15599 //enableCellEditOnFocus can only be used if cellnav module is used
15600 gridOptions.enableCellEditOnFocus = gridOptions.enableCellEditOnFocus === undefined ? false : gridOptions.enableCellEditOnFocus;
15605 * @name editColumnBuilder
15606 * @methodOf ui.grid.edit.service:uiGridEditService
15607 * @description columnBuilder function that adds edit properties to grid column
15608 * @returns {promise} promise that will load any needed templates when resolved
15610 editColumnBuilder: function (colDef, col, gridOptions) {
15616 * @name ui.grid.edit.api:ColumnDef
15618 * @description Column Definition for edit feature, these are available to be
15619 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
15624 * @name enableCellEdit
15625 * @propertyOf ui.grid.edit.api:ColumnDef
15626 * @description enable editing on column
15628 colDef.enableCellEdit = colDef.enableCellEdit === undefined ? (gridOptions.enableCellEdit === undefined ?
15629 (colDef.type !== 'object') : gridOptions.enableCellEdit) : colDef.enableCellEdit;
15633 * @name cellEditableCondition
15634 * @propertyOf ui.grid.edit.api:ColumnDef
15635 * @description If specified, either a value or function evaluated before editing cell. If falsy, then editing of cell is not allowed.
15638 * function($scope){
15639 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15644 colDef.cellEditableCondition = colDef.cellEditableCondition === undefined ? gridOptions.cellEditableCondition : colDef.cellEditableCondition;
15648 * @name editableCellTemplate
15649 * @propertyOf ui.grid.edit.api:ColumnDef
15650 * @description cell template to be used when editing this column. Can be Url or text template
15651 * <br/>Defaults to gridOptions.editableCellTemplate
15653 if (colDef.enableCellEdit) {
15654 colDef.editableCellTemplate = colDef.editableCellTemplate || gridOptions.editableCellTemplate || 'ui-grid/cellEditor';
15656 promises.push(gridUtil.getTemplate(colDef.editableCellTemplate)
15658 function (template) {
15659 col.editableCellTemplate = template;
15662 // Todo handle response error here?
15663 throw new Error("Couldn't fetch/use colDef.editableCellTemplate '" + colDef.editableCellTemplate + "'");
15669 * @name enableCellEditOnFocus
15670 * @propertyOf ui.grid.edit.api:ColumnDef
15671 * @requires ui.grid.cellNav
15672 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15673 * <br>_requires both the cellNav feature and the edit feature to be enabled_
15675 //enableCellEditOnFocus can only be used if cellnav module is used
15676 colDef.enableCellEditOnFocus = colDef.enableCellEditOnFocus === undefined ? gridOptions.enableCellEditOnFocus : colDef.enableCellEditOnFocus;
15681 * @name editModelField
15682 * @propertyOf ui.grid.edit.api:ColumnDef
15683 * @description a bindable string value that is used when binding to edit controls instead of colDef.field
15684 * <br/> example: You have a complex property on and object like state:{abbrev:'MS',name:'Mississippi'}. The
15685 * grid should display state.name in the cell and sort/filter based on the state.name property but the editor
15686 * requires the full state object.
15687 * <br/>colDef.field = 'state.name'
15688 * <br/>colDef.editModelField = 'state'
15690 //colDef.editModelField
15692 return $q.all(promises);
15697 * @name isStartEditKey
15698 * @methodOf ui.grid.edit.service:uiGridEditService
15699 * @description Determines if a keypress should start editing. Decorate this service to override with your
15700 * own key events. See service decorator in angular docs.
15701 * @param {Event} evt keydown event
15702 * @returns {boolean} true if an edit should start
15704 isStartEditKey: function (evt) {
15706 evt.keyCode === uiGridConstants.keymap.ESC ||
15707 evt.keyCode === uiGridConstants.keymap.SHIFT ||
15708 evt.keyCode === uiGridConstants.keymap.CTRL ||
15709 evt.keyCode === uiGridConstants.keymap.ALT ||
15710 evt.keyCode === uiGridConstants.keymap.WIN ||
15711 evt.keyCode === uiGridConstants.keymap.CAPSLOCK ||
15713 evt.keyCode === uiGridConstants.keymap.LEFT ||
15714 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey) ||
15716 evt.keyCode === uiGridConstants.keymap.RIGHT ||
15717 evt.keyCode === uiGridConstants.keymap.TAB ||
15719 evt.keyCode === uiGridConstants.keymap.UP ||
15720 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ||
15722 evt.keyCode === uiGridConstants.keymap.DOWN ||
15723 evt.keyCode === uiGridConstants.keymap.ENTER) {
15739 * @name ui.grid.edit.directive:uiGridEdit
15743 * @description Adds editing features to the ui-grid directive.
15746 <example module="app">
15747 <file name="app.js">
15748 var app = angular.module('app', ['ui.grid', 'ui.grid.edit']);
15750 app.controller('MainCtrl', ['$scope', function ($scope) {
15752 { name: 'Bob', title: 'CEO' },
15753 { name: 'Frank', title: 'Lowly Developer' }
15756 $scope.columnDefs = [
15757 {name: 'name', enableCellEdit: true},
15758 {name: 'title', enableCellEdit: true}
15762 <file name="index.html">
15763 <div ng-controller="MainCtrl">
15764 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit></div>
15769 module.directive('uiGridEdit', ['gridUtil', 'uiGridEditService', function (gridUtil, uiGridEditService) {
15773 require: '^uiGrid',
15775 compile: function () {
15777 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
15778 uiGridEditService.initializeGrid(uiGridCtrl.grid);
15780 post: function ($scope, $elm, $attrs, uiGridCtrl) {
15789 * @name ui.grid.edit.directive:uiGridRenderContainer
15793 * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits
15796 module.directive('uiGridViewport', [ 'uiGridEditConstants',
15797 function ( uiGridEditConstants) {
15800 priority: -99998, //run before cellNav
15801 require: ['^uiGrid', '^uiGridRenderContainer'],
15803 compile: function () {
15805 post: function ($scope, $elm, $attrs, controllers) {
15806 var uiGridCtrl = controllers[0];
15808 // Skip attaching if edit and cellNav is not enabled
15809 if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; }
15811 var containerId = controllers[1].containerId;
15812 //no need to process for other containers
15813 if (containerId !== 'body') {
15817 //refocus on the grid
15818 $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
15819 uiGridCtrl.focus();
15821 $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
15822 uiGridCtrl.focus();
15833 * @name ui.grid.edit.directive:uiGridCell
15837 * @description Stacks on top of ui.grid.uiGridCell to provide in-line editing capabilities to the cell
15840 * Binds edit start events to the uiGridCell element. When the events fire, the gridCell element is appended
15841 * with the columnDef.editableCellTemplate element ('cellEditor.html' by default).
15843 * The editableCellTemplate should respond to uiGridEditConstants.events.BEGIN\_CELL\_EDIT angular event
15844 * and do the initial steps needed to edit the cell (setfocus on input element, etc).
15846 * When the editableCellTemplate recognizes that the editing is ended (blur event, Enter key, etc.)
15847 * it should emit the uiGridEditConstants.events.END\_CELL\_EDIT event.
15849 * If editableCellTemplate recognizes that the editing has been cancelled (esc key)
15850 * it should emit the uiGridEditConstants.events.CANCEL\_CELL\_EDIT event. The original value
15851 * will be set back on the model by the uiGridCell directive.
15853 * Events that invoke editing:
15855 * - F2 keydown (when using cell selection)
15857 * Events that end editing:
15858 * - Dependent on the specific editableCellTemplate
15859 * - Standards should be blur and enter keydown
15861 * Events that cancel editing:
15862 * - Dependent on the specific editableCellTemplate
15863 * - Standards should be Esc keydown
15865 * Grid Events that end editing:
15866 * - uiGridConstants.events.GRID_SCROLL
15872 * @name ui.grid.edit.api:GridRow
15874 * @description GridRow options for edit feature, these are available to be
15875 * set internally only, by other features
15880 * @name enableCellEdit
15881 * @propertyOf ui.grid.edit.api:GridRow
15882 * @description enable editing on row, grouping for example might disable editing on group header rows
15885 module.directive('uiGridCell',
15886 ['$compile', '$injector', '$timeout', 'uiGridConstants', 'uiGridEditConstants', 'gridUtil', '$parse', 'uiGridEditService', '$rootScope', '$q',
15887 function ($compile, $injector, $timeout, uiGridConstants, uiGridEditConstants, gridUtil, $parse, uiGridEditService, $rootScope, $q) {
15888 var touchstartTimeout = 500;
15889 if ($injector.has('uiGridCellNavService')) {
15890 var uiGridCellNavService = $injector.get('uiGridCellNavService');
15894 priority: -100, // run after default uiGridCell directive
15897 require: '?^uiGrid',
15898 link: function ($scope, $elm, $attrs, uiGridCtrl) {
15901 var inEdit = false;
15903 var cancelTouchstartTimeout;
15907 if (!$scope.col.colDef.enableCellEdit) {
15911 var cellNavNavigateDereg = function() {};
15912 var viewPortKeyDownDereg = function() {};
15915 var setEditable = function() {
15916 if ($scope.col.colDef.enableCellEdit && $scope.row.enableCellEdit !== false) {
15917 if (!$scope.beginEditEventsWired) { //prevent multiple attachments
15918 registerBeginEditEvents();
15921 if ($scope.beginEditEventsWired) {
15922 cancelBeginEditEvents();
15929 var rowWatchDereg = $scope.$watch('row', function (n, o) {
15936 $scope.$on( '$destroy', rowWatchDereg );
15938 function registerBeginEditEvents() {
15939 $elm.on('dblclick', beginEdit);
15941 // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit
15942 $elm.on('touchstart', touchStart);
15944 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15946 viewPortKeyDownDereg = uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
15947 if (rowCol === null) {
15951 if (rowCol.row === $scope.row && rowCol.col === $scope.col && !$scope.col.colDef.enableCellEditOnFocus) {
15952 //important to do this before scrollToIfNecessary
15953 beginEditKeyDown(evt);
15957 cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) {
15958 if ($scope.col.colDef.enableCellEditOnFocus) {
15959 // Don't begin edit if the cell hasn't changed
15960 if ((!oldRowCol || newRowCol.row !== oldRowCol.row || newRowCol.col !== oldRowCol.col) &&
15961 newRowCol.row === $scope.row && newRowCol.col === $scope.col) {
15962 $timeout(function () {
15970 $scope.beginEditEventsWired = true;
15974 function touchStart(event) {
15975 // jQuery masks events
15976 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
15977 event = event.originalEvent;
15980 // Bind touchend handler
15981 $elm.on('touchend', touchEnd);
15984 cancelTouchstartTimeout = $timeout(function() { }, touchstartTimeout);
15986 // Timeout's done! Start the edit
15987 cancelTouchstartTimeout.then(function () {
15988 // Use setTimeout to start the edit because beginEdit expects to be outside of $digest
15989 setTimeout(beginEdit, 0);
15991 // Undbind the touchend handler, we don't need it anymore
15992 $elm.off('touchend', touchEnd);
15996 // Cancel any touchstart timeout
15997 function touchEnd(event) {
15998 $timeout.cancel(cancelTouchstartTimeout);
15999 $elm.off('touchend', touchEnd);
16002 function cancelBeginEditEvents() {
16003 $elm.off('dblclick', beginEdit);
16004 $elm.off('keydown', beginEditKeyDown);
16005 $elm.off('touchstart', touchStart);
16006 cellNavNavigateDereg();
16007 viewPortKeyDownDereg();
16008 $scope.beginEditEventsWired = false;
16011 function beginEditKeyDown(evt) {
16012 if (uiGridEditService.isStartEditKey(evt)) {
16017 function shouldEdit(col, row) {
16018 return !row.isSaving &&
16019 ( angular.isFunction(col.colDef.cellEditableCondition) ?
16020 col.colDef.cellEditableCondition($scope) :
16021 col.colDef.cellEditableCondition );
16025 function beginEdit(triggerEvent) {
16026 //we need to scroll the cell into focus before invoking the editor
16027 $scope.grid.api.core.scrollToIfNecessary($scope.row, $scope.col)
16028 .then(function () {
16029 beginEditAfterScroll(triggerEvent);
16035 * @name editDropdownOptionsArray
16036 * @propertyOf ui.grid.edit.api:ColumnDef
16037 * @description an array of values in the format
16038 * [ {id: xxx, value: xxx} ], which is populated
16039 * into the edit dropdown
16044 * @name editDropdownIdLabel
16045 * @propertyOf ui.grid.edit.api:ColumnDef
16046 * @description the label for the "id" field
16047 * in the editDropdownOptionsArray. Defaults
16051 * $scope.gridOptions = {
16053 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16054 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
16055 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16062 * @name editDropdownRowEntityOptionsArrayPath
16063 * @propertyOf ui.grid.edit.api:ColumnDef
16064 * @description a path to a property on row.entity containing an
16065 * array of values in the format
16066 * [ {id: xxx, value: xxx} ], which will be used to populate
16067 * the edit dropdown. This can be used when the dropdown values are dependent on
16068 * the backing row entity.
16069 * If this property is set then editDropdownOptionsArray will be ignored.
16072 * $scope.gridOptions = {
16074 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16075 * editDropdownRowEntityOptionsArrayPath: 'foo.bars[0].baz',
16076 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16083 * @name editDropdownOptionsFunction
16084 * @methodOf ui.grid.edit.api:ColumnDef
16085 * @description a function returning an array of values in the format
16086 * [ {id: xxx, value: xxx} ], which will be used to populate
16087 * the edit dropdown. This can be used when the dropdown values are dependent on
16088 * the backing row entity with some kind of algorithm.
16089 * If this property is set then both editDropdownOptionsArray and
16090 * editDropdownRowEntityOptionsArrayPath will be ignored.
16091 * @param {object} rowEntity the options.data element that the returned array refers to
16092 * @param {object} colDef the column that implements this dropdown
16093 * @returns {object} an array of values in the format
16094 * [ {id: xxx, value: xxx} ] used to populate the edit dropdown
16097 * $scope.gridOptions = {
16099 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16100 * editDropdownOptionsFunction: function(rowEntity, colDef) {
16101 * if (rowEntity.foo === 'bar') {
16102 * return [{id: 'bar1', value: 'BAR 1'},
16103 * {id: 'bar2', value: 'BAR 2'},
16104 * {id: 'bar3', value: 'BAR 3'}];
16106 * return [{id: 'foo1', value: 'FOO 1'},
16107 * {id: 'foo2', value: 'FOO 2'}];
16110 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16117 * @name editDropdownValueLabel
16118 * @propertyOf ui.grid.edit.api:ColumnDef
16119 * @description the label for the "value" field
16120 * in the editDropdownOptionsArray. Defaults
16124 * $scope.gridOptions = {
16126 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16127 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
16128 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16135 * @name editDropdownFilter
16136 * @propertyOf ui.grid.edit.api:ColumnDef
16137 * @description A filter that you would like to apply to the values in the options list
16138 * of the dropdown. For example if you were using angular-translate you might set this
16142 * $scope.gridOptions = {
16144 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16145 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
16146 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status', editDropdownFilter: 'translate' }
16151 function beginEditAfterScroll(triggerEvent) {
16152 // If we are already editing, then just skip this so we don't try editing twice...
16157 if (!shouldEdit($scope.col, $scope.row)) {
16162 cellModel = $parse($scope.row.getQualifiedColField($scope.col));
16163 //get original value from the cell
16164 origCellValue = cellModel($scope);
16166 html = $scope.col.editableCellTemplate;
16168 if ($scope.col.colDef.editModelField) {
16169 html = html.replace(uiGridConstants.MODEL_COL_FIELD, gridUtil.preEval('row.entity.' + $scope.col.colDef.editModelField));
16172 html = html.replace(uiGridConstants.MODEL_COL_FIELD, $scope.row.getQualifiedColField($scope.col));
16175 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
16177 var optionFilter = $scope.col.colDef.editDropdownFilter ? '|' + $scope.col.colDef.editDropdownFilter : '';
16178 html = html.replace(uiGridConstants.CUSTOM_FILTERS, optionFilter);
16180 var inputType = 'text';
16181 switch ($scope.col.colDef.type){
16183 inputType = 'checkbox';
16186 inputType = 'number';
16189 inputType = 'date';
16192 html = html.replace('INPUT_TYPE', inputType);
16194 // In order to fill dropdown options we use:
16195 // - A function/promise or
16196 // - An array inside of row entity if no function exists or
16197 // - A single array for the whole column if none of the previous exists.
16198 var editDropdownOptionsFunction = $scope.col.colDef.editDropdownOptionsFunction;
16199 if (editDropdownOptionsFunction) {
16200 $q.when(editDropdownOptionsFunction($scope.row.entity, $scope.col.colDef))
16201 .then(function(result) {
16202 $scope.editDropdownOptionsArray = result;
16205 var editDropdownRowEntityOptionsArrayPath = $scope.col.colDef.editDropdownRowEntityOptionsArrayPath;
16206 if (editDropdownRowEntityOptionsArrayPath) {
16207 $scope.editDropdownOptionsArray = resolveObjectFromPath($scope.row.entity, editDropdownRowEntityOptionsArrayPath);
16210 $scope.editDropdownOptionsArray = $scope.col.colDef.editDropdownOptionsArray;
16213 $scope.editDropdownIdLabel = $scope.col.colDef.editDropdownIdLabel ? $scope.col.colDef.editDropdownIdLabel : 'id';
16214 $scope.editDropdownValueLabel = $scope.col.colDef.editDropdownValueLabel ? $scope.col.colDef.editDropdownValueLabel : 'value';
16217 var createEditor = function(){
16219 cancelBeginEditEvents();
16220 var cellElement = angular.element(html);
16221 $elm.append(cellElement);
16222 editCellScope = $scope.$new();
16223 $compile(cellElement)(editCellScope);
16224 var gridCellContentsEl = angular.element($elm.children()[0]);
16225 gridCellContentsEl.addClass('ui-grid-cell-contents-hidden');
16227 if (!$rootScope.$$phase) {
16228 $scope.$apply(createEditor);
16233 //stop editing when grid is scrolled
16234 var deregOnGridScroll = $scope.col.grid.api.core.on.scrollBegin($scope, function () {
16235 if ($scope.grid.disableScrolling) {
16239 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
16240 deregOnGridScroll();
16241 deregOnEndCellEdit();
16242 deregOnCancelCellEdit();
16246 var deregOnEndCellEdit = $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
16248 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
16249 deregOnEndCellEdit();
16250 deregOnGridScroll();
16251 deregOnCancelCellEdit();
16255 var deregOnCancelCellEdit = $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
16257 deregOnCancelCellEdit();
16258 deregOnGridScroll();
16259 deregOnEndCellEdit();
16262 $scope.$broadcast(uiGridEditConstants.events.BEGIN_CELL_EDIT, triggerEvent);
16263 $timeout(function () {
16264 //execute in a timeout to give any complex editor templates a cycle to completely render
16265 $scope.grid.api.edit.raise.beginCellEdit($scope.row.entity, $scope.col.colDef, triggerEvent);
16269 function endEdit() {
16270 $scope.grid.disableScrolling = false;
16275 //sometimes the events can't keep up with the keyboard and grid focus is lost, so always focus
16276 //back to grid here. The focus call needs to be before the $destroy and removal of the control,
16277 //otherwise ng-model-options of UpdateOn: 'blur' will not work.
16278 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16279 uiGridCtrl.focus();
16282 var gridCellContentsEl = angular.element($elm.children()[0]);
16283 //remove edit element
16284 editCellScope.$destroy();
16285 angular.element($elm.children()[1]).remove();
16286 gridCellContentsEl.removeClass('ui-grid-cell-contents-hidden');
16288 registerBeginEditEvents();
16289 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.EDIT );
16292 function cancelEdit() {
16293 $scope.grid.disableScrolling = false;
16297 cellModel.assign($scope, origCellValue);
16300 $scope.grid.api.edit.raise.cancelCellEdit($scope.row.entity, $scope.col.colDef);
16304 // resolves a string path against the given object
16305 // shamelessly borrowed from
16306 // http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
16307 function resolveObjectFromPath(object, path) {
16308 path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
16309 path = path.replace(/^\./, ''); // strip a leading dot
16310 var a = path.split('.');
16314 object = object[n];
16328 * @name ui.grid.edit.directive:uiGridEditor
16332 * @description input editor directive for editable fields.
16333 * Provides EndEdit and CancelEdit events
16335 * Events that end editing:
16336 * blur and enter keydown
16338 * Events that cancel editing:
16342 module.directive('uiGridEditor',
16343 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', 'uiGridEditService',
16344 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout, uiGridEditService) {
16347 require: ['?^uiGrid', '?^uiGridRenderContainer', 'ngModel'],
16348 compile: function () {
16350 pre: function ($scope, $elm, $attrs) {
16353 post: function ($scope, $elm, $attrs, controllers) {
16354 var uiGridCtrl, renderContainerCtrl, ngModel;
16355 if (controllers[0]) { uiGridCtrl = controllers[0]; }
16356 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
16357 if (controllers[2]) { ngModel = controllers[2]; }
16359 //set focus at start of edit
16360 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function (evt,triggerEvent) {
16361 $timeout(function () {
16363 //only select text if it is not being replaced below in the cellNav viewPortKeyPress
16364 if ($elm[0].select && $scope.col.colDef.enableCellEditOnFocus || !(uiGridCtrl && uiGridCtrl.grid.api.cellNav)) {
16368 //some browsers (Chrome) stupidly, imo, support the w3 standard that number, email, ...
16369 //fields should not allow setSelectionRange. We ignore the error for those browsers
16370 //https://www.w3.org/Bugs/Public/show_bug.cgi?id=24796
16372 $elm[0].setSelectionRange($elm[0].value.length, $elm[0].value.length);
16380 //set the keystroke that started the edit event
16381 //we must do this because the BeginEdit is done in a different event loop than the intitial
16383 //fire this event for the keypress that is received
16384 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16385 var viewPortKeyDownUnregister = uiGridCtrl.grid.api.cellNav.on.viewPortKeyPress($scope, function (evt, rowCol) {
16386 if (uiGridEditService.isStartEditKey(evt)) {
16387 ngModel.$setViewValue(String.fromCharCode( typeof evt.which === 'number' ? evt.which : evt.keyCode), evt);
16390 viewPortKeyDownUnregister();
16394 $elm.on('blur', function (evt) {
16395 $scope.stopEdit(evt);
16400 $scope.deepEdit = false;
16402 $scope.stopEdit = function (evt) {
16403 if ($scope.inputForm && !$scope.inputForm.$valid) {
16404 evt.stopPropagation();
16405 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16408 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16410 $scope.deepEdit = false;
16414 $elm.on('click', function (evt) {
16415 if ($elm[0].type !== 'checkbox') {
16416 $scope.deepEdit = true;
16417 $timeout(function () {
16418 $scope.grid.disableScrolling = true;
16423 $elm.on('keydown', function (evt) {
16424 switch (evt.keyCode) {
16425 case uiGridConstants.keymap.ESC:
16426 evt.stopPropagation();
16427 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16431 if ($scope.deepEdit &&
16432 (evt.keyCode === uiGridConstants.keymap.LEFT ||
16433 evt.keyCode === uiGridConstants.keymap.RIGHT ||
16434 evt.keyCode === uiGridConstants.keymap.UP ||
16435 evt.keyCode === uiGridConstants.keymap.DOWN)) {
16436 evt.stopPropagation();
16438 // Pass the keydown event off to the cellNav service, if it exists
16439 else if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16440 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
16441 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
16442 $scope.stopEdit(evt);
16446 //handle enter and tab for editing not using cellNav
16447 switch (evt.keyCode) {
16448 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
16449 case uiGridConstants.keymap.TAB:
16450 evt.stopPropagation();
16451 evt.preventDefault();
16452 $scope.stopEdit(evt);
16467 * @name ui.grid.edit.directive:input
16471 * @description directive to provide binding between input[date] value and ng-model for angular 1.2
16472 * It is similar to input[date] directive of angular 1.3
16474 * Supported date format for input is 'yyyy-MM-dd'
16475 * The directive will set the $valid property of input element and the enclosing form to false if
16476 * model is invalid date or value of input is entered wrong.
16479 module.directive('uiGridEditor', ['$filter', function ($filter) {
16480 function parseDateString(dateString) {
16481 if (typeof(dateString) === 'undefined' || dateString === '') {
16484 var parts = dateString.split('-');
16485 if (parts.length !== 3) {
16488 var year = parseInt(parts[0], 10);
16489 var month = parseInt(parts[1], 10);
16490 var day = parseInt(parts[2], 10);
16492 if (month < 1 || year < 1 || day < 1) {
16495 return new Date(year, (month - 1), day);
16498 priority: -100, // run after default uiGridEditor directive
16499 require: '?ngModel',
16500 link: function (scope, element, attrs, ngModel) {
16502 if (angular.version.minor === 2 && attrs.type && attrs.type === 'date' && ngModel) {
16504 ngModel.$formatters.push(function (modelValue) {
16505 ngModel.$setValidity(null,(!modelValue || !isNaN(modelValue.getTime())));
16506 return $filter('date')(modelValue, 'yyyy-MM-dd');
16509 ngModel.$parsers.push(function (viewValue) {
16510 if (viewValue && viewValue.length > 0) {
16511 var dateValue = parseDateString(viewValue);
16512 ngModel.$setValidity(null, (dateValue && !isNaN(dateValue.getTime())));
16516 ngModel.$setValidity(null, true);
16528 * @name ui.grid.edit.directive:uiGridEditDropdown
16532 * @description dropdown editor for editable fields.
16533 * Provides EndEdit and CancelEdit events
16535 * Events that end editing:
16536 * blur and enter keydown, and any left/right nav
16538 * Events that cancel editing:
16542 module.directive('uiGridEditDropdown',
16543 ['uiGridConstants', 'uiGridEditConstants',
16544 function (uiGridConstants, uiGridEditConstants) {
16546 require: ['?^uiGrid', '?^uiGridRenderContainer'],
16548 compile: function () {
16550 pre: function ($scope, $elm, $attrs) {
16553 post: function ($scope, $elm, $attrs, controllers) {
16554 var uiGridCtrl = controllers[0];
16555 var renderContainerCtrl = controllers[1];
16557 //set focus at start of edit
16558 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16560 $elm[0].style.width = ($elm[0].parentElement.offsetWidth - 1) + 'px';
16561 $elm.on('blur', function (evt) {
16562 $scope.stopEdit(evt);
16567 $scope.stopEdit = function (evt) {
16568 // no need to validate a dropdown - invalid values shouldn't be
16569 // available in the list
16570 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16573 $elm.on('keydown', function (evt) {
16574 switch (evt.keyCode) {
16575 case uiGridConstants.keymap.ESC:
16576 evt.stopPropagation();
16577 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16580 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16581 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
16582 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
16583 $scope.stopEdit(evt);
16587 //handle enter and tab for editing not using cellNav
16588 switch (evt.keyCode) {
16589 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
16590 case uiGridConstants.keymap.TAB:
16591 evt.stopPropagation();
16592 evt.preventDefault();
16593 $scope.stopEdit(evt);
16607 * @name ui.grid.edit.directive:uiGridEditFileChooser
16611 * @description input editor directive for editable fields.
16612 * Provides EndEdit and CancelEdit events
16614 * Events that end editing:
16615 * blur and enter keydown
16617 * Events that cancel editing:
16621 module.directive('uiGridEditFileChooser',
16622 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout',
16623 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) {
16626 require: ['?^uiGrid', '?^uiGridRenderContainer'],
16627 compile: function () {
16629 pre: function ($scope, $elm, $attrs) {
16632 post: function ($scope, $elm, $attrs, controllers) {
16633 var uiGridCtrl, renderContainerCtrl;
16634 if (controllers[0]) { uiGridCtrl = controllers[0]; }
16635 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
16636 var grid = uiGridCtrl.grid;
16638 var handleFileSelect = function( event ){
16639 var target = event.srcElement || event.target;
16641 if (target && target.files && target.files.length > 0) {
16644 * @name editFileChooserCallback
16645 * @propertyOf ui.grid.edit.api:ColumnDef
16646 * @description A function that should be called when any files have been chosen
16647 * by the user. You should use this to process the files appropriately for your
16650 * It passes the gridCol, the gridRow (from which you can get gridRow.entity),
16651 * and the files. The files are in the format as returned from the file chooser,
16652 * an array of files, with each having useful information such as:
16653 * - `files[0].lastModifiedDate`
16654 * - `files[0].name`
16655 * - `files[0].size` (appears to be in bytes)
16656 * - `files[0].type` (MIME type by the looks)
16658 * Typically you would do something with these files - most commonly you would
16659 * use the filename or read the file itself in. The example function does both.
16663 * editFileChooserCallBack: function(gridRow, gridCol, files ){
16664 * // ignore all but the first file, it can only choose one anyway
16665 * // set the filename into this column
16666 * gridRow.entity.filename = file[0].name;
16668 * // read the file and set it into a hidden column, which we may do stuff with later
16669 * var setFile = function(fileContent){
16670 * gridRow.entity.file = fileContent.currentTarget.result;
16672 * var reader = new FileReader();
16673 * reader.onload = setFile;
16674 * reader.readAsText( files[0] );
16678 if ( typeof($scope.col.colDef.editFileChooserCallback) === 'function' ) {
16679 $scope.col.colDef.editFileChooserCallback($scope.row, $scope.col, target.files);
16681 gridUtil.logError('You need to set colDef.editFileChooserCallback to use the file chooser');
16684 target.form.reset();
16685 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16687 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16691 $elm[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
16693 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16697 $elm.on('blur', function (evt) {
16698 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16715 * @name ui.grid.expandable
16718 * # ui.grid.expandable
16720 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
16722 * This module provides the ability to create subgrids with the ability to expand a row
16723 * to show the subgrid.
16725 * <div doc-module-components="ui.grid.expandable"></div>
16727 var module = angular.module('ui.grid.expandable', ['ui.grid']);
16731 * @name ui.grid.expandable.service:uiGridExpandableService
16733 * @description Services for the expandable grid
16735 module.service('uiGridExpandableService', ['gridUtil', '$compile', function (gridUtil, $compile) {
16737 initializeGrid: function (grid) {
16739 grid.expandable = {};
16740 grid.expandable.expandedAll = false;
16744 * @name enableExpandable
16745 * @propertyOf ui.grid.expandable.api:GridOptions
16746 * @description Whether or not to use expandable feature, allows you to turn off expandable on specific grids
16747 * within your application, or in specific modes on _this_ grid. Defaults to true.
16750 * $scope.gridOptions = {
16751 * enableExpandable: false
16755 grid.options.enableExpandable = grid.options.enableExpandable !== false;
16759 * @name expandableRowHeight
16760 * @propertyOf ui.grid.expandable.api:GridOptions
16761 * @description Height in pixels of the expanded subgrid. Defaults to
16765 * $scope.gridOptions = {
16766 * expandableRowHeight: 150
16770 grid.options.expandableRowHeight = grid.options.expandableRowHeight || 150;
16775 * @propertyOf ui.grid.expandable.api:GridOptions
16776 * @description Width in pixels of the expandable column. Defaults to 40
16779 * $scope.gridOptions = {
16780 * expandableRowHeaderWidth: 40
16784 grid.options.expandableRowHeaderWidth = grid.options.expandableRowHeaderWidth || 40;
16788 * @name expandableRowTemplate
16789 * @propertyOf ui.grid.expandable.api:GridOptions
16790 * @description Mandatory. The template for your expanded row
16793 * $scope.gridOptions = {
16794 * expandableRowTemplate: 'expandableRowTemplate.html'
16798 if ( grid.options.enableExpandable && !grid.options.expandableRowTemplate ){
16799 gridUtil.logError( 'You have not set the expandableRowTemplate, disabling expandable module' );
16800 grid.options.enableExpandable = false;
16805 * @name ui.grid.expandable.api:PublicApi
16807 * @description Public Api for expandable feature
16811 * @name ui.grid.expandable.api:GridRow
16813 * @description Additional properties added to GridRow when using the expandable module
16817 * @name ui.grid.expandable.api:GridOptions
16819 * @description Options for configuring the expandable feature, these are available to be
16820 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
16827 * @name rowExpandedStateChanged
16828 * @eventOf ui.grid.expandable.api:PublicApi
16829 * @description raised when cell editing is complete
16831 * gridApi.expandable.on.rowExpandedStateChanged(scope,function(row){})
16833 * @param {GridRow} row the row that was expanded
16835 rowExpandedBeforeStateChanged: function(scope,row){
16837 rowExpandedStateChanged: function (scope, row) {
16846 * @name toggleRowExpansion
16847 * @methodOf ui.grid.expandable.api:PublicApi
16848 * @description Toggle a specific row
16850 * gridApi.expandable.toggleRowExpansion(rowEntity);
16852 * @param {object} rowEntity the data entity for the row you want to expand
16854 toggleRowExpansion: function (rowEntity) {
16855 var row = grid.getRow(rowEntity);
16856 if (row !== null) {
16857 service.toggleRowExpansion(grid, row);
16863 * @name expandAllRows
16864 * @methodOf ui.grid.expandable.api:PublicApi
16865 * @description Expand all subgrids.
16867 * gridApi.expandable.expandAllRows();
16870 expandAllRows: function() {
16871 service.expandAllRows(grid);
16876 * @name collapseAllRows
16877 * @methodOf ui.grid.expandable.api:PublicApi
16878 * @description Collapse all subgrids.
16880 * gridApi.expandable.collapseAllRows();
16883 collapseAllRows: function() {
16884 service.collapseAllRows(grid);
16889 * @name toggleAllRows
16890 * @methodOf ui.grid.expandable.api:PublicApi
16891 * @description Toggle all subgrids.
16893 * gridApi.expandable.toggleAllRows();
16896 toggleAllRows: function() {
16897 service.toggleAllRows(grid);
16902 * @methodOf ui.grid.expandable.api:PublicApi
16903 * @description Expand the data row
16904 * @param {object} rowEntity gridOptions.data[] array instance
16906 expandRow: function (rowEntity) {
16907 var row = grid.getRow(rowEntity);
16908 if (row !== null && !row.isExpanded) {
16909 service.toggleRowExpansion(grid, row);
16914 * @name collapseRow
16915 * @methodOf ui.grid.expandable.api:PublicApi
16916 * @description Collapse the data row
16917 * @param {object} rowEntity gridOptions.data[] array instance
16919 collapseRow: function (rowEntity) {
16920 var row = grid.getRow(rowEntity);
16921 if (row !== null && row.isExpanded) {
16922 service.toggleRowExpansion(grid, row);
16927 * @name getExpandedRows
16928 * @methodOf ui.grid.expandable.api:PublicApi
16929 * @description returns all expandedRow's entity references
16931 getExpandedRows: function () {
16932 return service.getExpandedRows(grid).map(function (gridRow) {
16933 return gridRow.entity;
16939 grid.api.registerEventsFromObject(publicApi.events);
16940 grid.api.registerMethodsFromObject(publicApi.methods);
16943 toggleRowExpansion: function (grid, row) {
16944 // trigger the "before change" event. Can change row height dynamically this way.
16945 grid.api.expandable.raise.rowExpandedBeforeStateChanged(row);
16949 * @propertyOf ui.grid.expandable.api:GridRow
16950 * @description Whether or not the row is currently expanded.
16953 * $scope.api.expandable.on.rowExpandedStateChanged($scope, function (row) {
16954 * if (row.isExpanded) {
16960 row.isExpanded = !row.isExpanded;
16961 if (angular.isUndefined(row.expandedRowHeight)){
16962 row.expandedRowHeight = grid.options.expandableRowHeight;
16965 if (row.isExpanded) {
16966 row.height = row.grid.options.rowHeight + row.expandedRowHeight;
16969 row.height = row.grid.options.rowHeight;
16970 grid.expandable.expandedAll = false;
16972 grid.api.expandable.raise.rowExpandedStateChanged(row);
16975 expandAllRows: function(grid, $scope) {
16976 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16977 if (!row.isExpanded) {
16978 service.toggleRowExpansion(grid, row);
16981 grid.expandable.expandedAll = true;
16982 grid.queueGridRefresh();
16985 collapseAllRows: function(grid) {
16986 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16987 if (row.isExpanded) {
16988 service.toggleRowExpansion(grid, row);
16991 grid.expandable.expandedAll = false;
16992 grid.queueGridRefresh();
16995 toggleAllRows: function(grid) {
16996 if (grid.expandable.expandedAll) {
16997 service.collapseAllRows(grid);
17000 service.expandAllRows(grid);
17004 getExpandedRows: function (grid) {
17005 return grid.rows.filter(function (row) {
17006 return row.isExpanded;
17015 * @name enableExpandableRowHeader
17016 * @propertyOf ui.grid.expandable.api:GridOptions
17017 * @description Show a rowHeader to provide the expandable buttons. If set to false then implies
17018 * you're going to use a custom method for expanding and collapsing the subgrids. Defaults to true.
17021 * $scope.gridOptions = {
17022 * enableExpandableRowHeader: false
17026 module.directive('uiGridExpandable', ['uiGridExpandableService', '$templateCache',
17027 function (uiGridExpandableService, $templateCache) {
17031 require: '^uiGrid',
17033 compile: function () {
17035 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
17036 if ( uiGridCtrl.grid.options.enableExpandableRowHeader !== false ) {
17037 var expandableRowHeaderColDef = {
17038 name: 'expandableButtons',
17040 exporterSuppressExport: true,
17041 enableColumnResizing: false,
17042 enableColumnMenu: false,
17043 width: uiGridCtrl.grid.options.expandableRowHeaderWidth || 40
17045 expandableRowHeaderColDef.cellTemplate = $templateCache.get('ui-grid/expandableRowHeader');
17046 expandableRowHeaderColDef.headerCellTemplate = $templateCache.get('ui-grid/expandableTopRowHeader');
17047 uiGridCtrl.grid.addRowHeaderColumn(expandableRowHeaderColDef);
17049 uiGridExpandableService.initializeGrid(uiGridCtrl.grid);
17051 post: function ($scope, $elm, $attrs, uiGridCtrl) {
17060 * @name ui.grid.expandable.directive:uiGrid
17061 * @description stacks on the uiGrid directive to register child grid with parent row when child is created
17063 module.directive('uiGrid', ['uiGridExpandableService', '$templateCache',
17064 function (uiGridExpandableService, $templateCache) {
17068 require: '^uiGrid',
17070 compile: function () {
17072 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
17074 uiGridCtrl.grid.api.core.on.renderingComplete($scope, function() {
17075 //if a parent grid row is on the scope, then add the parentRow property to this childGrid
17076 if ($scope.row && $scope.row.grid && $scope.row.grid.options && $scope.row.grid.options.enableExpandable) {
17080 * @name ui.grid.expandable.class:Grid
17081 * @description Additional Grid properties added by expandable module
17087 * @propertyOf ui.grid.expandable.class:Grid
17088 * @description reference to the expanded parent row that owns this grid
17090 uiGridCtrl.grid.parentRow = $scope.row;
17092 //todo: adjust height on parent row when child grid height changes. we need some sort of gridHeightChanged event
17093 // uiGridCtrl.grid.core.on.canvasHeightChanged($scope, function(oldHeight, newHeight) {
17094 // uiGridCtrl.grid.parentRow = newHeight;
17100 post: function ($scope, $elm, $attrs, uiGridCtrl) {
17110 * @name ui.grid.expandable.directive:uiGridExpandableRow
17111 * @description directive to render the expandable row template
17113 module.directive('uiGridExpandableRow',
17114 ['uiGridExpandableService', '$timeout', '$compile', 'uiGridConstants','gridUtil','$interval', '$log',
17115 function (uiGridExpandableService, $timeout, $compile, uiGridConstants, gridUtil, $interval, $log) {
17122 compile: function () {
17124 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
17125 gridUtil.getTemplate($scope.grid.options.expandableRowTemplate).then(
17126 function (template) {
17127 if ($scope.grid.options.expandableRowScope) {
17128 var expandableRowScope = $scope.grid.options.expandableRowScope;
17129 for (var property in expandableRowScope) {
17130 if (expandableRowScope.hasOwnProperty(property)) {
17131 $scope[property] = expandableRowScope[property];
17135 var expandedRowElement = $compile(template)($scope);
17136 $elm.append(expandedRowElement);
17137 $scope.row.expandedRendered = true;
17141 post: function ($scope, $elm, $attrs, uiGridCtrl) {
17142 $scope.$on('$destroy', function() {
17143 $scope.row.expandedRendered = false;
17153 * @name ui.grid.expandable.directive:uiGridRow
17154 * @description stacks on the uiGridRow directive to add support for expandable rows
17156 module.directive('uiGridRow',
17157 ['$compile', 'gridUtil', '$templateCache',
17158 function ($compile, gridUtil, $templateCache) {
17162 compile: function ($elm, $attrs) {
17164 pre: function ($scope, $elm, $attrs, controllers) {
17166 $scope.expandableRow = {};
17168 $scope.expandableRow.shouldRenderExpand = function () {
17169 var ret = $scope.colContainer.name === 'body' && $scope.grid.options.enableExpandable !== false && $scope.row.isExpanded && (!$scope.grid.isScrollingVertically || $scope.row.expandedRendered);
17173 $scope.expandableRow.shouldRenderFiller = function () {
17174 var ret = $scope.row.isExpanded && ( $scope.colContainer.name !== 'body' || ($scope.grid.isScrollingVertically && !$scope.row.expandedRendered));
17179 * Commented out @PaulL1. This has no purpose that I can see, and causes #2964. If this code needs to be reinstated for some
17180 * reason it needs to use drawnWidth, not width, and needs to check column visibility. It should really use render container
17181 * visible column cache also instead of checking column.renderContainer.
17182 function updateRowContainerWidth() {
17183 var grid = $scope.grid;
17185 grid.columns.forEach( function (column) {
17186 if (column.renderContainer === 'left') {
17187 colWidth += column.width;
17190 colWidth = Math.floor(colWidth);
17191 return '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.colContainer.name + ', .grid' + grid.id +
17192 ' .ui-grid-pinned-container-' + $scope.colContainer.name + ' .ui-grid-render-container-' + $scope.colContainer.name +
17193 ' .ui-grid-viewport .ui-grid-canvas .ui-grid-row { width: ' + colWidth + 'px; }';
17196 if ($scope.colContainer.name === 'left') {
17197 $scope.grid.registerStyleComputation({
17199 func: updateRowContainerWidth
17204 post: function ($scope, $elm, $attrs, controllers) {
17213 * @name ui.grid.expandable.directive:uiGridViewport
17214 * @description stacks on the uiGridViewport directive to append the expandable row html elements to the
17215 * default gridRow template
17217 module.directive('uiGridViewport',
17218 ['$compile', 'gridUtil', '$templateCache',
17219 function ($compile, gridUtil, $templateCache) {
17223 compile: function ($elm, $attrs) {
17224 var rowRepeatDiv = angular.element($elm.children().children()[0]);
17225 var expandedRowFillerElement = $templateCache.get('ui-grid/expandableScrollFiller');
17226 var expandedRowElement = $templateCache.get('ui-grid/expandableRow');
17227 rowRepeatDiv.append(expandedRowElement);
17228 rowRepeatDiv.append(expandedRowFillerElement);
17230 pre: function ($scope, $elm, $attrs, controllers) {
17232 post: function ($scope, $elm, $attrs, controllers) {
17241 /* global console */
17248 * @name ui.grid.exporter
17251 * # ui.grid.exporter
17253 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
17255 * This module provides the ability to export data from the grid.
17257 * Data can be exported in a range of formats, and all data, visible
17258 * data, or selected rows can be exported, with all columns or visible
17261 * No UI is provided, the caller should provide their own UI/buttons
17262 * as appropriate, or enable the gridMenu
17267 * <div doc-module-components="ui.grid.exporter"></div>
17270 var module = angular.module('ui.grid.exporter', ['ui.grid']);
17274 * @name ui.grid.exporter.constant:uiGridExporterConstants
17276 * @description constants available in exporter module
17280 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
17282 * @description export all data, including data not visible. Can
17283 * be set for either rowTypes or colTypes
17287 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
17289 * @description export only visible data, including data not visible. Can
17290 * be set for either rowTypes or colTypes
17294 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
17296 * @description export all data, including data not visible. Can
17297 * be set only for rowTypes, selection of only some columns is
17300 module.constant('uiGridExporterConstants', {
17301 featureName: 'exporter',
17303 VISIBLE: 'visible',
17304 SELECTED: 'selected',
17305 CSV_CONTENT: 'CSV_CONTENT',
17306 BUTTON_LABEL: 'BUTTON_LABEL',
17307 FILE_NAME: 'FILE_NAME'
17312 * @name ui.grid.exporter.service:uiGridExporterService
17314 * @description Services for exporter feature
17316 module.service('uiGridExporterService', ['$q', 'uiGridExporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService',
17317 function ($q, uiGridExporterConstants, gridUtil, $compile, $interval, i18nService) {
17323 initializeGrid: function (grid) {
17325 //add feature namespace and any properties to grid for needed state
17326 grid.exporter = {};
17327 this.defaultGridOptions(grid.options);
17331 * @name ui.grid.exporter.api:PublicApi
17333 * @description Public Api for exporter feature
17345 * @methodOf ui.grid.exporter.api:PublicApi
17346 * @description Exports rows from the grid in csv format,
17347 * the data exported is selected based on the provided options
17348 * @param {string} rowTypes which rows to export, valid values are
17349 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17350 * uiGridExporterConstants.SELECTED
17351 * @param {string} colTypes which columns to export, valid values are
17352 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
17354 csvExport: function (rowTypes, colTypes) {
17355 service.csvExport(grid, rowTypes, colTypes);
17360 * @methodOf ui.grid.exporter.api:PublicApi
17361 * @description Exports rows from the grid in pdf format,
17362 * the data exported is selected based on the provided options
17363 * Note that this function has a dependency on pdfMake, all
17364 * going well this has been installed for you.
17365 * The resulting pdf opens in a new browser window.
17366 * @param {string} rowTypes which rows to export, valid values are
17367 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17368 * uiGridExporterConstants.SELECTED
17369 * @param {string} colTypes which columns to export, valid values are
17370 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
17372 pdfExport: function (rowTypes, colTypes) {
17373 service.pdfExport(grid, rowTypes, colTypes);
17379 grid.api.registerEventsFromObject(publicApi.events);
17381 grid.api.registerMethodsFromObject(publicApi.methods);
17383 if (grid.api.core.addToGridMenu){
17384 service.addToMenu( grid );
17386 // order of registration is not guaranteed, register in a little while
17387 $interval( function() {
17388 if (grid.api.core.addToGridMenu){
17389 service.addToMenu( grid );
17396 defaultGridOptions: function (gridOptions) {
17397 //default option to true unless it was explicitly set to false
17400 * @name ui.grid.exporter.api:GridOptions
17402 * @description GridOptions for exporter feature, these are available to be
17403 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
17407 * @name ui.grid.exporter.api:ColumnDef
17408 * @description ColumnDef settings for exporter
17412 * @name exporterSuppressMenu
17413 * @propertyOf ui.grid.exporter.api:GridOptions
17414 * @description Don't show the export menu button, implying the user
17415 * will roll their own UI for calling the exporter
17416 * <br/>Defaults to false
17418 gridOptions.exporterSuppressMenu = gridOptions.exporterSuppressMenu === true;
17421 * @name exporterMenuLabel
17422 * @propertyOf ui.grid.exporter.api:GridOptions
17423 * @description The text to show on the exporter menu button
17425 * <br/>Defaults to 'Export'
17427 gridOptions.exporterMenuLabel = gridOptions.exporterMenuLabel ? gridOptions.exporterMenuLabel : 'Export';
17430 * @name exporterSuppressColumns
17431 * @propertyOf ui.grid.exporter.api:GridOptions
17432 * @description Columns that should not be exported. The selectionRowHeader is already automatically
17433 * suppressed, but if you had a button column or some other "system" column that shouldn't be shown in the
17434 * output then add it in this list. You should provide an array of column names.
17435 * <br/>Defaults to: []
17437 * gridOptions.exporterSuppressColumns = [ 'buttons' ];
17440 gridOptions.exporterSuppressColumns = gridOptions.exporterSuppressColumns ? gridOptions.exporterSuppressColumns : [];
17443 * @name exporterCsvColumnSeparator
17444 * @propertyOf ui.grid.exporter.api:GridOptions
17445 * @description The character to use as column separator
17447 * <br/>Defaults to ','
17449 gridOptions.exporterCsvColumnSeparator = gridOptions.exporterCsvColumnSeparator ? gridOptions.exporterCsvColumnSeparator : ',';
17452 * @name exporterCsvFilename
17453 * @propertyOf ui.grid.exporter.api:GridOptions
17454 * @description The default filename to use when saving the downloaded csv.
17455 * This will only work in some browsers.
17456 * <br/>Defaults to 'download.csv'
17458 gridOptions.exporterCsvFilename = gridOptions.exporterCsvFilename ? gridOptions.exporterCsvFilename : 'download.csv';
17461 * @name exporterPdfFilename
17462 * @propertyOf ui.grid.exporter.api:GridOptions
17463 * @description The default filename to use when saving the downloaded pdf, only used in IE (other browsers open pdfs in a new window)
17464 * <br/>Defaults to 'download.pdf'
17466 gridOptions.exporterPdfFilename = gridOptions.exporterPdfFilename ? gridOptions.exporterPdfFilename : 'download.pdf';
17469 * @name exporterOlderExcelCompatibility
17470 * @propertyOf ui.grid.exporter.api:GridOptions
17471 * @description Some versions of excel don't like the utf-16 BOM on the front, and it comes
17472 * through as  in the first column header. Setting this option to false will suppress this, at the
17473 * expense of proper utf-16 handling in applications that do recognise the BOM
17474 * <br/>Defaults to false
17476 gridOptions.exporterOlderExcelCompatibility = gridOptions.exporterOlderExcelCompatibility === true;
17479 * @name exporterPdfDefaultStyle
17480 * @propertyOf ui.grid.exporter.api:GridOptions
17481 * @description The default style in pdfMake format
17482 * <br/>Defaults to:
17489 gridOptions.exporterPdfDefaultStyle = gridOptions.exporterPdfDefaultStyle ? gridOptions.exporterPdfDefaultStyle : { fontSize: 11 };
17492 * @name exporterPdfTableStyle
17493 * @propertyOf ui.grid.exporter.api:GridOptions
17494 * @description The table style in pdfMake format
17495 * <br/>Defaults to:
17498 * margin: [0, 5, 0, 15]
17502 gridOptions.exporterPdfTableStyle = gridOptions.exporterPdfTableStyle ? gridOptions.exporterPdfTableStyle : { margin: [0, 5, 0, 15] };
17505 * @name exporterPdfTableHeaderStyle
17506 * @propertyOf ui.grid.exporter.api:GridOptions
17507 * @description The tableHeader style in pdfMake format
17508 * <br/>Defaults to:
17517 gridOptions.exporterPdfTableHeaderStyle = gridOptions.exporterPdfTableHeaderStyle ? gridOptions.exporterPdfTableHeaderStyle : { bold: true, fontSize: 12, color: 'black' };
17520 * @name exporterPdfHeader
17521 * @propertyOf ui.grid.exporter.api:GridOptions
17522 * @description The header section for pdf exports. Can be
17525 * gridOptions.exporterPdfHeader = 'My Header';
17527 * Can be a more complex object in pdfMake format:
17529 * gridOptions.exporterPdfHeader = {
17532 * { text: 'Right part', alignment: 'right' }
17536 * Or can be a function, allowing page numbers and the like
17538 * gridOptions.exporterPdfHeader: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
17541 gridOptions.exporterPdfHeader = gridOptions.exporterPdfHeader ? gridOptions.exporterPdfHeader : null;
17544 * @name exporterPdfFooter
17545 * @propertyOf ui.grid.exporter.api:GridOptions
17546 * @description The header section for pdf exports. Can be
17549 * gridOptions.exporterPdfFooter = 'My Footer';
17551 * Can be a more complex object in pdfMake format:
17553 * gridOptions.exporterPdfFooter = {
17556 * { text: 'Right part', alignment: 'right' }
17560 * Or can be a function, allowing page numbers and the like
17562 * gridOptions.exporterPdfFooter: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
17565 gridOptions.exporterPdfFooter = gridOptions.exporterPdfFooter ? gridOptions.exporterPdfFooter : null;
17568 * @name exporterPdfOrientation
17569 * @propertyOf ui.grid.exporter.api:GridOptions
17570 * @description The orientation, should be a valid pdfMake value,
17571 * 'landscape' or 'portrait'
17572 * <br/>Defaults to landscape
17574 gridOptions.exporterPdfOrientation = gridOptions.exporterPdfOrientation ? gridOptions.exporterPdfOrientation : 'landscape';
17577 * @name exporterPdfPageSize
17578 * @propertyOf ui.grid.exporter.api:GridOptions
17579 * @description The orientation, should be a valid pdfMake
17580 * paper size, usually 'A4' or 'LETTER'
17581 * {@link https://github.com/bpampuch/pdfmake/blob/master/src/standardPageSizes.js pdfMake page sizes}
17582 * <br/>Defaults to A4
17584 gridOptions.exporterPdfPageSize = gridOptions.exporterPdfPageSize ? gridOptions.exporterPdfPageSize : 'A4';
17587 * @name exporterPdfMaxGridWidth
17588 * @propertyOf ui.grid.exporter.api:GridOptions
17589 * @description The maxium grid width - the current grid width
17590 * will be scaled to match this, with any fixed width columns
17591 * being adjusted accordingly.
17592 * <br/>Defaults to 720 (for A4 landscape), use 670 for LETTER
17594 gridOptions.exporterPdfMaxGridWidth = gridOptions.exporterPdfMaxGridWidth ? gridOptions.exporterPdfMaxGridWidth : 720;
17597 * @name exporterPdfTableLayout
17598 * @propertyOf ui.grid.exporter.api:GridOptions
17599 * @description A tableLayout in pdfMake format,
17600 * controls gridlines and the like. We use the default
17602 * <br/>Defaults to null, which means no layout
17607 * @name exporterMenuAllData
17608 * @porpertyOf ui.grid.exporter.api:GridOptions
17609 * @description Add export all data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
17611 gridOptions.exporterMenuAllData = gridOptions.exporterMenuAllData !== undefined ? gridOptions.exporterMenuAllData : true;
17615 * @name exporterMenuVisibleData
17616 * @porpertyOf ui.grid.exporter.api:GridOptions
17617 * @description Add export visible data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
17619 gridOptions.exporterMenuVisibleData = gridOptions.exporterMenuVisibleData !== undefined ? gridOptions.exporterMenuVisibleData : true;
17623 * @name exporterMenuSelectedData
17624 * @porpertyOf ui.grid.exporter.api:GridOptions
17625 * @description Add export selected data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
17627 gridOptions.exporterMenuSelectedData = gridOptions.exporterMenuSelectedData !== undefined ? gridOptions.exporterMenuSelectedData : true;
17631 * @name exporterMenuCsv
17632 * @propertyOf ui.grid.exporter.api:GridOptions
17633 * @description Add csv export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17635 gridOptions.exporterMenuCsv = gridOptions.exporterMenuCsv !== undefined ? gridOptions.exporterMenuCsv : true;
17639 * @name exporterMenuPdf
17640 * @propertyOf ui.grid.exporter.api:GridOptions
17641 * @description Add pdf export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17643 gridOptions.exporterMenuPdf = gridOptions.exporterMenuPdf !== undefined ? gridOptions.exporterMenuPdf : true;
17647 * @name exporterPdfCustomFormatter
17648 * @propertyOf ui.grid.exporter.api:GridOptions
17649 * @description A custom callback routine that changes the pdf document, adding any
17650 * custom styling or content that is supported by pdfMake. Takes in the complete docDefinition, and
17651 * must return an updated docDefinition ready for pdfMake.
17653 * In this example we add a style to the style array, so that we can use it in our
17654 * footer definition.
17656 * gridOptions.exporterPdfCustomFormatter = function ( docDefinition ) {
17657 * docDefinition.styles.footerStyle = { bold: true, fontSize: 10 };
17658 * return docDefinition;
17661 * gridOptions.exporterPdfFooter = { text: 'My footer', style: 'footerStyle' }
17664 gridOptions.exporterPdfCustomFormatter = ( gridOptions.exporterPdfCustomFormatter && typeof( gridOptions.exporterPdfCustomFormatter ) === 'function' ) ? gridOptions.exporterPdfCustomFormatter : function ( docDef ) { return docDef; };
17668 * @name exporterHeaderFilterUseName
17669 * @propertyOf ui.grid.exporter.api:GridOptions
17670 * @description Defaults to false, which leads to `displayName` being passed into the headerFilter.
17671 * If set to true, then will pass `name` instead.
17676 * gridOptions.exporterHeaderFilterUseName = true;
17679 gridOptions.exporterHeaderFilterUseName = gridOptions.exporterHeaderFilterUseName === true;
17683 * @name exporterHeaderFilter
17684 * @propertyOf ui.grid.exporter.api:GridOptions
17685 * @description A function to apply to the header displayNames before exporting. Useful for internationalisation,
17686 * for example if you were using angular-translate you'd set this to `$translate.instant`. Note that this
17687 * call must be synchronous, it cannot be a call that returns a promise.
17689 * Behaviour can be changed to pass in `name` instead of `displayName` through use of `exporterHeaderFilterUseName: true`.
17693 * gridOptions.exporterHeaderFilter = function( displayName ){ return 'col: ' + name; };
17697 * gridOptions.exporterHeaderFilter = $translate.instant;
17703 * @name exporterFieldCallback
17704 * @propertyOf ui.grid.exporter.api:GridOptions
17705 * @description A function to call for each field before exporting it. Allows
17706 * massaging of raw data into a display format, for example if you have applied
17707 * filters to convert codes into decodes, or you require
17708 * a specific date format in the exported content.
17710 * The method is called once for each field exported, and provides the grid, the
17711 * gridCol and the GridRow for you to use as context in massaging the data.
17713 * @param {Grid} grid provides the grid in case you have need of it
17714 * @param {GridRow} row the row from which the data comes
17715 * @param {GridCol} col the column from which the data comes
17716 * @param {object} value the value for your massaging
17717 * @returns {object} you must return the massaged value ready for exporting
17721 * gridOptions.exporterFieldCallback = function ( grid, row, col, value ){
17722 * if ( col.name === 'status' ){
17723 * value = decodeStatus( value );
17729 gridOptions.exporterFieldCallback = gridOptions.exporterFieldCallback ? gridOptions.exporterFieldCallback : function( grid, row, col, value ) { return value; };
17733 * @name exporterAllDataFn
17734 * @propertyOf ui.grid.exporter.api:GridOptions
17735 * @description This promise is needed when exporting all rows,
17736 * and the data need to be provided by server side. Default is null.
17737 * @returns {Promise} a promise to load all data from server
17741 * gridOptions.exporterAllDataFn = function () {
17742 * return $http.get('/data/100.json')
17746 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataFn ? gridOptions.exporterAllDataFn : null;
17750 * @name exporterAllDataPromise
17751 * @propertyOf ui.grid.exporter.api:GridOptions
17752 * @description DEPRECATED - exporterAllDataFn used to be
17753 * called this, but it wasn't a promise, it was a function that returned
17754 * a promise. Deprecated, but supported for backward compatibility, use
17755 * exporterAllDataFn instead.
17756 * @returns {Promise} a promise to load all data from server
17760 * gridOptions.exporterAllDataFn = function () {
17761 * return $http.get('/data/100.json')
17765 if ( gridOptions.exporterAllDataFn == null && gridOptions.exporterAllDataPromise ) {
17766 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataPromise;
17774 * @methodOf ui.grid.exporter.service:uiGridExporterService
17775 * @description Adds export items to the grid menu,
17776 * allowing the user to select export options
17777 * @param {Grid} grid the grid from which data should be exported
17779 addToMenu: function ( grid ) {
17780 grid.api.core.addToGridMenu( grid, [
17782 title: i18nService.getSafeText('gridMenu.exporterAllAsCsv'),
17783 action: function ($event) {
17784 this.grid.api.exporter.csvExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17786 shown: function() {
17787 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuAllData;
17792 title: i18nService.getSafeText('gridMenu.exporterVisibleAsCsv'),
17793 action: function ($event) {
17794 this.grid.api.exporter.csvExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17796 shown: function() {
17797 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuVisibleData;
17802 title: i18nService.getSafeText('gridMenu.exporterSelectedAsCsv'),
17803 action: function ($event) {
17804 this.grid.api.exporter.csvExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17806 shown: function() {
17807 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuSelectedData &&
17808 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17813 title: i18nService.getSafeText('gridMenu.exporterAllAsPdf'),
17814 action: function ($event) {
17815 this.grid.api.exporter.pdfExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17817 shown: function() {
17818 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuAllData;
17823 title: i18nService.getSafeText('gridMenu.exporterVisibleAsPdf'),
17824 action: function ($event) {
17825 this.grid.api.exporter.pdfExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17827 shown: function() {
17828 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuVisibleData;
17833 title: i18nService.getSafeText('gridMenu.exporterSelectedAsPdf'),
17834 action: function ($event) {
17835 this.grid.api.exporter.pdfExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17837 shown: function() {
17838 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuSelectedData &&
17839 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17850 * @methodOf ui.grid.exporter.service:uiGridExporterService
17851 * @description Exports rows from the grid in csv format,
17852 * the data exported is selected based on the provided options
17853 * @param {Grid} grid the grid from which data should be exported
17854 * @param {string} rowTypes which rows to export, valid values are
17855 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17856 * uiGridExporterConstants.SELECTED
17857 * @param {string} colTypes which columns to export, valid values are
17858 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17859 * uiGridExporterConstants.SELECTED
17861 csvExport: function (grid, rowTypes, colTypes) {
17863 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function() {
17864 var exportColumnHeaders = grid.options.showHeader ? self.getColumnHeaders(grid, colTypes) : [];
17865 var exportData = self.getData(grid, rowTypes, colTypes);
17866 var csvContent = self.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator);
17868 self.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility);
17874 * @name loadAllDataIfNeeded
17875 * @methodOf ui.grid.exporter.service:uiGridExporterService
17876 * @description When using server side pagination, use exporterAllDataFn to
17877 * load all data before continuing processing.
17878 * When using client side pagination, return a resolved promise so processing
17879 * continues immediately
17880 * @param {Grid} grid the grid from which data should be exported
17881 * @param {string} rowTypes which rows to export, valid values are
17882 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17883 * uiGridExporterConstants.SELECTED
17884 * @param {string} colTypes which columns to export, valid values are
17885 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17886 * uiGridExporterConstants.SELECTED
17888 loadAllDataIfNeeded: function (grid, rowTypes, colTypes) {
17889 if ( rowTypes === uiGridExporterConstants.ALL && grid.rows.length !== grid.options.totalItems && grid.options.exporterAllDataFn) {
17890 return grid.options.exporterAllDataFn()
17892 grid.modifyRows(grid.options.data);
17895 var deferred = $q.defer();
17896 deferred.resolve();
17897 return deferred.promise;
17903 * @propertyOf ui.grid.exporter.api:ColumnDef
17904 * @name exporterSuppressExport
17905 * @description Suppresses export for this column. Used by selection and expandable.
17910 * @name getColumnHeaders
17911 * @methodOf ui.grid.exporter.service:uiGridExporterService
17912 * @description Gets the column headers from the grid to use
17913 * as a title row for the exported file, all headers have
17914 * headerCellFilters applied as appropriate.
17916 * Column headers are an array of objects, each object has
17917 * name, displayName, width and align attributes. Only name is
17918 * used for csv, all attributes are used for pdf.
17920 * @param {Grid} grid the grid from which data should be exported
17921 * @param {string} colTypes which columns to export, valid values are
17922 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17923 * uiGridExporterConstants.SELECTED
17925 getColumnHeaders: function (grid, colTypes) {
17929 if ( colTypes === uiGridExporterConstants.ALL ){
17930 columns = grid.columns;
17932 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17933 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17934 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17936 columns = leftColumns.concat(bodyColumns,rightColumns);
17939 columns.forEach( function( gridCol, index ) {
17940 if ( gridCol.colDef.exporterSuppressExport !== true &&
17941 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17943 name: gridCol.field,
17944 displayName: grid.options.exporterHeaderFilter ? ( grid.options.exporterHeaderFilterUseName ? grid.options.exporterHeaderFilter(gridCol.name) : grid.options.exporterHeaderFilter(gridCol.displayName) ) : gridCol.displayName,
17945 width: gridCol.drawnWidth ? gridCol.drawnWidth : gridCol.width,
17946 align: gridCol.colDef.type === 'number' ? 'right' : 'left'
17957 * @propertyOf ui.grid.exporter.api:ColumnDef
17958 * @name exporterPdfAlign
17959 * @description the alignment you'd like for this specific column when
17960 * exported into a pdf. Can be 'left', 'right', 'center' or any other
17961 * valid pdfMake alignment option.
17967 * @name ui.grid.exporter.api:GridRow
17968 * @description GridRow settings for exporter
17972 * @name exporterEnableExporting
17973 * @propertyOf ui.grid.exporter.api:GridRow
17974 * @description If set to false, then don't export this row, notwithstanding visible or
17976 * <br/>Defaults to true
17982 * @methodOf ui.grid.exporter.service:uiGridExporterService
17983 * @description Gets data from the grid based on the provided options,
17984 * all cells have cellFilters applied as appropriate. Any rows marked
17985 * `exporterEnableExporting: false` will not be exported
17986 * @param {Grid} grid the grid from which data should be exported
17987 * @param {string} rowTypes which rows to export, valid values are
17988 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17989 * uiGridExporterConstants.SELECTED
17990 * @param {string} colTypes which columns to export, valid values are
17991 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17992 * uiGridExporterConstants.SELECTED
17993 * @param {boolean} applyCellFilters whether or not to get the display value or the raw value of the data
17995 getData: function (grid, rowTypes, colTypes, applyCellFilters) {
18000 switch ( rowTypes ) {
18001 case uiGridExporterConstants.ALL:
18004 case uiGridExporterConstants.VISIBLE:
18005 rows = grid.getVisibleRows();
18007 case uiGridExporterConstants.SELECTED:
18008 if ( grid.api.selection ){
18009 rows = grid.api.selection.getSelectedGridRows();
18011 gridUtil.logError('selection feature must be enabled to allow selected rows to be exported');
18016 if ( colTypes === uiGridExporterConstants.ALL ){
18017 columns = grid.columns;
18019 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
18020 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
18021 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
18023 columns = leftColumns.concat(bodyColumns,rightColumns);
18026 rows.forEach( function( row, index ) {
18028 if (row.exporterEnableExporting !== false) {
18029 var extractedRow = [];
18032 columns.forEach( function( gridCol, index ) {
18033 if ( (gridCol.visible || colTypes === uiGridExporterConstants.ALL ) &&
18034 gridCol.colDef.exporterSuppressExport !== true &&
18035 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
18036 var cellValue = applyCellFilters ? grid.getCellDisplayValue( row, gridCol ) : grid.getCellValue( row, gridCol );
18037 var extractedField = { value: grid.options.exporterFieldCallback( grid, row, gridCol, cellValue ) };
18038 if ( gridCol.colDef.exporterPdfAlign ) {
18039 extractedField.alignment = gridCol.colDef.exporterPdfAlign;
18041 extractedRow.push(extractedField);
18045 data.push(extractedRow);
18055 * @name formatAsCSV
18056 * @methodOf ui.grid.exporter.service:uiGridExporterService
18057 * @description Formats the column headers and data as a CSV,
18058 * and sends that data to the user
18059 * @param {array} exportColumnHeaders an array of column headers,
18060 * where each header is an object with name, width and maybe alignment
18061 * @param {array} exportData an array of rows, where each row is
18062 * an array of column data
18063 * @returns {string} csv the formatted csv as a string
18065 formatAsCsv: function (exportColumnHeaders, exportData, separator) {
18068 var bareHeaders = exportColumnHeaders.map(function(header){return { value: header.displayName };});
18070 var csv = bareHeaders.length > 0 ? (self.formatRowAsCsv(this, separator)(bareHeaders) + '\n') : '';
18072 csv += exportData.map(this.formatRowAsCsv(this, separator)).join('\n');
18079 * @name formatRowAsCsv
18080 * @methodOf ui.grid.exporter.service:uiGridExporterService
18081 * @description Renders a single field as a csv field, including
18082 * quotes around the value
18083 * @param {exporterService} exporter pass in exporter
18084 * @param {array} row the row to be turned into a csv string
18085 * @returns {string} a csv-ified version of the row
18087 formatRowAsCsv: function (exporter, separator) {
18088 return function (row) {
18089 return row.map(exporter.formatFieldAsCsv).join(separator);
18095 * @name formatFieldAsCsv
18096 * @methodOf ui.grid.exporter.service:uiGridExporterService
18097 * @description Renders a single field as a csv field, including
18098 * quotes around the value
18099 * @param {field} field the field to be turned into a csv string,
18100 * may be of any type
18101 * @returns {string} a csv-ified version of the field
18103 formatFieldAsCsv: function (field) {
18104 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
18107 if (typeof(field.value) === 'number') {
18108 return field.value;
18110 if (typeof(field.value) === 'boolean') {
18111 return (field.value ? 'TRUE' : 'FALSE') ;
18113 if (typeof(field.value) === 'string') {
18114 return '"' + field.value.replace(/"/g,'""') + '"';
18117 return JSON.stringify(field.value);
18124 * @methodOf ui.grid.exporter.service:uiGridExporterService
18125 * @description Checks whether current browser is IE and returns it's version if it is
18127 isIE: function () {
18128 var match = navigator.userAgent.search(/(?:Edge|MSIE|Trident\/.*; rv:)/);
18131 if (match !== -1) {
18141 * @name downloadFile
18142 * @methodOf ui.grid.exporter.service:uiGridExporterService
18143 * @description Triggers download of a csv file. Logic provided
18144 * by @cssensei (from his colleagues at https://github.com/ifeelgoods) in issue #2391
18145 * @param {string} fileName the filename we'd like our file to be
18147 * @param {string} csvContent the csv content that we'd like to
18148 * download as a file
18149 * @param {boolean} exporterOlderExcelCompatibility whether or not we put a utf-16 BOM on the from (\uFEFF)
18151 downloadFile: function (fileName, csvContent, exporterOlderExcelCompatibility) {
18153 var a = D.createElement('a');
18154 var strMimeType = 'application/octet-stream;charset=utf-8';
18158 ieVersion = this.isIE();
18159 if (ieVersion && ieVersion < 10) {
18160 var frame = D.createElement('iframe');
18161 document.body.appendChild(frame);
18163 frame.contentWindow.document.open("text/html", "replace");
18164 frame.contentWindow.document.write('sep=,\r\n' + csvContent);
18165 frame.contentWindow.document.close();
18166 frame.contentWindow.focus();
18167 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
18169 document.body.removeChild(frame);
18174 if (navigator.msSaveBlob) {
18175 return navigator.msSaveOrOpenBlob(
18177 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
18178 { type: strMimeType } ),
18183 //html5 A[download]
18184 if ('download' in a) {
18185 var blob = new Blob(
18186 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
18187 { type: strMimeType }
18189 rawFile = URL.createObjectURL(blob);
18190 a.setAttribute('download', fileName);
18192 rawFile = 'data:' + strMimeType + ',' + encodeURIComponent(csvContent);
18193 a.setAttribute('target', '_blank');
18197 a.setAttribute('style', 'display:none;');
18198 D.body.appendChild(a);
18199 setTimeout(function() {
18202 // Workaround for Safari 5
18203 } else if (document.createEvent) {
18204 var eventObj = document.createEvent('MouseEvents');
18205 eventObj.initEvent('click', true, true);
18206 a.dispatchEvent(eventObj);
18208 D.body.removeChild(a);
18216 * @methodOf ui.grid.exporter.service:uiGridExporterService
18217 * @description Exports rows from the grid in pdf format,
18218 * the data exported is selected based on the provided options.
18219 * Note that this function has a dependency on pdfMake, which must
18220 * be installed. The resulting pdf opens in a new
18222 * @param {Grid} grid the grid from which data should be exported
18223 * @param {string} rowTypes which rows to export, valid values are
18224 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
18225 * uiGridExporterConstants.SELECTED
18226 * @param {string} colTypes which columns to export, valid values are
18227 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
18228 * uiGridExporterConstants.SELECTED
18230 pdfExport: function (grid, rowTypes, colTypes) {
18232 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function () {
18233 var exportColumnHeaders = self.getColumnHeaders(grid, colTypes);
18234 var exportData = self.getData(grid, rowTypes, colTypes);
18235 var docDefinition = self.prepareAsPdf(grid, exportColumnHeaders, exportData);
18237 if (self.isIE() || navigator.appVersion.indexOf("Edge") !== -1) {
18238 self.downloadPDF(grid.options.exporterPdfFilename, docDefinition);
18240 pdfMake.createPdf(docDefinition).open();
18248 * @name downloadPdf
18249 * @methodOf ui.grid.exporter.service:uiGridExporterService
18250 * @description Generates and retrieves the pdf as a blob, then downloads
18251 * it as a file. Only used in IE, in all other browsers we use the native
18252 * pdfMake.open function to just open the PDF
18253 * @param {string} fileName the filename to give to the pdf, can be set
18254 * through exporterPdfFilename
18255 * @param {object} docDefinition a pdf docDefinition that we can generate
18256 * and get a blob from
18258 downloadPDF: function (fileName, docDefinition) {
18260 var a = D.createElement('a');
18261 var strMimeType = 'application/octet-stream;charset=utf-8';
18265 ieVersion = this.isIE(); // This is now a boolean value
18266 var doc = pdfMake.createPdf(docDefinition);
18269 doc.getBuffer( function (buffer) {
18270 blob = new Blob([buffer]);
18273 if (navigator.msSaveBlob) {
18274 return navigator.msSaveBlob(
18279 // Previously: && ieVersion < 10
18280 // ieVersion now returns a boolean for the
18281 // sake of sanity. We just check `msSaveBlob` first.
18283 var frame = D.createElement('iframe');
18284 document.body.appendChild(frame);
18286 frame.contentWindow.document.open("text/html", "replace");
18287 frame.contentWindow.document.write(blob);
18288 frame.contentWindow.document.close();
18289 frame.contentWindow.focus();
18290 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
18292 document.body.removeChild(frame);
18301 * @name renderAsPdf
18302 * @methodOf ui.grid.exporter.service:uiGridExporterService
18303 * @description Renders the data into a pdf, and opens that pdf.
18305 * @param {Grid} grid the grid from which data should be exported
18306 * @param {array} exportColumnHeaders an array of column headers,
18307 * where each header is an object with name, width and maybe alignment
18308 * @param {array} exportData an array of rows, where each row is
18309 * an array of column data
18310 * @returns {object} a pdfMake format document definition, ready
18313 prepareAsPdf: function(grid, exportColumnHeaders, exportData) {
18314 var headerWidths = this.calculatePdfHeaderWidths( grid, exportColumnHeaders );
18316 var headerColumns = exportColumnHeaders.map( function( header ) {
18317 return { text: header.displayName, style: 'tableHeader' };
18320 var stringData = exportData.map(this.formatRowAsPdf(this));
18322 var allData = [headerColumns].concat(stringData);
18324 var docDefinition = {
18325 pageOrientation: grid.options.exporterPdfOrientation,
18326 pageSize: grid.options.exporterPdfPageSize,
18328 style: 'tableStyle',
18331 widths: headerWidths,
18336 tableStyle: grid.options.exporterPdfTableStyle,
18337 tableHeader: grid.options.exporterPdfTableHeaderStyle
18339 defaultStyle: grid.options.exporterPdfDefaultStyle
18342 if ( grid.options.exporterPdfLayout ){
18343 docDefinition.layout = grid.options.exporterPdfLayout;
18346 if ( grid.options.exporterPdfHeader ){
18347 docDefinition.header = grid.options.exporterPdfHeader;
18350 if ( grid.options.exporterPdfFooter ){
18351 docDefinition.footer = grid.options.exporterPdfFooter;
18354 if ( grid.options.exporterPdfCustomFormatter ){
18355 docDefinition = grid.options.exporterPdfCustomFormatter( docDefinition );
18357 return docDefinition;
18364 * @name calculatePdfHeaderWidths
18365 * @methodOf ui.grid.exporter.service:uiGridExporterService
18366 * @description Determines the column widths base on the
18367 * widths we got from the grid. If the column is drawn
18368 * then we have a drawnWidth. If the column is not visible
18369 * then we have '*', 'x%' or a width. When columns are
18370 * not visible they don't contribute to the overall gridWidth,
18371 * so we need to adjust to allow for extra columns
18373 * Our basic heuristic is to take the current gridWidth, plus
18374 * numeric columns and call this the base gridwidth.
18376 * To that we add 100 for any '*' column, and x% of the base gridWidth
18377 * for any column that is a %
18379 * @param {Grid} grid the grid from which data should be exported
18380 * @param {array} exportHeaders array of header information
18381 * @returns {object} an array of header widths
18383 calculatePdfHeaderWidths: function ( grid, exportHeaders ) {
18384 var baseGridWidth = 0;
18385 exportHeaders.forEach( function(value){
18386 if (typeof(value.width) === 'number'){
18387 baseGridWidth += value.width;
18391 var extraColumns = 0;
18392 exportHeaders.forEach( function(value){
18393 if (value.width === '*'){
18394 extraColumns += 100;
18396 if (typeof(value.width) === 'string' && value.width.match(/(\d)*%/)) {
18397 var percent = parseInt(value.width.match(/(\d)*%/)[0]);
18399 value.width = baseGridWidth * percent / 100;
18400 extraColumns += value.width;
18404 var gridWidth = baseGridWidth + extraColumns;
18406 return exportHeaders.map(function( header ) {
18407 return header.width === '*' ? header.width : header.width * grid.options.exporterPdfMaxGridWidth / gridWidth;
18414 * @name formatRowAsPdf
18415 * @methodOf ui.grid.exporter.service:uiGridExporterService
18416 * @description Renders a row in a format consumable by PDF,
18417 * mainly meaning casting everything to a string
18418 * @param {exporterService} exporter pass in exporter
18419 * @param {array} row the row to be turned into a csv string
18420 * @returns {string} a csv-ified version of the row
18422 formatRowAsPdf: function ( exporter ) {
18423 return function( row ) {
18424 return row.map(exporter.formatFieldAsPdfString);
18431 * @name formatFieldAsCsv
18432 * @methodOf ui.grid.exporter.service:uiGridExporterService
18433 * @description Renders a single field as a pdf-able field, which
18434 * is different from a csv field only in that strings don't have quotes
18436 * @param {field} field the field to be turned into a pdf string,
18437 * may be of any type
18438 * @returns {string} a string-ified version of the field
18440 formatFieldAsPdfString: function (field) {
18442 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
18444 } else if (typeof(field.value) === 'number') {
18445 returnVal = field.value.toString();
18446 } else if (typeof(field.value) === 'boolean') {
18447 returnVal = (field.value ? 'TRUE' : 'FALSE') ;
18448 } else if (typeof(field.value) === 'string') {
18449 returnVal = field.value.replace(/"/g,'""');
18451 returnVal = JSON.stringify(field.value).replace(/^"/,'').replace(/"$/,'');
18454 if (field.alignment && typeof(field.alignment) === 'string' ){
18455 returnVal = { text: returnVal, alignment: field.alignment };
18469 * @name ui.grid.exporter.directive:uiGridExporter
18473 * @description Adds exporter features to grid
18476 <example module="app">
18477 <file name="app.js">
18478 var app = angular.module('app', ['ui.grid', 'ui.grid.exporter']);
18480 app.controller('MainCtrl', ['$scope', function ($scope) {
18482 { name: 'Bob', title: 'CEO' },
18483 { name: 'Frank', title: 'Lowly Developer' }
18486 $scope.gridOptions = {
18487 enableGridMenu: true,
18488 exporterMenuCsv: false,
18490 {name: 'name', enableCellEdit: true},
18491 {name: 'title', enableCellEdit: true}
18497 <file name="index.html">
18498 <div ng-controller="MainCtrl">
18499 <div ui-grid="gridOptions" ui-grid-exporter></div>
18504 module.directive('uiGridExporter', ['uiGridExporterConstants', 'uiGridExporterService', 'gridUtil', '$compile',
18505 function (uiGridExporterConstants, uiGridExporterService, gridUtil, $compile) {
18509 require: '^uiGrid',
18511 link: function ($scope, $elm, $attrs, uiGridCtrl) {
18512 uiGridExporterService.initializeGrid(uiGridCtrl.grid);
18513 uiGridCtrl.grid.exporter.$scope = $scope;
18525 * @name ui.grid.grouping
18528 * # ui.grid.grouping
18530 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
18532 * This module provides grouping of rows based on the data in them, similar
18533 * in concept to excel grouping. You can group multiple columns, resulting in
18536 * In concept this feature is similar to sorting + grid footer/aggregation, it
18537 * sorts the data based on the grouped columns, then creates group rows that
18538 * reflect a break in the data. Each of those group rows can have aggregations for
18539 * the data within that group.
18541 * This feature leverages treeBase to provide the tree functionality itself,
18542 * the key thing this feature does therefore is to set treeLevels on the rows
18543 * and insert the group headers.
18545 * Design information:
18546 * -------------------
18548 * Each column will get new menu items - group by, and aggregate by. Group by
18549 * will cause this column to be sorted (if not already), and will move this column
18550 * to the front of the sorted columns (i.e. grouped columns take precedence over
18551 * sorted columns). It will respect the sort order already set if there is one,
18552 * and it will allow the sorting logic to change that sort order, it just forces
18553 * the column to the front of the sorting. You can group by multiple columns, the
18554 * logic will add this column to the sorting after any already grouped columns.
18556 * Once a grouping is defined, grouping logic is added to the rowsProcessors. This
18557 * will process the rows, identifying a break in the data value, and inserting a grouping row.
18558 * Grouping rows have specific attributes on them:
18560 * - internalRow = true: tells us that this isn't a real row, so we can ignore it
18561 * from any processing that it looking at core data rows. This is used by the core
18562 * logic (or will be one day), as it's not grouping specific
18563 * - groupHeader = true: tells us this is a groupHeader. This is used by the grouping logic
18564 * to know if this is a groupHeader row or not
18566 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
18567 * row order or filtering or anything like that is changed. In order to avoid the row instantiation
18568 * time, and to preserve state across invocations, we hold a cache of the rows that we created
18569 * last time, and we use them again this time if we can.
18571 * By default rows are collapsed, which means all data rows have their visible property
18572 * set to false, and only level 0 group rows are set to visible.
18577 * <div doc-module-components="ui.grid.grouping"></div>
18580 var module = angular.module('ui.grid.grouping', ['ui.grid', 'ui.grid.treeBase']);
18584 * @name ui.grid.grouping.constant:uiGridGroupingConstants
18586 * @description constants available in grouping module, this includes
18587 * all the constants declared in the treeBase module (these are manually copied
18588 * as there isn't an easy way to include constants in another constants file, and
18589 * we don't want to make users include treeBase)
18592 module.constant('uiGridGroupingConstants', {
18593 featureName: "grouping",
18594 rowHeaderColName: 'treeBaseRowHeaderCol',
18595 EXPANDED: 'expanded',
18596 COLLAPSED: 'collapsed',
18608 * @name ui.grid.grouping.service:uiGridGroupingService
18610 * @description Services for grouping features
18612 module.service('uiGridGroupingService', ['$q', 'uiGridGroupingConstants', 'gridUtil', 'rowSorter', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'uiGridTreeBaseService',
18613 function ($q, uiGridGroupingConstants, gridUtil, rowSorter, GridRow, gridClassFactory, i18nService, uiGridConstants, uiGridTreeBaseService) {
18617 initializeGrid: function (grid, $scope) {
18618 uiGridTreeBaseService.initializeGrid( grid, $scope );
18620 //add feature namespace and any properties to grid for needed
18623 * @name ui.grid.grouping.grid:grouping
18625 * @description Grid properties and functions added for grouping
18627 grid.grouping = {};
18631 * @propertyOf ui.grid.grouping.grid:grouping
18632 * @name groupHeaderCache
18634 * @description Cache that holds the group header rows we created last time, we'll
18635 * reuse these next time, not least because they hold our expanded states.
18637 * We need to take care with these that they don't become a memory leak, we
18638 * create a new cache each time using the values from the old cache. This works
18639 * so long as we're creating group rows for invisible rows as well.
18641 * The cache is a nested hash, indexed on the value we grouped by. So if we
18642 * grouped by gender then age, we'd maybe have something like:
18646 * row: <pointer to the old row>,
18648 * 22: { row: <pointer to the old row> },
18649 * 31: { row: <pointer to the old row> }
18652 * row: <pointer to the old row>,
18654 * 28: { row: <pointer to the old row> },
18655 * 55: { row: <pointer to the old row> }
18660 * We create new rows for any missing rows, this means that they come in as collapsed.
18663 grid.grouping.groupHeaderCache = {};
18665 service.defaultGridOptions(grid.options);
18667 grid.registerRowsProcessor(service.groupRows, 400);
18669 grid.registerColumnBuilder( service.groupingColumnBuilder);
18671 grid.registerColumnsProcessor(service.groupingColumnProcessor, 400);
18675 * @name ui.grid.grouping.api:PublicApi
18677 * @description Public Api for grouping feature
18684 * @eventOf ui.grid.grouping.api:PublicApi
18685 * @name aggregationChanged
18686 * @description raised whenever aggregation is changed, added or removed from a column
18689 * gridApi.grouping.on.aggregationChanged(scope,function(col){})
18691 * @param {gridCol} col the column which on which aggregation changed. The aggregation
18692 * type is available as `col.treeAggregation.type`
18694 aggregationChanged: {},
18697 * @eventOf ui.grid.grouping.api:PublicApi
18698 * @name groupingChanged
18699 * @description raised whenever the grouped columns changes
18702 * gridApi.grouping.on.groupingChanged(scope,function(col){})
18704 * @param {gridCol} col the column which on which grouping changed. The new grouping is
18705 * available as `col.grouping`
18707 groupingChanged: {}
18714 * @name getGrouping
18715 * @methodOf ui.grid.grouping.api:PublicApi
18716 * @description Get the grouping configuration for this grid,
18717 * used by the saveState feature. Adds expandedState to the information
18718 * provided by the internal getGrouping, and removes any aggregations that have a source
18719 * of grouping (i.e. will be automatically reapplied when we regroup the column)
18720 * Returned grouping is an object
18721 * `{ grouping: groupArray, treeAggregations: aggregateArray, expandedState: hash }`
18722 * where grouping contains an array of objects:
18723 * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }`
18724 * and aggregations contains an array of objects:
18725 * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }`
18726 * and expandedState is a hash of the currently expanded nodes
18728 * The groupArray will be sorted by groupPriority.
18730 * @param {boolean} getExpanded whether or not to return the expanded state
18731 * @returns {object} grouping configuration
18733 getGrouping: function ( getExpanded ) {
18734 var grouping = service.getGrouping(grid);
18736 grouping.grouping.forEach( function( group ) {
18737 group.colName = group.col.name;
18741 grouping.aggregations.forEach( function( aggregation ) {
18742 aggregation.colName = aggregation.col.name;
18743 delete aggregation.col;
18746 grouping.aggregations = grouping.aggregations.filter( function( aggregation ){
18747 return !aggregation.aggregation.source || aggregation.aggregation.source !== 'grouping';
18750 if ( getExpanded ){
18751 grouping.rowExpandedStates = service.getRowExpandedStates( grid.grouping.groupingHeaderCache );
18759 * @name setGrouping
18760 * @methodOf ui.grid.grouping.api:PublicApi
18761 * @description Set the grouping configuration for this grid,
18762 * used by the saveState feature, but can also be used by any
18763 * user to specify a combined grouping and aggregation configuration
18764 * @param {object} config the config you want to apply, in the format
18765 * provided out by getGrouping
18767 setGrouping: function ( config ) {
18768 service.setGrouping(grid, config);
18773 * @name groupColumn
18774 * @methodOf ui.grid.grouping.api:PublicApi
18775 * @description Adds this column to the existing grouping, at the end of the priority order.
18776 * If the column doesn't have a sort, adds one, by default ASC
18778 * This column will move to the left of any non-group columns, the
18779 * move is handled in a columnProcessor, so gets called as part of refresh
18781 * @param {string} columnName the name of the column we want to group
18783 groupColumn: function( columnName ) {
18784 var column = grid.getColumn(columnName);
18785 service.groupColumn(grid, column);
18790 * @name ungroupColumn
18791 * @methodOf ui.grid.grouping.api:PublicApi
18792 * @description Removes the groupPriority from this column. If the
18793 * column was previously aggregated the aggregation will come back.
18794 * The sort will remain.
18796 * This column will move to the right of any other group columns, the
18797 * move is handled in a columnProcessor, so gets called as part of refresh
18799 * @param {string} columnName the name of the column we want to ungroup
18801 ungroupColumn: function( columnName ) {
18802 var column = grid.getColumn(columnName);
18803 service.ungroupColumn(grid, column);
18808 * @name clearGrouping
18809 * @methodOf ui.grid.grouping.api:PublicApi
18810 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
18811 * as we don't know whether that sorting was added by grouping or was there beforehand
18814 clearGrouping: function() {
18815 service.clearGrouping(grid);
18820 * @name aggregateColumn
18821 * @methodOf ui.grid.grouping.api:PublicApi
18822 * @description Sets the aggregation type on a column, if the
18823 * column is currently grouped then it removes the grouping first.
18824 * If the aggregationDef is null then will result in the aggregation
18827 * @param {string} columnName the column we want to aggregate
18828 * @param {string} or {function} aggregationDef one of the recognised types
18829 * from uiGridGroupingConstants or a custom aggregation function.
18830 * @param {string} aggregationLabel (optional) The label to use for this aggregation.
18832 aggregateColumn: function( columnName, aggregationDef, aggregationLabel){
18833 var column = grid.getColumn(columnName);
18834 service.aggregateColumn( grid, column, aggregationDef, aggregationLabel);
18841 grid.api.registerEventsFromObject(publicApi.events);
18843 grid.api.registerMethodsFromObject(publicApi.methods);
18845 grid.api.core.on.sortChanged( $scope, service.tidyPriorities);
18849 defaultGridOptions: function (gridOptions) {
18850 //default option to true unless it was explicitly set to false
18853 * @name ui.grid.grouping.api:GridOptions
18855 * @description GridOptions for grouping feature, these are available to be
18856 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
18861 * @name enableGrouping
18862 * @propertyOf ui.grid.grouping.api:GridOptions
18863 * @description Enable row grouping for entire grid.
18864 * <br/>Defaults to true
18866 gridOptions.enableGrouping = gridOptions.enableGrouping !== false;
18870 * @name groupingShowCounts
18871 * @propertyOf ui.grid.grouping.api:GridOptions
18872 * @description shows counts on the groupHeader rows. Not that if you are using a cellFilter or a
18873 * sortingAlgorithm which relies on a specific format or data type, showing counts may cause that
18874 * to break, since the group header rows will always be a string with groupingShowCounts enabled.
18875 * <br/>Defaults to true except on columns of type 'date'
18877 gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false;
18881 * @name groupingNullLabel
18882 * @propertyOf ui.grid.grouping.api:GridOptions
18883 * @description The string to use for the grouping header row label on rows which contain a null or undefined value in the grouped column.
18884 * <br/>Defaults to "Null"
18886 gridOptions.groupingNullLabel = typeof(gridOptions.groupingNullLabel) === 'undefined' ? 'Null' : gridOptions.groupingNullLabel;
18890 * @name enableGroupHeaderSelection
18891 * @propertyOf ui.grid.grouping.api:GridOptions
18892 * @description Allows group header rows to be selected.
18893 * <br/>Defaults to false
18895 gridOptions.enableGroupHeaderSelection = gridOptions.enableGroupHeaderSelection === true;
18901 * @name groupingColumnBuilder
18902 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18903 * @description Sets the grouping defaults based on the columnDefs
18905 * @param {object} colDef columnDef we're basing on
18906 * @param {GridCol} col the column we're to update
18907 * @param {object} gridOptions the options we should use
18908 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
18910 groupingColumnBuilder: function (colDef, col, gridOptions) {
18913 * @name ui.grid.grouping.api:ColumnDef
18915 * @description ColumnDef for grouping feature, these are available to be
18916 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
18921 * @name enableGrouping
18922 * @propertyOf ui.grid.grouping.api:ColumnDef
18923 * @description Enable grouping on this column
18924 * <br/>Defaults to true.
18926 if (colDef.enableGrouping === false){
18933 * @propertyOf ui.grid.grouping.api:ColumnDef
18934 * @description Set the grouping for a column. Format is:
18937 * groupPriority: <number, starts at 0, if less than 0 or undefined then we're aggregating in this column>
18941 * **Note that aggregation used to be included in grouping, but is now separately set on the column via treeAggregation
18942 * setting in treeBase**
18944 * We group in the priority order given, this will also put these columns to the high order of the sort irrespective
18945 * of the sort priority given them. If there is no sort defined then we sort ascending, if there is a sort defined then
18946 * we use that sort.
18948 * If the groupPriority is undefined or less than 0, then we expect to be aggregating, and we look at the
18949 * aggregation types to determine what sort of aggregation we can do. Values are in the constants file, but
18950 * include SUM, COUNT, MAX, MIN
18952 * groupPriorities should generally be sequential, if they're not then the next time getGrouping is called
18953 * we'll renumber them to be sequential.
18954 * <br/>Defaults to undefined.
18957 if ( typeof(col.grouping) === 'undefined' && typeof(colDef.grouping) !== 'undefined') {
18958 col.grouping = angular.copy(colDef.grouping);
18959 if ( typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority > -1 ){
18960 col.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
18961 col.treeAggregationFinalizerFn = service.groupedFinalizerFn;
18963 } else if (typeof(col.grouping) === 'undefined'){
18967 if (typeof(col.grouping) !== 'undefined' && typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority >= 0){
18968 col.suppressRemoveSort = true;
18971 var groupColumn = {
18972 name: 'ui.grid.grouping.group',
18973 title: i18nService.get().grouping.group,
18974 icon: 'ui-grid-icon-indent-right',
18975 shown: function () {
18976 return typeof(this.context.col.grouping) === 'undefined' ||
18977 typeof(this.context.col.grouping.groupPriority) === 'undefined' ||
18978 this.context.col.grouping.groupPriority < 0;
18980 action: function () {
18981 service.groupColumn( this.context.col.grid, this.context.col );
18985 var ungroupColumn = {
18986 name: 'ui.grid.grouping.ungroup',
18987 title: i18nService.get().grouping.ungroup,
18988 icon: 'ui-grid-icon-indent-left',
18989 shown: function () {
18990 return typeof(this.context.col.grouping) !== 'undefined' &&
18991 typeof(this.context.col.grouping.groupPriority) !== 'undefined' &&
18992 this.context.col.grouping.groupPriority >= 0;
18994 action: function () {
18995 service.ungroupColumn( this.context.col.grid, this.context.col );
18999 var aggregateRemove = {
19000 name: 'ui.grid.grouping.aggregateRemove',
19001 title: i18nService.get().grouping.aggregate_remove,
19002 shown: function () {
19003 return typeof(this.context.col.treeAggregationFn) !== 'undefined';
19005 action: function () {
19006 service.aggregateColumn( this.context.col.grid, this.context.col, null);
19010 // generic adder for the aggregation menus, which follow a pattern
19011 var addAggregationMenu = function(type, title){
19012 title = title || i18nService.get().grouping['aggregate_' + type] || type;
19014 name: 'ui.grid.grouping.aggregate' + type,
19016 shown: function () {
19017 return typeof(this.context.col.treeAggregation) === 'undefined' ||
19018 typeof(this.context.col.treeAggregation.type) === 'undefined' ||
19019 this.context.col.treeAggregation.type !== type;
19021 action: function () {
19022 service.aggregateColumn( this.context.col.grid, this.context.col, type);
19026 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregate' + type)) {
19027 col.menuItems.push(menuItem);
19033 * @name groupingShowGroupingMenu
19034 * @propertyOf ui.grid.grouping.api:ColumnDef
19035 * @description Show the grouping (group and ungroup items) menu on this column
19036 * <br/>Defaults to true.
19038 if ( col.colDef.groupingShowGroupingMenu !== false ){
19039 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.group')) {
19040 col.menuItems.push(groupColumn);
19043 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.ungroup')) {
19044 col.menuItems.push(ungroupColumn);
19051 * @name groupingShowAggregationMenu
19052 * @propertyOf ui.grid.grouping.api:ColumnDef
19053 * @description Show the aggregation menu on this column
19054 * <br/>Defaults to true.
19056 if ( col.colDef.groupingShowAggregationMenu !== false ){
19057 angular.forEach(uiGridTreeBaseService.nativeAggregations(), function(aggregationDef, name){
19058 addAggregationMenu(name);
19060 angular.forEach(gridOptions.treeCustomAggregations, function(aggregationDef, name){
19061 addAggregationMenu(name, aggregationDef.menuTitle);
19064 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregateRemove')) {
19065 col.menuItems.push(aggregateRemove);
19075 * @name groupingColumnProcessor
19076 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19077 * @description Moves the columns around based on which are grouped
19079 * @param {array} columns the columns to consider rendering
19080 * @param {array} rows the grid rows, which we don't use but are passed to us
19081 * @returns {array} updated columns array
19083 groupingColumnProcessor: function( columns, rows ) {
19086 columns = service.moveGroupColumns(this, columns, rows);
19092 * @name groupedFinalizerFn
19093 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19094 * @description Used on group columns to display the rendered value and optionally
19095 * display the count of rows.
19097 * @param {aggregation} the aggregation entity for a grouped column
19099 groupedFinalizerFn: function( aggregation ){
19102 if ( typeof(aggregation.groupVal) !== 'undefined') {
19103 aggregation.rendered = aggregation.groupVal;
19104 if ( col.grid.options.groupingShowCounts && col.colDef.type !== 'date' ){
19105 aggregation.rendered += (' (' + aggregation.value + ')');
19108 aggregation.rendered = null;
19114 * @name moveGroupColumns
19115 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19116 * @description Moves the column order so that the grouped columns are lined up
19117 * to the left (well, unless you're RTL, then it's the right). By doing this in
19118 * the columnsProcessor, we make it transient - when the column is ungrouped it'll
19119 * go back to where it was.
19121 * Does nothing if the option `moveGroupColumns` is set to false.
19123 * @param {Grid} grid grid object
19124 * @param {array} columns the columns that we should process/move
19125 * @param {array} rows the grid rows
19126 * @returns {array} updated columns
19128 moveGroupColumns: function( grid, columns, rows ){
19129 if ( grid.options.moveGroupColumns === false){
19133 columns.forEach( function(column, index){
19134 // position used to make stable sort in moveGroupColumns
19135 column.groupingPosition = index;
19138 columns.sort(function(a, b){
19139 var a_group, b_group;
19140 if (a.isRowHeader){
19143 else if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){
19146 a_group = a.grouping.groupPriority;
19149 if (b.isRowHeader){
19152 else if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){
19155 b_group = b.grouping.groupPriority;
19158 // groups get sorted to the top
19159 if ( a_group !== null && b_group === null) { return -1; }
19160 if ( b_group !== null && a_group === null) { return 1; }
19161 if ( a_group !== null && b_group !== null) {return a_group - b_group; }
19163 return a.groupingPosition - b.groupingPosition;
19166 columns.forEach( function(column, index) {
19167 delete column.groupingPosition;
19176 * @name groupColumn
19177 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19178 * @description Adds this column to the existing grouping, at the end of the priority order.
19179 * If the column doesn't have a sort, adds one, by default ASC
19181 * This column will move to the left of any non-group columns, the
19182 * move is handled in a columnProcessor, so gets called as part of refresh
19184 * @param {Grid} grid grid object
19185 * @param {GridCol} column the column we want to group
19187 groupColumn: function( grid, column){
19188 if ( typeof(column.grouping) === 'undefined' ){
19189 column.grouping = {};
19192 // set the group priority to the next number in the hierarchy
19193 var existingGrouping = service.getGrouping( grid );
19194 column.grouping.groupPriority = existingGrouping.grouping.length;
19196 // add sort if not present
19197 if ( !column.sort ){
19198 column.sort = { direction: uiGridConstants.ASC };
19199 } else if ( typeof(column.sort.direction) === 'undefined' || column.sort.direction === null ){
19200 column.sort.direction = uiGridConstants.ASC;
19203 column.treeAggregation = { type: uiGridGroupingConstants.aggregation.COUNT, source: 'grouping' };
19204 column.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
19205 column.treeAggregationFinalizerFn = service.groupedFinalizerFn;
19207 grid.api.grouping.raise.groupingChanged(column);
19208 // This indirectly calls service.tidyPriorities( grid );
19209 grid.api.core.raise.sortChanged(grid, grid.getColumnSorting());
19211 grid.queueGridRefresh();
19217 * @name ungroupColumn
19218 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19219 * @description Removes the groupPriority from this column. If the
19220 * column was previously aggregated the aggregation will come back.
19221 * The sort will remain.
19223 * This column will move to the right of any other group columns, the
19224 * move is handled in a columnProcessor, so gets called as part of refresh
19226 * @param {Grid} grid grid object
19227 * @param {GridCol} column the column we want to ungroup
19229 ungroupColumn: function( grid, column){
19230 if ( typeof(column.grouping) === 'undefined' ){
19234 delete column.grouping.groupPriority;
19235 delete column.treeAggregation;
19236 delete column.customTreeAggregationFinalizer;
19238 service.tidyPriorities( grid );
19240 grid.api.grouping.raise.groupingChanged(column);
19242 grid.queueGridRefresh();
19247 * @name aggregateColumn
19248 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19249 * @description Sets the aggregation type on a column, if the
19250 * column is currently grouped then it removes the grouping first.
19252 * @param {Grid} grid grid object
19253 * @param {GridCol} column the column we want to aggregate
19254 * @param {string} one of the recognised types from uiGridGroupingConstants or one of the custom aggregations from gridOptions
19256 aggregateColumn: function( grid, column, aggregationType){
19258 if (typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
19259 service.ungroupColumn( grid, column );
19262 var aggregationDef = {};
19263 if ( typeof(grid.options.treeCustomAggregations[aggregationType]) !== 'undefined' ){
19264 aggregationDef = grid.options.treeCustomAggregations[aggregationType];
19265 } else if ( typeof(uiGridTreeBaseService.nativeAggregations()[aggregationType]) !== 'undefined' ){
19266 aggregationDef = uiGridTreeBaseService.nativeAggregations()[aggregationType];
19269 column.treeAggregation = { type: aggregationType, label: i18nService.get().aggregation[aggregationDef.label] || aggregationDef.label };
19270 column.treeAggregationFn = aggregationDef.aggregationFn;
19271 column.treeAggregationFinalizerFn = aggregationDef.finalizerFn;
19273 grid.api.grouping.raise.aggregationChanged(column);
19275 grid.queueGridRefresh();
19281 * @name setGrouping
19282 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19283 * @description Set the grouping based on a config object, used by the save state feature
19284 * (more specifically, by the restore function in that feature )
19286 * @param {Grid} grid grid object
19287 * @param {object} config the config we want to set, same format as that returned by getGrouping
19289 setGrouping: function ( grid, config ){
19290 if ( typeof(config) === 'undefined' ){
19294 // first remove any existing grouping
19295 service.clearGrouping(grid);
19297 if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){
19298 config.grouping.forEach( function( group ) {
19299 var col = grid.getColumn(group.colName);
19302 service.groupColumn( grid, col );
19307 if ( config.aggregations && config.aggregations.length ){
19308 config.aggregations.forEach( function( aggregation ) {
19309 var col = grid.getColumn(aggregation.colName);
19312 service.aggregateColumn( grid, col, aggregation.aggregation.type );
19317 if ( config.rowExpandedStates ){
19318 service.applyRowExpandedStates( grid.grouping.groupingHeaderCache, config.rowExpandedStates );
19325 * @name clearGrouping
19326 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19327 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
19328 * as we don't know whether that sorting was added by grouping or was there beforehand
19330 * @param {Grid} grid grid object
19332 clearGrouping: function( grid ) {
19333 var currentGrouping = service.getGrouping(grid);
19335 if ( currentGrouping.grouping.length > 0 ){
19336 currentGrouping.grouping.forEach( function( group ) {
19338 // should have a group.colName if there's no col
19339 group.col = grid.getColumn(group.colName);
19341 service.ungroupColumn(grid, group.col);
19345 if ( currentGrouping.aggregations.length > 0 ){
19346 currentGrouping.aggregations.forEach( function( aggregation ){
19347 if (!aggregation.col){
19348 // should have a group.colName if there's no col
19349 aggregation.col = grid.getColumn(aggregation.colName);
19351 service.aggregateColumn(grid, aggregation.col, null);
19359 * @name tidyPriorities
19360 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19361 * @description Renumbers groupPriority and sortPriority such that
19362 * groupPriority is contiguous, and sortPriority either matches
19363 * groupPriority (for group columns), and otherwise is contiguous and
19364 * higher than groupPriority.
19366 * @param {Grid} grid grid object
19368 tidyPriorities: function( grid ){
19369 // if we're called from sortChanged, grid is in this, not passed as param, the param can be a column or undefined
19370 if ( ( typeof(grid) === 'undefined' || typeof(grid.grid) !== 'undefined' ) && typeof(this.grid) !== 'undefined' ) {
19374 var groupArray = [];
19375 var sortArray = [];
19377 grid.columns.forEach( function(column, index){
19378 if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
19379 groupArray.push(column);
19380 } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0){
19381 sortArray.push(column);
19385 groupArray.sort(function(a, b){ return a.grouping.groupPriority - b.grouping.groupPriority; });
19386 groupArray.forEach( function(column, index){
19387 column.grouping.groupPriority = index;
19388 column.suppressRemoveSort = true;
19389 if ( typeof(column.sort) === 'undefined'){
19392 column.sort.priority = index;
19395 var i = groupArray.length;
19396 sortArray.sort(function(a, b){ return a.sort.priority - b.sort.priority; });
19397 sortArray.forEach( function(column, index){
19398 column.sort.priority = i;
19399 column.suppressRemoveSort = column.colDef.suppressRemoveSort;
19408 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19409 * @description The rowProcessor that creates the groupHeaders (i.e. does
19410 * the actual grouping).
19412 * Assumes it is always called after the sorting processor, guaranteed by the priority setting
19414 * Processes all the rows in order, inserting a groupHeader row whenever there is a change
19415 * in value of a grouped row, based on the sortAlgorithm used for the column. The group header row
19416 * is looked up in the groupHeaderCache, and used from there if there is one. The entity is reset
19417 * to {} if one is found.
19419 * As it processes it maintains a `processingState` array. This records, for each level of grouping we're
19420 * working with, the following information:
19425 * initialised: boolean,
19426 * currentValue: value,
19427 * currentRow: gridRow,
19430 * We look for changes in the currentValue at any of the levels. Where we find a change we:
19432 * - create a new groupHeader row in the array
19434 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
19435 * @returns {array} the updated rows, including our new group rows
19437 groupRows: function( renderableRows ) {
19438 if (renderableRows.length === 0){
19439 return renderableRows;
19443 grid.grouping.oldGroupingHeaderCache = grid.grouping.groupingHeaderCache || {};
19444 grid.grouping.groupingHeaderCache = {};
19446 var processingState = service.initialiseProcessingState( grid );
19448 // processes each of the fields we are grouping by, checks if the value has changed and inserts a groupHeader
19449 // Broken out as shouldn't create functions in a loop.
19450 var updateProcessingState = function( groupFieldState, stateIndex ) {
19451 var fieldValue = grid.getCellValue(row, groupFieldState.col);
19453 // look for change of value - and insert a header
19454 if ( !groupFieldState.initialised || rowSorter.getSortFn(grid, groupFieldState.col, renderableRows)(fieldValue, groupFieldState.currentValue) !== 0 ){
19455 service.insertGroupHeader( grid, renderableRows, i, processingState, stateIndex );
19460 // use a for loop because it's tolerant of the array length changing whilst we go - we can
19461 // manipulate the iterator when we insert groupHeader rows
19462 for (var i = 0; i < renderableRows.length; i++ ){
19463 var row = renderableRows[i];
19465 if ( row.visible ){
19466 processingState.forEach( updateProcessingState );
19470 delete grid.grouping.oldGroupingHeaderCache;
19471 return renderableRows;
19477 * @name initialiseProcessingState
19478 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19479 * @description Creates the processing state array that is used
19482 * @param {Grid} grid grid object
19483 * @returns {array} an array in the format described in the groupRows method,
19484 * initialised with blank values
19486 initialiseProcessingState: function( grid ){
19487 var processingState = [];
19488 var columnSettings = service.getGrouping( grid );
19490 columnSettings.grouping.forEach( function( groupItem, index){
19491 processingState.push({
19492 fieldName: groupItem.field,
19493 col: groupItem.col,
19494 initialised: false,
19495 currentValue: null,
19500 return processingState;
19506 * @name getGrouping
19507 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19508 * @description Get the grouping settings from the columns. As a side effect
19509 * this always renumbers the grouping starting at 0
19510 * @param {Grid} grid grid object
19511 * @returns {array} an array of the group fields, in order of priority
19513 getGrouping: function( grid ){
19514 var groupArray = [];
19515 var aggregateArray = [];
19517 // get all the grouping
19518 grid.columns.forEach( function(column, columnIndex){
19519 if ( column.grouping ){
19520 if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
19521 groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping });
19524 if ( column.treeAggregation && column.treeAggregation.type ){
19525 aggregateArray.push({ field: column.field, col: column, aggregation: column.treeAggregation });
19529 // sort grouping into priority order
19530 groupArray.sort( function(a, b){
19531 return a.groupPriority - b.groupPriority;
19534 // renumber the priority in case it was somewhat messed up, then remove the grouping reference
19535 groupArray.forEach( function( group, index) {
19536 group.grouping.groupPriority = index;
19537 group.groupPriority = index;
19538 delete group.grouping;
19541 return { grouping: groupArray, aggregations: aggregateArray };
19547 * @name insertGroupHeader
19548 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19549 * @description Create a group header row, and link it to the various configuration
19550 * items that we use.
19552 * Look for the row in the oldGroupingHeaderCache, write the row into the new groupingHeaderCache.
19554 * @param {Grid} grid grid object
19555 * @param {array} renderableRows the rows that we are processing
19556 * @param {number} rowIndex the row we were up to processing
19557 * @param {array} processingState the current processing state
19558 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header -
19559 * i.e. the column that we want to create a header for
19561 insertGroupHeader: function( grid, renderableRows, rowIndex, processingState, stateIndex ) {
19562 // set the value that caused the end of a group into the header row and the processing state
19563 var fieldName = processingState[stateIndex].fieldName;
19564 var col = processingState[stateIndex].col;
19566 var newValue = grid.getCellValue(renderableRows[rowIndex], col);
19567 var newDisplayValue = newValue;
19568 if ( typeof(newValue) === 'undefined' || newValue === null ) {
19569 newDisplayValue = grid.options.groupingNullLabel;
19572 var getKeyAsValueForCacheMap = function(key) {
19573 if (angular.isObject(key)) {
19574 return JSON.stringify(key);
19580 var cacheItem = grid.grouping.oldGroupingHeaderCache;
19581 for ( var i = 0; i < stateIndex; i++ ){
19582 if ( cacheItem && cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)] ){
19583 cacheItem = cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)].children;
19588 if ( cacheItem && cacheItem[getKeyAsValueForCacheMap(newValue)]){
19589 headerRow = cacheItem[getKeyAsValueForCacheMap(newValue)].row;
19590 headerRow.entity = {};
19592 headerRow = new GridRow( {}, null, grid );
19593 gridClassFactory.rowTemplateAssigner.call(grid, headerRow);
19596 headerRow.entity['$$' + processingState[stateIndex].col.uid] = { groupVal: newDisplayValue };
19597 headerRow.treeLevel = stateIndex;
19598 headerRow.groupHeader = true;
19599 headerRow.internalRow = true;
19600 headerRow.enableCellEdit = false;
19601 headerRow.enableSelection = grid.options.enableGroupHeaderSelection;
19602 processingState[stateIndex].initialised = true;
19603 processingState[stateIndex].currentValue = newValue;
19604 processingState[stateIndex].currentRow = headerRow;
19606 // set all processing states below this one to not be initialised - change of this state
19607 // means all those need to start again
19608 service.finaliseProcessingState( processingState, stateIndex + 1);
19610 // insert our new header row
19611 renderableRows.splice(rowIndex, 0, headerRow);
19613 // add our new header row to the cache
19614 cacheItem = grid.grouping.groupingHeaderCache;
19615 for ( i = 0; i < stateIndex; i++ ){
19616 cacheItem = cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)].children;
19618 cacheItem[getKeyAsValueForCacheMap(newValue)] = { row: headerRow, children: {} };
19624 * @name finaliseProcessingState
19625 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19626 * @description Set all processing states lower than the one that had a break in value to
19627 * no longer be initialised. Render the counts into the entity ready for display.
19629 * @param {Grid} grid grid object
19630 * @param {array} processingState the current processing state
19631 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header, all
19632 * processing states after this need to be finalised
19634 finaliseProcessingState: function( processingState, stateIndex ){
19635 for ( var i = stateIndex; i < processingState.length; i++){
19636 processingState[i].initialised = false;
19637 processingState[i].currentRow = null;
19638 processingState[i].currentValue = null;
19645 * @name getRowExpandedStates
19646 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19647 * @description Extract the groupHeaderCache hash, pulling out only the states.
19649 * The example below shows a grid that is grouped by gender then age
19654 * state: 'expanded',
19656 * 22: { state: 'expanded' },
19657 * 30: { state: 'collapsed' }
19661 * state: 'expanded',
19663 * 28: { state: 'expanded' },
19664 * 55: { state: 'collapsed' }
19670 * @param {Grid} grid grid object
19671 * @returns {hash} the expanded states as a hash
19673 getRowExpandedStates: function(treeChildren){
19674 if ( typeof(treeChildren) === 'undefined' ){
19678 var newChildren = {};
19680 angular.forEach( treeChildren, function( value, key ){
19681 newChildren[key] = { state: value.row.treeNode.state };
19682 if ( value.children ){
19683 newChildren[key].children = service.getRowExpandedStates( value.children );
19685 newChildren[key].children = {};
19689 return newChildren;
19695 * @name applyRowExpandedStates
19696 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19697 * @description Take a hash in the format as created by getRowExpandedStates,
19698 * and apply it to the grid.grouping.groupHeaderCache.
19700 * Takes a treeSubset, and applies to a treeSubset - so can be called
19703 * @param {object} currentNode can be grid.grouping.groupHeaderCache, or any of
19704 * the children of that hash
19705 * @returns {hash} expandedStates can be the full expanded states, or children
19706 * of that expanded states (which hopefully matches the subset of the groupHeaderCache)
19708 applyRowExpandedStates: function( currentNode, expandedStates ){
19709 if ( typeof(expandedStates) === 'undefined' ){
19713 angular.forEach(expandedStates, function( value, key ) {
19714 if ( currentNode[key] ){
19715 currentNode[key].row.treeNode.state = value.state;
19717 if (value.children && currentNode[key].children){
19718 service.applyRowExpandedStates( currentNode[key].children, value.children );
19734 * @name ui.grid.grouping.directive:uiGridGrouping
19738 * @description Adds grouping features to grid
19741 <example module="app">
19742 <file name="app.js">
19743 var app = angular.module('app', ['ui.grid', 'ui.grid.grouping']);
19745 app.controller('MainCtrl', ['$scope', function ($scope) {
19747 { name: 'Bob', title: 'CEO' },
19748 { name: 'Frank', title: 'Lowly Developer' }
19751 $scope.columnDefs = [
19752 {name: 'name', enableCellEdit: true},
19753 {name: 'title', enableCellEdit: true}
19756 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
19759 <file name="index.html">
19760 <div ng-controller="MainCtrl">
19761 <div ui-grid="gridOptions" ui-grid-grouping></div>
19766 module.directive('uiGridGrouping', ['uiGridGroupingConstants', 'uiGridGroupingService', '$templateCache',
19767 function (uiGridGroupingConstants, uiGridGroupingService, $templateCache) {
19771 require: '^uiGrid',
19773 compile: function () {
19775 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
19776 if (uiGridCtrl.grid.options.enableGrouping !== false){
19777 uiGridGroupingService.initializeGrid(uiGridCtrl.grid, $scope);
19780 post: function ($scope, $elm, $attrs, uiGridCtrl) {
19794 * @name ui.grid.importer
19797 * # ui.grid.importer
19799 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
19801 * This module provides the ability to import data into the grid. It
19802 * uses the column defs to work out which data belongs in which column,
19803 * and creates entities from a configured class (typically a $resource).
19805 * If the rowEdit feature is enabled, it also calls save on those newly
19806 * created objects, and then displays any errors in the imported data.
19808 * Currently the importer imports only CSV and json files, although provision has been
19809 * made to process other file formats, and these can be added over time.
19811 * For json files, the properties within each object in the json must match the column names
19812 * (to put it another way, the importer doesn't process the json, it just copies the objects
19813 * within the json into a new instance of the specified object type)
19815 * For CSV import, the default column identification relies on each column in the
19816 * header row matching a column.name or column.displayName. Optionally, a column identification
19817 * callback can be used. This allows matching using other attributes, which is particularly
19818 * useful if your application has internationalised column headings (i.e. the headings that
19819 * the user sees don't match the column names).
19821 * The importer makes use of the grid menu as the UI for requesting an
19824 * <div ui-grid-importer></div>
19827 var module = angular.module('ui.grid.importer', ['ui.grid']);
19831 * @name ui.grid.importer.constant:uiGridImporterConstants
19833 * @description constants available in importer module
19836 module.constant('uiGridImporterConstants', {
19837 featureName: 'importer'
19842 * @name ui.grid.importer.service:uiGridImporterService
19844 * @description Services for importer feature
19846 module.service('uiGridImporterService', ['$q', 'uiGridConstants', 'uiGridImporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService', '$window',
19847 function ($q, uiGridConstants, uiGridImporterConstants, gridUtil, $compile, $interval, i18nService, $window) {
19851 initializeGrid: function ($scope, grid) {
19853 //add feature namespace and any properties to grid for needed state
19858 this.defaultGridOptions(grid.options);
19862 * @name ui.grid.importer.api:PublicApi
19864 * @description Public Api for importer feature
19876 * @methodOf ui.grid.importer.api:PublicApi
19877 * @description Imports a file into the grid using the file object
19878 * provided. Bypasses the grid menu
19879 * @param {File} fileObject the file we want to import, as a javascript
19882 importFile: function ( fileObject ) {
19883 service.importThisFile( grid, fileObject );
19889 grid.api.registerEventsFromObject(publicApi.events);
19891 grid.api.registerMethodsFromObject(publicApi.methods);
19893 if ( grid.options.enableImporter && grid.options.importerShowMenu ){
19894 if ( grid.api.core.addToGridMenu ){
19895 service.addToMenu( grid );
19897 // order of registration is not guaranteed, register in a little while
19898 $interval( function() {
19899 if (grid.api.core.addToGridMenu){
19900 service.addToMenu( grid );
19908 defaultGridOptions: function (gridOptions) {
19909 //default option to true unless it was explicitly set to false
19912 * @name ui.grid.importer.api:GridOptions
19914 * @description GridOptions for importer feature, these are available to be
19915 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
19920 * @propertyOf ui.grid.importer.api:GridOptions
19921 * @name enableImporter
19922 * @description Whether or not importer is enabled. Automatically set
19923 * to false if the user's browser does not support the required fileApi.
19924 * Otherwise defaults to true.
19927 if (gridOptions.enableImporter || gridOptions.enableImporter === undefined) {
19928 if ( !($window.hasOwnProperty('File') && $window.hasOwnProperty('FileReader') && $window.hasOwnProperty('FileList') && $window.hasOwnProperty('Blob')) ) {
19929 gridUtil.logError('The File APIs are not fully supported in this browser, grid importer cannot be used.');
19930 gridOptions.enableImporter = false;
19932 gridOptions.enableImporter = true;
19935 gridOptions.enableImporter = false;
19940 * @name importerProcessHeaders
19941 * @methodOf ui.grid.importer.api:GridOptions
19942 * @description A callback function that will process headers using custom
19943 * logic. Set this callback function if the headers that your user will provide in their
19944 * import file don't necessarily match the grid header or field names. This might commonly
19945 * occur where your application is internationalised, and therefore the field names
19946 * that the user recognises are in a different language than the field names that
19947 * ui-grid knows about.
19949 * Defaults to the internal `processHeaders` method, which seeks to match using both
19950 * displayName and column.name. Any non-matching columns are discarded.
19952 * Your callback routine should respond by processing the header array, and returning an array
19953 * of matching column names. A null value in any given position means "don't import this column"
19956 * gridOptions.importerProcessHeaders: function( headerArray ) {
19957 * var myHeaderColumns = [];
19959 * headerArray.forEach( function( value, index ) {
19960 * thisCol = mySpecialLookupFunction( value );
19961 * myHeaderColumns.push( thisCol.name );
19964 * return myHeaderCols;
19967 * @param {Grid} grid the grid we're importing into
19968 * @param {array} headerArray an array of the text from the first row of the csv file,
19969 * which you need to match to column.names
19970 * @returns {array} array of matching column names, in the same order as the headerArray
19973 gridOptions.importerProcessHeaders = gridOptions.importerProcessHeaders || service.processHeaders;
19977 * @name importerHeaderFilter
19978 * @methodOf ui.grid.importer.api:GridOptions
19979 * @description A callback function that will filter (usually translate) a single
19980 * header. Used when you want to match the passed in column names to the column
19981 * displayName after the header filter.
19983 * Your callback routine needs to return the filtered header value.
19985 * gridOptions.importerHeaderFilter: function( displayName ) {
19986 * return $translate.instant( displayName );
19992 * gridOptions.importerHeaderFilter: $translate.instant
19994 * @param {string} displayName the displayName that we'd like to translate
19995 * @returns {string} the translated name
19998 gridOptions.importerHeaderFilter = gridOptions.importerHeaderFilter || function( displayName ) { return displayName; };
20002 * @name importerErrorCallback
20003 * @methodOf ui.grid.importer.api:GridOptions
20004 * @description A callback function that provides custom error handling, rather
20005 * than the standard grid behaviour of an alert box and a console message. You
20006 * might use this to internationalise the console log messages, or to write to a
20007 * custom logging routine that returned errors to the server.
20010 * gridOptions.importerErrorCallback: function( grid, errorKey, consoleMessage, context ) {
20011 * myUserDisplayRoutine( errorKey );
20012 * myLoggingRoutine( consoleMessage, context );
20015 * @param {Grid} grid the grid we're importing into, may be useful if you're positioning messages
20017 * @param {string} errorKey one of the i18n keys the importer can return - importer.noHeaders,
20018 * importer.noObjects, importer.invalidCsv, importer.invalidJson, importer.jsonNotArray
20019 * @param {string} consoleMessage the English console message that importer would have written
20020 * @param {object} context the context data that importer would have appended to that console message,
20021 * often the file content itself or the element that is in error
20024 if ( !gridOptions.importerErrorCallback || typeof(gridOptions.importerErrorCallback) !== 'function' ){
20025 delete gridOptions.importerErrorCallback;
20030 * @name importerDataAddCallback
20031 * @methodOf ui.grid.importer.api:GridOptions
20032 * @description A mandatory callback function that adds data to the source data array. The grid
20033 * generally doesn't add rows to the source data array, it is tidier to handle this through a user
20037 * gridOptions.importerDataAddCallback: function( grid, newObjects ) {
20038 * $scope.myData = $scope.myData.concat( newObjects );
20041 * @param {Grid} grid the grid we're importing into, may be useful in some way
20042 * @param {array} newObjects an array of new objects that you should add to your data
20045 if ( gridOptions.enableImporter === true && !gridOptions.importerDataAddCallback ) {
20046 gridUtil.logError("You have not set an importerDataAddCallback, importer is disabled");
20047 gridOptions.enableImporter = false;
20052 * @name importerNewObject
20053 * @propertyOf ui.grid.importer.api:GridOptions
20054 * @description An object on which we call `new` to create each new row before inserting it into
20055 * the data array. Typically this would be a $resource entity, which means that if you're using
20056 * the rowEdit feature, you can directly call save on this entity when the save event is triggered.
20058 * Defaults to a vanilla javascript object
20062 * gridOptions.importerNewObject = MyRes;
20069 * @propertyOf ui.grid.importer.api:GridOptions
20070 * @name importerShowMenu
20071 * @description Whether or not to show an item in the grid menu. Defaults to true.
20074 gridOptions.importerShowMenu = gridOptions.importerShowMenu !== false;
20078 * @methodOf ui.grid.importer.api:GridOptions
20079 * @name importerObjectCallback
20080 * @description A callback that massages the data for each object. For example,
20081 * you might have data stored as a code value, but display the decode. This callback
20082 * can be used to change the decoded value back into a code. Defaults to doing nothing.
20083 * @param {Grid} grid in case you need it
20084 * @param {object} newObject the new object as importer has created it, modify it
20085 * then return the modified version
20086 * @returns {object} the modified object
20089 * gridOptions.importerObjectCallback = function ( grid, newObject ) {
20090 * switch newObject.status {
20092 * newObject.status = 1;
20095 * newObject.status = 2;
20098 * return newObject;
20102 gridOptions.importerObjectCallback = gridOptions.importerObjectCallback || function( grid, newObject ) { return newObject; };
20109 * @methodOf ui.grid.importer.service:uiGridImporterService
20110 * @description Adds import menu item to the grid menu,
20111 * allowing the user to request import of a file
20112 * @param {Grid} grid the grid into which data should be imported
20114 addToMenu: function ( grid ) {
20115 grid.api.core.addToGridMenu( grid, [
20117 title: i18nService.getSafeText('gridMenu.importerTitle'),
20121 templateUrl: 'ui-grid/importerMenuItemContainer',
20122 action: function ($event) {
20123 this.grid.api.importer.importAFile( grid );
20133 * @name importThisFile
20134 * @methodOf ui.grid.importer.service:uiGridImporterService
20135 * @description Imports the provided file into the grid using the file object
20136 * provided. Bypasses the grid menu
20137 * @param {Grid} grid the grid we're importing into
20138 * @param {File} fileObject the file we want to import, as returned from the File
20139 * javascript object
20141 importThisFile: function ( grid, fileObject ) {
20143 gridUtil.logError( 'No file object provided to importThisFile, should be impossible, aborting');
20147 var reader = new FileReader();
20149 switch ( fileObject.type ){
20150 case 'application/json':
20151 reader.onload = service.importJsonClosure( grid );
20154 reader.onload = service.importCsvClosure( grid );
20158 reader.readAsText( fileObject );
20165 * @methodOf ui.grid.importer.service:uiGridImporterService
20166 * @description Creates a function that imports a json file into the grid.
20167 * The json data is imported into new objects of type `gridOptions.importerNewObject`,
20168 * and if the rowEdit feature is enabled the rows are marked as dirty
20169 * @param {Grid} grid the grid we want to import into
20170 * @param {FileObject} importFile the file that we want to import, as
20173 importJsonClosure: function( grid ) {
20174 return function( importFile ){
20175 var newObjects = [];
20178 var importArray = service.parseJson( grid, importFile );
20179 if (importArray === null){
20182 importArray.forEach( function( value, index ) {
20183 newObject = service.newObject( grid );
20184 angular.extend( newObject, value );
20185 newObject = grid.options.importerObjectCallback( grid, newObject );
20186 newObjects.push( newObject );
20189 service.addObjects( grid, newObjects );
20198 * @methodOf ui.grid.importer.service:uiGridImporterService
20199 * @description Parses a json file, returns the parsed data.
20200 * Displays an error if file doesn't parse
20201 * @param {Grid} grid the grid that we want to import into
20202 * @param {FileObject} importFile the file that we want to import, as
20204 * @returns {array} array of objects from the imported json
20206 parseJson: function( grid, importFile ){
20209 loadedObjects = JSON.parse( importFile.target.result );
20211 service.alertError( grid, 'importer.invalidJson', 'File could not be processed, is it valid json? Content was: ', importFile.target.result );
20215 if ( !Array.isArray( loadedObjects ) ){
20216 service.alertError( grid, 'importer.jsonNotarray', 'Import failed, file is not an array, file was: ', importFile.target.result );
20219 return loadedObjects;
20227 * @name importCsvClosure
20228 * @methodOf ui.grid.importer.service:uiGridImporterService
20229 * @description Creates a function that imports a csv file into the grid
20230 * (allowing it to be used in the reader.onload event)
20231 * @param {Grid} grid the grid that we want to import into
20232 * @param {FileObject} importFile the file that we want to import, as
20235 importCsvClosure: function( grid ) {
20236 return function( importFile ){
20237 var importArray = service.parseCsv( importFile );
20238 if ( !importArray || importArray.length < 1 ){
20239 service.alertError( grid, 'importer.invalidCsv', 'File could not be processed, is it valid csv? Content was: ', importFile.target.result );
20243 var newObjects = service.createCsvObjects( grid, importArray );
20244 if ( !newObjects || newObjects.length === 0 ){
20245 service.alertError( grid, 'importer.noObjects', 'Objects were not able to be derived, content was: ', importFile.target.result );
20249 service.addObjects( grid, newObjects );
20257 * @methodOf ui.grid.importer.service:uiGridImporterService
20258 * @description Parses a csv file into an array of arrays, with the first
20259 * array being the headers, and the remaining arrays being the data.
20260 * The logic for this comes from https://github.com/thetalecrafter/excel.js/blob/master/src/csv.js,
20261 * which is noted as being under the MIT license. The code is modified to pass the jscs yoda condition
20263 * @param {FileObject} importFile the file that we want to import, as a
20266 parseCsv: function( importFile ) {
20267 var csv = importFile.target.result;
20269 // use the CSV-JS library to parse
20270 return CSV.parse(csv);
20276 * @name createCsvObjects
20277 * @methodOf ui.grid.importer.service:uiGridImporterService
20278 * @description Converts an array of arrays (representing the csv file)
20279 * into a set of objects. Uses the provided `gridOptions.importerNewObject`
20280 * to create the objects, and maps the header row into the individual columns
20281 * using either `gridOptions.importerProcessHeaders`, or by using a native method
20282 * of matching to either the displayName, column name or column field of
20283 * the columns in the column defs. The resulting objects will have attributes
20284 * that are named based on the column.field or column.name, in that order.
20285 * @param {Grid} grid the grid that we want to import into
20286 * @param {Array} importArray the data that we want to import, as an array
20288 createCsvObjects: function( grid, importArray ){
20289 // pull off header row and turn into headers
20290 var headerMapping = grid.options.importerProcessHeaders( grid, importArray.shift() );
20291 if ( !headerMapping || headerMapping.length === 0 ){
20292 service.alertError( grid, 'importer.noHeaders', 'Column names could not be derived, content was: ', importArray );
20296 var newObjects = [];
20298 importArray.forEach( function( row, index ) {
20299 newObject = service.newObject( grid );
20300 if ( row !== null ){
20301 row.forEach( function( field, index ){
20302 if ( headerMapping[index] !== null ){
20303 newObject[ headerMapping[index] ] = field;
20307 newObject = grid.options.importerObjectCallback( grid, newObject );
20308 newObjects.push( newObject );
20317 * @name processHeaders
20318 * @methodOf ui.grid.importer.service:uiGridImporterService
20319 * @description Determines the columns that the header row from
20320 * a csv (or other) file represents.
20321 * @param {Grid} grid the grid we're importing into
20322 * @param {array} headerRow the header row that we wish to match against
20323 * the column definitions
20324 * @returns {array} an array of the attribute names that should be used
20325 * for that column, based on matching the headers or creating the headers
20328 processHeaders: function( grid, headerRow ) {
20330 if ( !grid.options.columnDefs || grid.options.columnDefs.length === 0 ){
20331 // we are going to create new columnDefs for all these columns, so just remove
20332 // spaces from the names to create fields
20333 headerRow.forEach( function( value, index ) {
20334 headers.push( value.replace( /[^0-9a-zA-Z\-_]/g, '_' ) );
20338 var lookupHash = service.flattenColumnDefs( grid, grid.options.columnDefs );
20339 headerRow.forEach( function( value, index ) {
20340 if ( lookupHash[value] ) {
20341 headers.push( lookupHash[value] );
20342 } else if ( lookupHash[ value.toLowerCase() ] ) {
20343 headers.push( lookupHash[ value.toLowerCase() ] );
20345 headers.push( null );
20354 * @name flattenColumnDefs
20355 * @methodOf ui.grid.importer.service:uiGridImporterService
20356 * @description Runs through the column defs and creates a hash of
20357 * the displayName, name and field, and of each of those values forced to lower case,
20358 * with each pointing to the field or name
20359 * (whichever is present). Used to lookup column headers and decide what
20360 * attribute name to give to the resulting field.
20361 * @param {Grid} grid the grid we're importing into
20362 * @param {array} columnDefs the columnDefs that we should flatten
20363 * @returns {hash} the flattened version of the column def information, allowing
20364 * us to look up a value by `flattenedHash[ headerValue ]`
20366 flattenColumnDefs: function( grid, columnDefs ){
20367 var flattenedHash = {};
20368 columnDefs.forEach( function( columnDef, index) {
20369 if ( columnDef.name ){
20370 flattenedHash[ columnDef.name ] = columnDef.field || columnDef.name;
20371 flattenedHash[ columnDef.name.toLowerCase() ] = columnDef.field || columnDef.name;
20374 if ( columnDef.field ){
20375 flattenedHash[ columnDef.field ] = columnDef.field || columnDef.name;
20376 flattenedHash[ columnDef.field.toLowerCase() ] = columnDef.field || columnDef.name;
20379 if ( columnDef.displayName ){
20380 flattenedHash[ columnDef.displayName ] = columnDef.field || columnDef.name;
20381 flattenedHash[ columnDef.displayName.toLowerCase() ] = columnDef.field || columnDef.name;
20384 if ( columnDef.displayName && grid.options.importerHeaderFilter ){
20385 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName) ] = columnDef.field || columnDef.name;
20386 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName).toLowerCase() ] = columnDef.field || columnDef.name;
20390 return flattenedHash;
20397 * @methodOf ui.grid.importer.service:uiGridImporterService
20398 * @description Inserts our new objects into the grid data, and
20399 * sets the rows to dirty if the rowEdit feature is being used
20401 * Does this by registering a watch on dataChanges, which essentially
20402 * is waiting on the result of the grid data watch, and downstream processing.
20404 * When the callback is called, it deregisters itself - we don't want to run
20405 * again next time data is added.
20407 * If we never get called, we deregister on destroy.
20409 * @param {Grid} grid the grid we're importing into
20410 * @param {array} newObjects the objects we want to insert into the grid data
20411 * @returns {object} the new object
20413 addObjects: function( grid, newObjects, $scope ){
20414 if ( grid.api.rowEdit ){
20415 var dataChangeDereg = grid.registerDataChangeCallback( function() {
20416 grid.api.rowEdit.setRowsDirty( newObjects );
20418 }, [uiGridConstants.dataChange.ROW] );
20420 grid.importer.$scope.$on( '$destroy', dataChangeDereg );
20423 grid.importer.$scope.$apply( grid.options.importerDataAddCallback( grid, newObjects ) );
20431 * @methodOf ui.grid.importer.service:uiGridImporterService
20432 * @description Makes a new object based on `gridOptions.importerNewObject`,
20433 * or based on an empty object if not present
20434 * @param {Grid} grid the grid we're importing into
20435 * @returns {object} the new object
20437 newObject: function( grid ){
20438 if ( typeof(grid.options) !== "undefined" && typeof(grid.options.importerNewObject) !== "undefined" ){
20439 return new grid.options.importerNewObject();
20449 * @methodOf ui.grid.importer.service:uiGridImporterService
20450 * @description Provides an internationalised user alert for the failure,
20451 * and logs a console message including diagnostic content.
20452 * Optionally, if the the `gridOptions.importerErrorCallback` routine
20453 * is defined, then calls that instead, allowing user specified error routines
20454 * @param {Grid} grid the grid we're importing into
20455 * @param {array} headerRow the header row that we wish to match against
20456 * the column definitions
20458 alertError: function( grid, alertI18nToken, consoleMessage, context ){
20459 if ( grid.options.importerErrorCallback ){
20460 grid.options.importerErrorCallback( grid, alertI18nToken, consoleMessage, context );
20462 $window.alert(i18nService.getSafeText( alertI18nToken ));
20463 gridUtil.logError(consoleMessage + context );
20475 * @name ui.grid.importer.directive:uiGridImporter
20479 * @description Adds importer features to grid
20482 module.directive('uiGridImporter', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
20483 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
20487 require: '^uiGrid',
20489 link: function ($scope, $elm, $attrs, uiGridCtrl) {
20490 uiGridImporterService.initializeGrid($scope, uiGridCtrl.grid);
20498 * @name ui.grid.importer.directive:uiGridImporterMenuItem
20502 * @description Handles the processing from the importer menu item - once a file is
20506 module.directive('uiGridImporterMenuItem', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
20507 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
20511 require: '^uiGrid',
20513 templateUrl: 'ui-grid/importerMenuItem',
20514 link: function ($scope, $elm, $attrs, uiGridCtrl) {
20515 var handleFileSelect = function( event ){
20516 var target = event.srcElement || event.target;
20518 if (target && target.files && target.files.length === 1) {
20519 var fileObject = target.files[0];
20520 uiGridImporterService.importThisFile( grid, fileObject );
20521 target.form.reset();
20525 var fileChooser = $elm[0].querySelectorAll('.ui-grid-importer-file-chooser');
20526 var grid = uiGridCtrl.grid;
20528 if ( fileChooser.length !== 1 ){
20529 gridUtil.logError('Found > 1 or < 1 file choosers within the menu item, error, cannot continue');
20531 fileChooser[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
20543 * @name ui.grid.infiniteScroll
20547 * #ui.grid.infiniteScroll
20549 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
20551 * This module provides infinite scroll functionality to ui-grid
20554 var module = angular.module('ui.grid.infiniteScroll', ['ui.grid']);
20557 * @name ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20559 * @description Service for infinite scroll features
20561 module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', '$q', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent, $q) {
20567 * @name initializeGrid
20568 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20569 * @description This method register events and methods into grid public API
20572 initializeGrid: function(grid, $scope) {
20573 service.defaultGridOptions(grid.options);
20575 if (!grid.options.enableInfiniteScroll){
20579 grid.infiniteScroll = { dataLoading: false };
20580 service.setScrollDirections( grid, grid.options.infiniteScrollUp, grid.options.infiniteScrollDown );
20581 grid.api.core.on.scrollEnd($scope, service.handleScroll);
20585 * @name ui.grid.infiniteScroll.api:PublicAPI
20587 * @description Public API for infinite scroll feature
20595 * @name needLoadMoreData
20596 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
20597 * @description This event fires when scroll reaches bottom percentage of grid
20598 * and needs to load data
20601 needLoadMoreData: function ($scope, fn) {
20606 * @name needLoadMoreDataTop
20607 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
20608 * @description This event fires when scroll reaches top percentage of grid
20609 * and needs to load data
20612 needLoadMoreDataTop: function ($scope, fn) {
20622 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20623 * @description Call this function when you have loaded the additional data
20624 * requested. You should set scrollUp and scrollDown to indicate
20625 * whether there are still more pages in each direction.
20627 * If you call dataLoaded without first calling `saveScrollPercentage` then we will
20628 * scroll the user to the start of the newly loaded data, which usually gives a smooth scroll
20629 * experience, but can give a jumpy experience with large `infiniteScrollRowsFromEnd` values, and
20630 * on variable speed internet connections. Using `saveScrollPercentage` as demonstrated in the tutorial
20631 * should give a smoother scrolling experience for users.
20633 * See infinite_scroll tutorial for example of usage
20634 * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire
20635 * any more infinite scroll events upward
20636 * @param {boolean} scrollDown if set to false flags that there are no more pages downwards, so don't
20637 * fire any more infinite scroll events downward
20638 * @returns {promise} a promise that is resolved when the grid scrolling is fully adjusted. If you're
20639 * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning
20641 dataLoaded: function( scrollUp, scrollDown ) {
20642 service.setScrollDirections(grid, scrollUp, scrollDown);
20644 var promise = service.adjustScroll(grid).then(function() {
20645 grid.infiniteScroll.dataLoading = false;
20653 * @name resetScroll
20654 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20655 * @description Call this function when you have taken some action that makes the current
20656 * scroll position invalid. For example, if you're using external sorting and you've resorted
20657 * then you might reset the scroll, or if you've otherwise substantially changed the data, perhaps
20658 * you've reused an existing grid for a new data set
20660 * You must tell us whether there is data upwards or downwards after the reset
20662 * @param {boolean} scrollUp flag that there are pages upwards, fire
20663 * infinite scroll events upward
20664 * @param {boolean} scrollDown flag that there are pages downwards, so
20665 * fire infinite scroll events downward
20666 * @returns {promise} promise that is resolved when the scroll reset is complete
20668 resetScroll: function( scrollUp, scrollDown ) {
20669 service.setScrollDirections( grid, scrollUp, scrollDown);
20671 return service.adjustInfiniteScrollPosition(grid, 0);
20677 * @name saveScrollPercentage
20678 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20679 * @description Saves the scroll percentage and number of visible rows before you adjust the data,
20680 * used if you're subsequently going to call `dataRemovedTop` or `dataRemovedBottom`
20682 saveScrollPercentage: function() {
20683 grid.infiniteScroll.prevScrollTop = grid.renderContainers.body.prevScrollTop;
20684 grid.infiniteScroll.previousVisibleRows = grid.getVisibleRowCount();
20690 * @name dataRemovedTop
20691 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20692 * @description Adjusts the scroll position after you've removed data at the top
20693 * @param {boolean} scrollUp flag that there are pages upwards, fire
20694 * infinite scroll events upward
20695 * @param {boolean} scrollDown flag that there are pages downwards, so
20696 * fire infinite scroll events downward
20698 dataRemovedTop: function( scrollUp, scrollDown ) {
20699 service.dataRemovedTop( grid, scrollUp, scrollDown );
20704 * @name dataRemovedBottom
20705 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20706 * @description Adjusts the scroll position after you've removed data at the bottom
20707 * @param {boolean} scrollUp flag that there are pages upwards, fire
20708 * infinite scroll events upward
20709 * @param {boolean} scrollDown flag that there are pages downwards, so
20710 * fire infinite scroll events downward
20712 dataRemovedBottom: function( scrollUp, scrollDown ) {
20713 service.dataRemovedBottom( grid, scrollUp, scrollDown );
20718 * @name setScrollDirections
20719 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20720 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20721 * and also sets the grid.suppressParentScroll
20722 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20723 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20725 setScrollDirections: function ( scrollUp, scrollDown ) {
20726 service.setScrollDirections( grid, scrollUp, scrollDown );
20732 grid.api.registerEventsFromObject(publicApi.events);
20733 grid.api.registerMethodsFromObject(publicApi.methods);
20737 defaultGridOptions: function (gridOptions) {
20738 //default option to true unless it was explicitly set to false
20741 * @name ui.grid.infiniteScroll.api:GridOptions
20743 * @description GridOptions for infinite scroll feature, these are available to be
20744 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
20749 * @name enableInfiniteScroll
20750 * @propertyOf ui.grid.infiniteScroll.api:GridOptions
20751 * @description Enable infinite scrolling for this grid
20752 * <br/>Defaults to true
20754 gridOptions.enableInfiniteScroll = gridOptions.enableInfiniteScroll !== false;
20758 * @name infiniteScrollRowsFromEnd
20759 * @propertyOf ui.grid.class:GridOptions
20760 * @description This setting controls how close to the end of the dataset a user gets before
20761 * more data is requested by the infinite scroll, whether scrolling up or down. This allows you to
20762 * 'prefetch' rows before the user actually runs out of scrolling.
20764 * Note that if you set this value too high it may give jumpy scrolling behaviour, if you're getting
20765 * this behaviour you could use the `saveScrollPercentageMethod` right before loading your data, and we'll
20766 * preserve that scroll position
20768 * <br> Defaults to 20
20770 gridOptions.infiniteScrollRowsFromEnd = gridOptions.infiniteScrollRowsFromEnd || 20;
20774 * @name infiniteScrollUp
20775 * @propertyOf ui.grid.class:GridOptions
20776 * @description Whether you allow infinite scroll up, implying that the first page of data
20777 * you have displayed is in the middle of your data set. If set to true then we trigger the
20778 * needMoreDataTop event when the user hits the top of the scrollbar.
20779 * <br> Defaults to false
20781 gridOptions.infiniteScrollUp = gridOptions.infiniteScrollUp === true;
20785 * @name infiniteScrollDown
20786 * @propertyOf ui.grid.class:GridOptions
20787 * @description Whether you allow infinite scroll down, implying that the first page of data
20788 * you have displayed is in the middle of your data set. If set to true then we trigger the
20789 * needMoreData event when the user hits the bottom of the scrollbar.
20790 * <br> Defaults to true
20792 gridOptions.infiniteScrollDown = gridOptions.infiniteScrollDown !== false;
20798 * @name setScrollDirections
20799 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20800 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20801 * and also sets the grid.suppressParentScroll
20802 * @param {grid} grid the grid we're operating on
20803 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20804 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20806 setScrollDirections: function ( grid, scrollUp, scrollDown ) {
20807 grid.infiniteScroll.scrollUp = ( scrollUp === true );
20808 grid.suppressParentScrollUp = ( scrollUp === true );
20810 grid.infiniteScroll.scrollDown = ( scrollDown !== false);
20811 grid.suppressParentScrollDown = ( scrollDown !== false);
20817 * @name handleScroll
20818 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20819 * @description Called whenever the grid scrolls, determines whether the scroll should
20820 * trigger an infinite scroll request for more data
20821 * @param {object} args the args from the event
20823 handleScroll: function (args) {
20824 // don't request data if already waiting for data, or if source is coming from ui.grid.adjustInfiniteScrollPosition() function
20825 if ( args.grid.infiniteScroll && args.grid.infiniteScroll.dataLoading || args.source === 'ui.grid.adjustInfiniteScrollPosition' ){
20831 var targetPercentage = args.grid.options.infiniteScrollRowsFromEnd / args.grid.renderContainers.body.visibleRowCache.length;
20832 if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) {
20833 percentage = args.y.percentage;
20834 if (percentage <= targetPercentage){
20835 service.loadData(args.grid);
20837 } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) {
20838 percentage = 1 - args.y.percentage;
20839 if (percentage <= targetPercentage){
20840 service.loadData(args.grid);
20850 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20851 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20852 * and whether there are more pages upwards or downwards. It also stores the number of rows that we had previously,
20853 * and clears out any saved scroll position so that we know whether or not the user calls `saveScrollPercentage`
20854 * @param {Grid} grid the grid we're working on
20856 loadData: function (grid) {
20857 // save number of currently visible rows to calculate new scroll position later - we know that we want
20858 // to be at approximately the row we're currently at
20859 grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20860 grid.infiniteScroll.direction = grid.scrollDirection;
20861 delete grid.infiniteScroll.prevScrollTop;
20863 if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) {
20864 grid.infiniteScroll.dataLoading = true;
20865 grid.api.infiniteScroll.raise.needLoadMoreDataTop();
20866 } else if (grid.scrollDirection === uiGridConstants.scrollDirection.DOWN && grid.infiniteScroll.scrollDown ) {
20867 grid.infiniteScroll.dataLoading = true;
20868 grid.api.infiniteScroll.raise.needLoadMoreData();
20875 * @name adjustScroll
20876 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20877 * @description Once we are informed that data has been loaded, adjust the scroll position to account for that
20878 * addition and to make things look clean.
20880 * If we're scrolling up we scroll to the first row of the old data set -
20881 * so we're assuming that you would have gotten to the top of the grid (from the 20% need more data trigger) by
20882 * the time the data comes back. If we're scrolling down we scoll to the last row of the old data set - so we're
20883 * assuming that you would have gotten to the bottom of the grid (from the 80% need more data trigger) by the time
20884 * the data comes back.
20886 * Neither of these are good assumptions, but making this a smoother experience really requires
20887 * that trigger to not be a percentage, and to be much closer to the end of the data (say, 5 rows off the end). Even then
20888 * it'd be better still to actually run into the end. But if the data takes a while to come back, they may have scrolled
20889 * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for
20890 * now, until someone wants to do better.
20891 * @param {Grid} grid the grid we're working on
20892 * @returns {promise} a promise that is resolved when scrolling has finished
20894 adjustScroll: function(grid){
20895 var promise = $q.defer();
20896 $timeout(function () {
20897 var newPercentage, viewportHeight, rowHeight, newVisibleRows, oldTop, newTop;
20899 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight;
20900 rowHeight = grid.options.rowHeight;
20902 if ( grid.infiniteScroll.direction === undefined ){
20903 // called from initialize, tweak our scroll up a little
20904 service.adjustInfiniteScrollPosition(grid, 0);
20907 newVisibleRows = grid.getVisibleRowCount();
20909 // in case not enough data is loaded to enable scroller - load more data
20910 var canvasHeight = rowHeight * newVisibleRows;
20911 if (grid.infiniteScroll.scrollDown && (viewportHeight > canvasHeight)) {
20912 grid.api.infiniteScroll.raise.needLoadMoreData();
20915 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){
20916 oldTop = grid.infiniteScroll.prevScrollTop || 0;
20917 newTop = oldTop + (newVisibleRows - grid.infiniteScroll.previousVisibleRows)*rowHeight;
20918 service.adjustInfiniteScrollPosition(grid, newTop);
20919 $timeout( function() {
20924 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){
20925 newTop = grid.infiniteScroll.prevScrollTop || (grid.infiniteScroll.previousVisibleRows*rowHeight - viewportHeight);
20926 service.adjustInfiniteScrollPosition(grid, newTop);
20927 $timeout( function() {
20933 return promise.promise;
20939 * @name adjustInfiniteScrollPosition
20940 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20941 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20942 * @param {Grid} grid the grid we're working on
20943 * @param {number} scrollTop the position through the grid that we want to scroll to
20944 * @returns {promise} a promise that is resolved when the scrolling finishes
20946 adjustInfiniteScrollPosition: function (grid, scrollTop) {
20947 var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'),
20948 visibleRows = grid.getVisibleRowCount(),
20949 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight,
20950 rowHeight = grid.options.rowHeight,
20951 scrollHeight = visibleRows*rowHeight-viewportHeight;
20953 //for infinite scroll, if there are pages upwards then never allow it to be at the zero position so the up button can be active
20954 if (scrollTop === 0 && grid.infiniteScroll.scrollUp) {
20955 // using pixels results in a relative scroll, hence we have to use percentage
20956 scrollEvent.y = {percentage: 1/scrollHeight};
20959 scrollEvent.y = {percentage: scrollTop/scrollHeight};
20961 grid.scrollContainers('', scrollEvent);
20967 * @name dataRemovedTop
20968 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20969 * @description Adjusts the scroll position after you've removed data at the top. You should
20970 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20971 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20972 * before you start removing data
20973 * @param {Grid} grid the grid we're working on
20974 * @param {boolean} scrollUp flag that there are pages upwards, fire
20975 * infinite scroll events upward
20976 * @param {boolean} scrollDown flag that there are pages downwards, so
20977 * fire infinite scroll events downward
20978 * @returns {promise} a promise that is resolved when the scrolling finishes
20980 dataRemovedTop: function( grid, scrollUp, scrollDown ) {
20981 var newVisibleRows, oldTop, newTop, rowHeight;
20982 service.setScrollDirections( grid, scrollUp, scrollDown );
20984 newVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20985 oldTop = grid.infiniteScroll.prevScrollTop;
20986 rowHeight = grid.options.rowHeight;
20988 // since we removed from the top, our new scroll row will be the old scroll row less the number
20990 newTop = oldTop - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows )*rowHeight;
20992 return service.adjustInfiniteScrollPosition( grid, newTop );
20997 * @name dataRemovedBottom
20998 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20999 * @description Adjusts the scroll position after you've removed data at the bottom. You should
21000 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
21001 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
21002 * before you start removing data
21003 * @param {Grid} grid the grid we're working on
21004 * @param {boolean} scrollUp flag that there are pages upwards, fire
21005 * infinite scroll events upward
21006 * @param {boolean} scrollDown flag that there are pages downwards, so
21007 * fire infinite scroll events downward
21009 dataRemovedBottom: function( grid, scrollUp, scrollDown ) {
21011 service.setScrollDirections( grid, scrollUp, scrollDown );
21013 newTop = grid.infiniteScroll.prevScrollTop;
21015 return service.adjustInfiniteScrollPosition( grid, newTop );
21022 * @name ui.grid.infiniteScroll.directive:uiGridInfiniteScroll
21026 * @description Adds infinite scroll features to grid
21029 <example module="app">
21030 <file name="app.js">
21031 var app = angular.module('app', ['ui.grid', 'ui.grid.infiniteScroll']);
21033 app.controller('MainCtrl', ['$scope', function ($scope) {
21035 { name: 'Alex', car: 'Toyota' },
21036 { name: 'Sam', car: 'Lexus' }
21039 $scope.columnDefs = [
21045 <file name="index.html">
21046 <div ng-controller="MainCtrl">
21047 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-infinite-scroll="20"></div>
21053 module.directive('uiGridInfiniteScroll', ['uiGridInfiniteScrollService',
21054 function (uiGridInfiniteScrollService) {
21058 require: '^uiGrid',
21059 compile: function($scope, $elm, $attr){
21061 pre: function($scope, $elm, $attr, uiGridCtrl) {
21062 uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid, $scope);
21064 post: function($scope, $elm, $attr) {
21078 * @name ui.grid.moveColumns
21081 * # ui.grid.moveColumns
21083 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
21085 * This module provides column moving capability to ui.grid. It enables to change the position of columns.
21086 * <div doc-module-components="ui.grid.moveColumns"></div>
21088 var module = angular.module('ui.grid.moveColumns', ['ui.grid']);
21092 * @name ui.grid.moveColumns.service:uiGridMoveColumnService
21093 * @description Service for column moving feature.
21095 module.service('uiGridMoveColumnService', ['$q', '$timeout', '$log', 'ScrollEvent', 'uiGridConstants', 'gridUtil', function ($q, $timeout, $log, ScrollEvent, uiGridConstants, gridUtil) {
21098 initializeGrid: function (grid) {
21100 this.registerPublicApi(grid);
21101 this.defaultGridOptions(grid.options);
21102 grid.moveColumns = {orderCache: []}; // Used to cache the order before columns are rebuilt
21103 grid.registerColumnBuilder(self.movableColumnBuilder);
21104 grid.registerDataChangeCallback(self.verifyColumnOrder, [uiGridConstants.dataChange.COLUMN]);
21106 registerPublicApi: function (grid) {
21110 * @name ui.grid.moveColumns.api:PublicApi
21111 * @description Public Api for column moving feature.
21117 * @name columnPositionChanged
21118 * @eventOf ui.grid.moveColumns.api:PublicApi
21119 * @description raised when column is moved
21121 * gridApi.colMovable.on.columnPositionChanged(scope,function(colDef, originalPosition, newPosition){})
21123 * @param {object} colDef the column that was moved
21124 * @param {integer} originalPosition of the column
21125 * @param {integer} finalPosition of the column
21128 columnPositionChanged: function (colDef, originalPosition, newPosition) {
21136 * @methodOf ui.grid.moveColumns.api:PublicApi
21137 * @description Method can be used to change column position.
21139 * gridApi.colMovable.moveColumn(oldPosition, newPosition)
21141 * @param {integer} originalPosition of the column
21142 * @param {integer} finalPosition of the column
21145 moveColumn: function (originalPosition, finalPosition) {
21146 var columns = grid.columns;
21147 if (!angular.isNumber(originalPosition) || !angular.isNumber(finalPosition)) {
21148 gridUtil.logError('MoveColumn: Please provide valid values for originalPosition and finalPosition');
21151 var nonMovableColumns = 0;
21152 for (var i = 0; i < columns.length; i++) {
21153 if ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true) {
21154 nonMovableColumns++;
21157 if (originalPosition >= (columns.length - nonMovableColumns) || finalPosition >= (columns.length - nonMovableColumns)) {
21158 gridUtil.logError('MoveColumn: Invalid values for originalPosition, finalPosition');
21161 var findPositionForRenderIndex = function (index) {
21162 var position = index;
21163 for (var i = 0; i <= position; i++) {
21164 if (angular.isDefined(columns[i]) && ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true)) {
21170 self.redrawColumnAtPosition(grid, findPositionForRenderIndex(originalPosition), findPositionForRenderIndex(finalPosition));
21175 grid.api.registerEventsFromObject(publicApi.events);
21176 grid.api.registerMethodsFromObject(publicApi.methods);
21178 defaultGridOptions: function (gridOptions) {
21181 * @name ui.grid.moveColumns.api:GridOptions
21183 * @description Options for configuring the move column feature, these are available to be
21184 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21188 * @name enableColumnMoving
21189 * @propertyOf ui.grid.moveColumns.api:GridOptions
21190 * @description If defined, sets the default value for the colMovable flag on each individual colDefs
21191 * if their individual enableColumnMoving configuration is not defined. Defaults to true.
21193 gridOptions.enableColumnMoving = gridOptions.enableColumnMoving !== false;
21195 movableColumnBuilder: function (colDef, col, gridOptions) {
21199 * @name ui.grid.moveColumns.api:ColumnDef
21201 * @description Column Definition for move column feature, these are available to be
21202 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
21206 * @name enableColumnMoving
21207 * @propertyOf ui.grid.moveColumns.api:ColumnDef
21208 * @description Enable column moving for the column.
21210 colDef.enableColumnMoving = colDef.enableColumnMoving === undefined ? gridOptions.enableColumnMoving
21211 : colDef.enableColumnMoving;
21212 return $q.all(promises);
21216 * @name updateColumnCache
21217 * @methodOf ui.grid.moveColumns
21218 * @description Cache the current order of columns, so we can restore them after new columnDefs are defined
21220 updateColumnCache: function(grid){
21221 grid.moveColumns.orderCache = grid.getOnlyDataColumns();
21225 * @name verifyColumnOrder
21226 * @methodOf ui.grid.moveColumns
21227 * @description dataChangeCallback which uses the cached column order to restore the column order
21228 * when it is reset by altering the columnDefs array.
21230 verifyColumnOrder: function(grid){
21231 var headerRowOffset = grid.rowHeaderColumns.length;
21234 angular.forEach(grid.moveColumns.orderCache, function(cacheCol, cacheIndex){
21235 newIndex = grid.columns.indexOf(cacheCol);
21236 if ( newIndex !== -1 && newIndex - headerRowOffset !== cacheIndex ){
21237 var column = grid.columns.splice(newIndex, 1)[0];
21238 grid.columns.splice(cacheIndex + headerRowOffset, 0, column);
21242 redrawColumnAtPosition: function (grid, originalPosition, newPosition) {
21243 if (originalPosition === newPosition) {
21247 var columns = grid.columns;
21249 var originalColumn = columns[originalPosition];
21250 if (originalColumn.colDef.enableColumnMoving) {
21251 if (originalPosition > newPosition) {
21252 for (var i1 = originalPosition; i1 > newPosition; i1--) {
21253 columns[i1] = columns[i1 - 1];
21256 else if (newPosition > originalPosition) {
21257 for (var i2 = originalPosition; i2 < newPosition; i2++) {
21258 columns[i2] = columns[i2 + 1];
21261 columns[newPosition] = originalColumn;
21262 service.updateColumnCache(grid);
21263 grid.queueGridRefresh();
21264 $timeout(function () {
21265 grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
21266 grid.api.colMovable.raise.columnPositionChanged(originalColumn.colDef, originalPosition, newPosition);
21276 * @name ui.grid.moveColumns.directive:uiGridMoveColumns
21279 * @description Adds column moving features to the ui-grid directive.
21281 <example module="app">
21282 <file name="app.js">
21283 var app = angular.module('app', ['ui.grid', 'ui.grid.moveColumns']);
21284 app.controller('MainCtrl', ['$scope', function ($scope) {
21286 { name: 'Bob', title: 'CEO', age: 45 },
21287 { name: 'Frank', title: 'Lowly Developer', age: 25 },
21288 { name: 'Jenny', title: 'Highly Developer', age: 35 }
21290 $scope.columnDefs = [
21297 <file name="main.css">
21303 <file name="index.html">
21304 <div ng-controller="MainCtrl">
21305 <div class="grid" ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-move-columns></div>
21310 module.directive('uiGridMoveColumns', ['uiGridMoveColumnService', function (uiGridMoveColumnService) {
21314 require: '^uiGrid',
21316 compile: function () {
21318 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
21319 uiGridMoveColumnService.initializeGrid(uiGridCtrl.grid);
21321 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21330 * @name ui.grid.moveColumns.directive:uiGridHeaderCell
21334 * @description Stacks on top of ui.grid.uiGridHeaderCell to provide capability to be able to move it to reposition column.
21336 * On receiving mouseDown event headerCell is cloned, now as the mouse moves the cloned header cell also moved in the grid.
21337 * In case the moving cloned header cell reaches the left or right extreme of grid, grid scrolling is triggered (if horizontal scroll exists).
21338 * On mouseUp event column is repositioned at position where mouse is released and cloned header cell is removed.
21340 * Events that invoke cloning of header cell:
21343 * Events that invoke movement of cloned header cell:
21346 * Events that invoke repositioning of column:
21349 module.directive('uiGridHeaderCell', ['$q', 'gridUtil', 'uiGridMoveColumnService', '$document', '$log', 'uiGridConstants', 'ScrollEvent',
21350 function ($q, gridUtil, uiGridMoveColumnService, $document, $log, uiGridConstants, ScrollEvent) {
21353 require: '^uiGrid',
21354 compile: function () {
21356 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21358 if ($scope.col.colDef.enableColumnMoving) {
21361 * Our general approach to column move is that we listen to a touchstart or mousedown
21362 * event over the column header. When we hear one, then we wait for a move of the same type
21363 * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for
21364 * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move,
21365 * and we instead get a mouseup or a touchend, then we just drop out again and do nothing.
21368 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
21371 var previousMouseX;
21372 var totalMouseMovement;
21373 var rightMoveLimit;
21374 var elmCloned = false;
21377 var moveOccurred = false;
21379 var downFn = function( event ){
21380 //Setting some variables required for calculations.
21381 gridLeft = $scope.grid.element[0].getBoundingClientRect().left;
21382 if ( $scope.grid.hasLeftContainer() ){
21383 gridLeft += $scope.grid.renderContainers.left.header[0].getBoundingClientRect().width;
21386 previousMouseX = event.pageX;
21387 totalMouseMovement = 0;
21388 rightMoveLimit = gridLeft + $scope.grid.getViewportWidth();
21390 if ( event.type === 'mousedown' ){
21391 $document.on('mousemove', moveFn);
21392 $document.on('mouseup', upFn);
21393 } else if ( event.type === 'touchstart' ){
21394 $document.on('touchmove', moveFn);
21395 $document.on('touchend', upFn);
21399 var moveFn = function( event ) {
21400 var changeValue = event.pageX - previousMouseX;
21401 if ( changeValue === 0 ){ return; }
21402 //Disable text selection in Chrome during column move
21403 document.onselectstart = function() { return false; };
21405 moveOccurred = true;
21410 else if (elmCloned) {
21411 moveElement(changeValue);
21412 previousMouseX = event.pageX;
21416 var upFn = function( event ){
21417 //Re-enable text selection after column move
21418 document.onselectstart = null;
21420 //Remove the cloned element on mouse up.
21422 movingElm.remove();
21429 if (!moveOccurred){
21433 var columns = $scope.grid.columns;
21434 var columnIndex = 0;
21435 for (var i = 0; i < columns.length; i++) {
21436 if (columns[i].colDef.name !== $scope.col.colDef.name) {
21446 //Case where column should be moved to a position on its left
21447 if (totalMouseMovement < 0) {
21448 var totalColumnsLeftWidth = 0;
21450 if ( $scope.grid.isRTL() ){
21451 for (il = columnIndex + 1; il < columns.length; il++) {
21452 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
21453 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
21454 if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) {
21455 uiGridMoveColumnService.redrawColumnAtPosition
21456 ($scope.grid, columnIndex, il - 1);
21463 for (il = columnIndex - 1; il >= 0; il--) {
21464 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
21465 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
21466 if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) {
21467 uiGridMoveColumnService.redrawColumnAtPosition
21468 ($scope.grid, columnIndex, il + 1);
21475 //Case where column should be moved to beginning (or end in RTL) of the grid.
21476 if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) {
21478 if ( $scope.grid.isRTL() ){
21479 targetIndex = columns.length - 1;
21481 uiGridMoveColumnService.redrawColumnAtPosition
21482 ($scope.grid, columnIndex, targetIndex);
21486 //Case where column should be moved to a position on its right
21487 else if (totalMouseMovement > 0) {
21488 var totalColumnsRightWidth = 0;
21490 if ( $scope.grid.isRTL() ){
21491 for (ir = columnIndex - 1; ir > 0; ir--) {
21492 if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) {
21493 totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width;
21494 if (totalColumnsRightWidth > totalMouseMovement) {
21495 uiGridMoveColumnService.redrawColumnAtPosition
21496 ($scope.grid, columnIndex, ir);
21503 for (ir = columnIndex + 1; ir < columns.length; ir++) {
21504 if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) {
21505 totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width;
21506 if (totalColumnsRightWidth > totalMouseMovement) {
21507 uiGridMoveColumnService.redrawColumnAtPosition
21508 ($scope.grid, columnIndex, ir - 1);
21516 //Case where column should be moved to end (or beginning in RTL) of the grid.
21517 if (totalColumnsRightWidth < totalMouseMovement) {
21518 targetIndex = columns.length - 1;
21519 if ( $scope.grid.isRTL() ){
21522 uiGridMoveColumnService.redrawColumnAtPosition
21523 ($scope.grid, columnIndex, targetIndex);
21531 var onDownEvents = function(){
21532 $contentsElm.on('touchstart', downFn);
21533 $contentsElm.on('mousedown', downFn);
21536 var offAllEvents = function() {
21537 $contentsElm.off('touchstart', downFn);
21538 $contentsElm.off('mousedown', downFn);
21540 $document.off('mousemove', moveFn);
21541 $document.off('touchmove', moveFn);
21543 $document.off('mouseup', upFn);
21544 $document.off('touchend', upFn);
21550 var cloneElement = function () {
21553 //Cloning header cell and appending to current header cell.
21554 movingElm = $elm.clone();
21555 $elm.parent().append(movingElm);
21557 //Left of cloned element should be aligned to original header cell.
21558 movingElm.addClass('movingColumn');
21559 var movingElementStyles = {};
21560 movingElementStyles.left = $elm[0].offsetLeft + 'px';
21561 var gridRight = $scope.grid.element[0].getBoundingClientRect().right;
21562 var elmRight = $elm[0].getBoundingClientRect().right;
21563 if (elmRight > gridRight) {
21564 reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight);
21565 movingElementStyles.width = reducedWidth + 'px';
21567 movingElm.css(movingElementStyles);
21570 var moveElement = function (changeValue) {
21571 //Calculate total column width
21572 var columns = $scope.grid.columns;
21573 var totalColumnWidth = 0;
21574 for (var i = 0; i < columns.length; i++) {
21575 if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) {
21576 totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width;
21580 //Calculate new position of left of column
21581 var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1;
21582 var currentElmRight = movingElm[0].getBoundingClientRect().right;
21583 var newElementLeft;
21585 newElementLeft = currentElmLeft - gridLeft + changeValue;
21586 newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit;
21588 //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid
21589 if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) {
21590 movingElm.css({visibility: 'visible', 'left': (movingElm[0].offsetLeft +
21591 (newElementLeft < rightMoveLimit ? changeValue : (rightMoveLimit - currentElmLeft))) + 'px'});
21593 else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) {
21595 var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement');
21596 scrollEvent.x = {pixels: changeValue};
21597 scrollEvent.grid.scrollContainers('',scrollEvent);
21600 //Calculate total width of columns on the left of the moving column and the mouse movement
21601 var totalColumnsLeftWidth = 0;
21602 for (var il = 0; il < columns.length; il++) {
21603 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
21604 if (columns[il].colDef.name !== $scope.col.colDef.name) {
21605 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
21612 if ($scope.newScrollLeft === undefined) {
21613 totalMouseMovement += changeValue;
21616 totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth;
21619 //Increase width of moving column, in case the rightmost column was moved and its width was
21620 //decreased because of overflow
21621 if (reducedWidth < $scope.col.drawnWidth) {
21622 reducedWidth += Math.abs(changeValue);
21623 movingElm.css({'width': reducedWidth + 'px'});
21639 * @name ui.grid.pagination
21643 * # ui.grid.pagination
21645 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
21647 * This module provides pagination support to ui-grid
21649 var module = angular.module('ui.grid.pagination', ['ng', 'ui.grid']);
21653 * @name ui.grid.pagination.service:uiGridPaginationService
21655 * @description Service for the pagination feature
21657 module.service('uiGridPaginationService', ['gridUtil',
21658 function (gridUtil) {
21662 * @name initializeGrid
21663 * @methodOf ui.grid.pagination.service:uiGridPaginationService
21664 * @description Attaches the service to a certain grid
21665 * @param {Grid} grid The grid we want to work with
21667 initializeGrid: function (grid) {
21668 service.defaultGridOptions(grid.options);
21672 * @name ui.grid.pagination.api:PublicAPI
21674 * @description Public API for the pagination feature
21681 * @name paginationChanged
21682 * @eventOf ui.grid.pagination.api:PublicAPI
21683 * @description This event fires when the pageSize or currentPage changes
21684 * @param {int} currentPage requested page number
21685 * @param {int} pageSize requested page size
21687 paginationChanged: function (currentPage, pageSize) { }
21695 * @methodOf ui.grid.pagination.api:PublicAPI
21696 * @description Returns the number of the current page
21698 getPage: function () {
21699 return grid.options.enablePagination ? grid.options.paginationCurrentPage : null;
21703 * @name getTotalPages
21704 * @methodOf ui.grid.pagination.api:PublicAPI
21705 * @description Returns the total number of pages
21707 getTotalPages: function () {
21708 if (!grid.options.enablePagination) {
21712 return (grid.options.totalItems === 0) ? 1 : Math.ceil(grid.options.totalItems / grid.options.paginationPageSize);
21717 * @methodOf ui.grid.pagination.api:PublicAPI
21718 * @description Moves to the next page, if possible
21720 nextPage: function () {
21721 if (!grid.options.enablePagination) {
21725 if (grid.options.totalItems > 0) {
21726 grid.options.paginationCurrentPage = Math.min(
21727 grid.options.paginationCurrentPage + 1,
21728 publicApi.methods.pagination.getTotalPages()
21731 grid.options.paginationCurrentPage++;
21736 * @name previousPage
21737 * @methodOf ui.grid.pagination.api:PublicAPI
21738 * @description Moves to the previous page, if we're not on the first page
21740 previousPage: function () {
21741 if (!grid.options.enablePagination) {
21745 grid.options.paginationCurrentPage = Math.max(grid.options.paginationCurrentPage - 1, 1);
21750 * @methodOf ui.grid.pagination.api:PublicAPI
21751 * @description Moves to the requested page
21752 * @param {int} page The number of the page that should be displayed
21754 seek: function (page) {
21755 if (!grid.options.enablePagination) {
21758 if (!angular.isNumber(page) || page < 1) {
21759 throw 'Invalid page number: ' + page;
21762 grid.options.paginationCurrentPage = Math.min(page, publicApi.methods.pagination.getTotalPages());
21768 grid.api.registerEventsFromObject(publicApi.events);
21769 grid.api.registerMethodsFromObject(publicApi.methods);
21771 var processPagination = function( renderableRows ){
21772 if (grid.options.useExternalPagination || !grid.options.enablePagination) {
21773 return renderableRows;
21775 //client side pagination
21776 var pageSize = parseInt(grid.options.paginationPageSize, 10);
21777 var currentPage = parseInt(grid.options.paginationCurrentPage, 10);
21779 var visibleRows = renderableRows.filter(function (row) { return row.visible; });
21780 grid.options.totalItems = visibleRows.length;
21782 var firstRow = (currentPage - 1) * pageSize;
21783 if (firstRow > visibleRows.length) {
21784 currentPage = grid.options.paginationCurrentPage = 1;
21785 firstRow = (currentPage - 1) * pageSize;
21787 return visibleRows.slice(firstRow, firstRow + pageSize);
21790 grid.registerRowsProcessor(processPagination, 900 );
21793 defaultGridOptions: function (gridOptions) {
21796 * @name ui.grid.pagination.api:GridOptions
21798 * @description GridOptions for the pagination feature, these are available to be
21799 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21804 * @name enablePagination
21805 * @propertyOf ui.grid.pagination.api:GridOptions
21806 * @description Enables pagination. Defaults to true.
21808 gridOptions.enablePagination = gridOptions.enablePagination !== false;
21811 * @name enablePaginationControls
21812 * @propertyOf ui.grid.pagination.api:GridOptions
21813 * @description Enables the paginator at the bottom of the grid. Turn this off if you want to implement your
21814 * own controls outside the grid.
21816 gridOptions.enablePaginationControls = gridOptions.enablePaginationControls !== false;
21819 * @name useExternalPagination
21820 * @propertyOf ui.grid.pagination.api:GridOptions
21821 * @description Disables client side pagination. When true, handle the paginationChanged event and set data
21822 * and totalItems. Defaults to `false`
21824 gridOptions.useExternalPagination = gridOptions.useExternalPagination === true;
21828 * @propertyOf ui.grid.pagination.api:GridOptions
21829 * @description Total number of items, set automatically when using client side pagination, but needs set by user
21830 * for server side pagination
21832 if (gridUtil.isNullOrUndefined(gridOptions.totalItems)) {
21833 gridOptions.totalItems = 0;
21837 * @name paginationPageSizes
21838 * @propertyOf ui.grid.pagination.api:GridOptions
21839 * @description Array of page sizes, defaults to `[250, 500, 1000]`
21841 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSizes)) {
21842 gridOptions.paginationPageSizes = [250, 500, 1000];
21846 * @name paginationPageSize
21847 * @propertyOf ui.grid.pagination.api:GridOptions
21848 * @description Page size, defaults to the first item in paginationPageSizes, or 0 if paginationPageSizes is empty
21850 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSize)) {
21851 if (gridOptions.paginationPageSizes.length > 0) {
21852 gridOptions.paginationPageSize = gridOptions.paginationPageSizes[0];
21854 gridOptions.paginationPageSize = 0;
21859 * @name paginationCurrentPage
21860 * @propertyOf ui.grid.pagination.api:GridOptions
21861 * @description Current page number, defaults to 1
21863 if (gridUtil.isNullOrUndefined(gridOptions.paginationCurrentPage)) {
21864 gridOptions.paginationCurrentPage = 1;
21869 * @name paginationTemplate
21870 * @propertyOf ui.grid.pagination.api:GridOptions
21871 * @description A custom template for the pager, defaults to `ui-grid/pagination`
21873 if (gridUtil.isNullOrUndefined(gridOptions.paginationTemplate)) {
21874 gridOptions.paginationTemplate = 'ui-grid/pagination';
21879 * @methodOf ui.grid.pagination.service:uiGridPaginationService
21880 * @name uiGridPaginationService
21881 * @description Raises paginationChanged and calls refresh for client side pagination
21882 * @param {Grid} grid the grid for which the pagination changed
21883 * @param {int} currentPage requested page number
21884 * @param {int} pageSize requested page size
21886 onPaginationChanged: function (grid, currentPage, pageSize) {
21887 grid.api.pagination.raise.paginationChanged(currentPage, pageSize);
21888 if (!grid.options.useExternalPagination) {
21889 grid.queueGridRefresh(); //client side pagination
21899 * @name ui.grid.pagination.directive:uiGridPagination
21903 * @description Adds pagination features to grid
21905 <example module="app">
21906 <file name="app.js">
21907 var app = angular.module('app', ['ui.grid', 'ui.grid.pagination']);
21909 app.controller('MainCtrl', ['$scope', function ($scope) {
21911 { name: 'Alex', car: 'Toyota' },
21912 { name: 'Sam', car: 'Lexus' },
21913 { name: 'Joe', car: 'Dodge' },
21914 { name: 'Bob', car: 'Buick' },
21915 { name: 'Cindy', car: 'Ford' },
21916 { name: 'Brian', car: 'Audi' },
21917 { name: 'Malcom', car: 'Mercedes Benz' },
21918 { name: 'Dave', car: 'Ford' },
21919 { name: 'Stacey', car: 'Audi' },
21920 { name: 'Amy', car: 'Acura' },
21921 { name: 'Scott', car: 'Toyota' },
21922 { name: 'Ryan', car: 'BMW' },
21925 $scope.gridOptions = {
21927 paginationPageSizes: [5, 10, 25],
21928 paginationPageSize: 5,
21936 <file name="index.html">
21937 <div ng-controller="MainCtrl">
21938 <div ui-grid="gridOptions" ui-grid-pagination></div>
21943 module.directive('uiGridPagination', ['gridUtil', 'uiGridPaginationService',
21944 function (gridUtil, uiGridPaginationService) {
21950 pre: function ($scope, $elm, $attr, uiGridCtrl) {
21951 uiGridPaginationService.initializeGrid(uiGridCtrl.grid);
21953 gridUtil.getTemplate(uiGridCtrl.grid.options.paginationTemplate)
21954 .then(function (contents) {
21955 var template = angular.element(contents);
21956 $elm.append(template);
21957 uiGridCtrl.innerCompile(template);
21967 * @name ui.grid.pagination.directive:uiGridPager
21970 * @description Panel for handling pagination
21972 module.directive('uiGridPager', ['uiGridPaginationService', 'uiGridConstants', 'gridUtil', 'i18nService',
21973 function (uiGridPaginationService, uiGridConstants, gridUtil, i18nService) {
21977 require: '^uiGrid',
21978 link: function ($scope, $elm, $attr, uiGridCtrl) {
21979 var defaultFocusElementSelector = '.ui-grid-pager-control-input';
21980 $scope.aria = i18nService.getSafeText('pagination.aria'); //Returns an object with all of the aria labels
21982 $scope.paginationApi = uiGridCtrl.grid.api.pagination;
21983 $scope.sizesLabel = i18nService.getSafeText('pagination.sizes');
21984 $scope.totalItemsLabel = i18nService.getSafeText('pagination.totalItems');
21985 $scope.paginationOf = i18nService.getSafeText('pagination.of');
21986 $scope.paginationThrough = i18nService.getSafeText('pagination.through');
21988 var options = uiGridCtrl.grid.options;
21990 uiGridCtrl.grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
21991 adjustment.height = adjustment.height - gridUtil.elementHeight($elm, "padding");
21995 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback(function (grid) {
21996 if (!grid.options.useExternalPagination) {
21997 grid.options.totalItems = grid.rows.length;
21999 }, [uiGridConstants.dataChange.ROW]);
22001 $scope.$on('$destroy', dataChangeDereg);
22003 var setShowing = function () {
22004 $scope.showingLow = ((options.paginationCurrentPage - 1) * options.paginationPageSize) + 1;
22005 $scope.showingHigh = Math.min(options.paginationCurrentPage * options.paginationPageSize, options.totalItems);
22008 var deregT = $scope.$watch('grid.options.totalItems + grid.options.paginationPageSize', setShowing);
22010 var deregP = $scope.$watch('grid.options.paginationCurrentPage + grid.options.paginationPageSize', function (newValues, oldValues) {
22011 if (newValues === oldValues || oldValues === undefined) {
22015 if (!angular.isNumber(options.paginationCurrentPage) || options.paginationCurrentPage < 1) {
22016 options.paginationCurrentPage = 1;
22020 if (options.totalItems > 0 && options.paginationCurrentPage > $scope.paginationApi.getTotalPages()) {
22021 options.paginationCurrentPage = $scope.paginationApi.getTotalPages();
22026 uiGridPaginationService.onPaginationChanged($scope.grid, options.paginationCurrentPage, options.paginationPageSize);
22030 $scope.$on('$destroy', function() {
22035 $scope.cantPageForward = function () {
22036 if (options.totalItems > 0) {
22037 return options.paginationCurrentPage >= $scope.paginationApi.getTotalPages();
22039 return options.data.length < 1;
22043 $scope.cantPageToLast = function () {
22044 if (options.totalItems > 0) {
22045 return $scope.cantPageForward();
22051 $scope.cantPageBackward = function () {
22052 return options.paginationCurrentPage <= 1;
22055 var focusToInputIf = function(condition){
22057 gridUtil.focus.bySelector($elm, defaultFocusElementSelector);
22061 //Takes care of setting focus to the middle element when focus is lost
22062 $scope.pageFirstPageClick = function () {
22063 $scope.paginationApi.seek(1);
22064 focusToInputIf($scope.cantPageBackward());
22067 $scope.pagePreviousPageClick = function () {
22068 $scope.paginationApi.previousPage();
22069 focusToInputIf($scope.cantPageBackward());
22072 $scope.pageNextPageClick = function () {
22073 $scope.paginationApi.nextPage();
22074 focusToInputIf($scope.cantPageForward());
22077 $scope.pageLastPageClick = function () {
22078 $scope.paginationApi.seek($scope.paginationApi.getTotalPages());
22079 focusToInputIf($scope.cantPageToLast());
22093 * @name ui.grid.pinning
22096 * # ui.grid.pinning
22098 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
22100 * This module provides column pinning to the end user via menu options in the column header
22102 * <div doc-module-components="ui.grid.pinning"></div>
22105 var module = angular.module('ui.grid.pinning', ['ui.grid']);
22107 module.constant('uiGridPinningConstants', {
22115 module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', 'uiGridPinningConstants', function (gridUtil, GridRenderContainer, i18nService, uiGridPinningConstants) {
22118 initializeGrid: function (grid) {
22119 service.defaultGridOptions(grid.options);
22121 // Register a column builder to add new menu items for pinning left and right
22122 grid.registerColumnBuilder(service.pinningColumnBuilder);
22126 * @name ui.grid.pinning.api:PublicApi
22128 * @description Public Api for pinning feature
22136 * @eventOf ui.grid.pinning.api:PublicApi
22137 * @description raised when column pin state has changed
22139 * gridApi.pinning.on.columnPinned(scope, function(colDef){})
22141 * @param {object} colDef the column that was changed
22142 * @param {string} container the render container the column is in ('left', 'right', '')
22144 columnPinned: function(colDef, container) {
22153 * @methodOf ui.grid.pinning.api:PublicApi
22154 * @description pin column left, right, or none
22156 * gridApi.pinning.pinColumn(col, uiGridPinningConstants.container.LEFT)
22158 * @param {gridColumn} col the column being pinned
22159 * @param {string} container one of the recognised types
22160 * from uiGridPinningConstants
22162 pinColumn: function(col, container) {
22163 service.pinColumn(grid, col, container);
22169 grid.api.registerEventsFromObject(publicApi.events);
22170 grid.api.registerMethodsFromObject(publicApi.methods);
22173 defaultGridOptions: function (gridOptions) {
22174 //default option to true unless it was explicitly set to false
22177 * @name ui.grid.pinning.api:GridOptions
22179 * @description GridOptions for pinning feature, these are available to be
22180 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
22185 * @name enablePinning
22186 * @propertyOf ui.grid.pinning.api:GridOptions
22187 * @description Enable pinning for the entire grid.
22188 * <br/>Defaults to true
22190 gridOptions.enablePinning = gridOptions.enablePinning !== false;
22194 pinningColumnBuilder: function (colDef, col, gridOptions) {
22195 //default to true unless gridOptions or colDef is explicitly false
22199 * @name ui.grid.pinning.api:ColumnDef
22201 * @description ColumnDef for pinning feature, these are available to be
22202 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
22207 * @name enablePinning
22208 * @propertyOf ui.grid.pinning.api:ColumnDef
22209 * @description Enable pinning for the individual column.
22210 * <br/>Defaults to true
22212 colDef.enablePinning = colDef.enablePinning === undefined ? gridOptions.enablePinning : colDef.enablePinning;
22218 * @propertyOf ui.grid.pinning.api:ColumnDef
22219 * @description Column is pinned left when grid is rendered
22220 * <br/>Defaults to false
22225 * @name pinnedRight
22226 * @propertyOf ui.grid.pinning.api:ColumnDef
22227 * @description Column is pinned right when grid is rendered
22228 * <br/>Defaults to false
22230 if (colDef.pinnedLeft) {
22231 col.renderContainer = 'left';
22232 col.grid.createLeftContainer();
22234 else if (colDef.pinnedRight) {
22235 col.renderContainer = 'right';
22236 col.grid.createRightContainer();
22239 if (!colDef.enablePinning) {
22243 var pinColumnLeftAction = {
22244 name: 'ui.grid.pinning.pinLeft',
22245 title: i18nService.get().pinning.pinLeft,
22246 icon: 'ui-grid-icon-left-open',
22247 shown: function () {
22248 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'left';
22250 action: function () {
22251 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.LEFT);
22255 var pinColumnRightAction = {
22256 name: 'ui.grid.pinning.pinRight',
22257 title: i18nService.get().pinning.pinRight,
22258 icon: 'ui-grid-icon-right-open',
22259 shown: function () {
22260 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'right';
22262 action: function () {
22263 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.RIGHT);
22267 var removePinAction = {
22268 name: 'ui.grid.pinning.unpin',
22269 title: i18nService.get().pinning.unpin,
22270 icon: 'ui-grid-icon-cancel',
22271 shown: function () {
22272 return typeof(this.context.col.renderContainer) !== 'undefined' && this.context.col.renderContainer !== null && this.context.col.renderContainer !== 'body';
22274 action: function () {
22275 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.NONE);
22279 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinLeft')) {
22280 col.menuItems.push(pinColumnLeftAction);
22282 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinRight')) {
22283 col.menuItems.push(pinColumnRightAction);
22285 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.unpin')) {
22286 col.menuItems.push(removePinAction);
22290 pinColumn: function(grid, col, container) {
22291 if (container === uiGridPinningConstants.container.NONE) {
22292 col.renderContainer = null;
22293 col.colDef.pinnedLeft = col.colDef.pinnedRight = false;
22296 col.renderContainer = container;
22297 if (container === uiGridPinningConstants.container.LEFT) {
22298 grid.createLeftContainer();
22300 else if (container === uiGridPinningConstants.container.RIGHT) {
22301 grid.createRightContainer();
22307 grid.api.pinning.raise.columnPinned( col.colDef, container );
22315 module.directive('uiGridPinning', ['gridUtil', 'uiGridPinningService',
22316 function (gridUtil, uiGridPinningService) {
22320 compile: function () {
22322 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
22323 uiGridPinningService.initializeGrid(uiGridCtrl.grid);
22325 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22340 * @name ui.grid.resizeColumns
22343 * # ui.grid.resizeColumns
22345 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
22347 * This module allows columns to be resized.
22349 var module = angular.module('ui.grid.resizeColumns', ['ui.grid']);
22351 module.service('uiGridResizeColumnsService', ['gridUtil', '$q', '$timeout',
22352 function (gridUtil, $q, $timeout) {
22355 defaultGridOptions: function(gridOptions){
22356 //default option to true unless it was explicitly set to false
22359 * @name ui.grid.resizeColumns.api:GridOptions
22361 * @description GridOptions for resizeColumns feature, these are available to be
22362 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
22367 * @name enableColumnResizing
22368 * @propertyOf ui.grid.resizeColumns.api:GridOptions
22369 * @description Enable column resizing on the entire grid
22370 * <br/>Defaults to true
22372 gridOptions.enableColumnResizing = gridOptions.enableColumnResizing !== false;
22375 //use old name if it is explicitly false
22376 if (gridOptions.enableColumnResize === false){
22377 gridOptions.enableColumnResizing = false;
22381 colResizerColumnBuilder: function (colDef, col, gridOptions) {
22386 * @name ui.grid.resizeColumns.api:ColumnDef
22388 * @description ColumnDef for resizeColumns feature, these are available to be
22389 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
22394 * @name enableColumnResizing
22395 * @propertyOf ui.grid.resizeColumns.api:ColumnDef
22396 * @description Enable column resizing on an individual column
22397 * <br/>Defaults to GridOptions.enableColumnResizing
22399 //default to true unless gridOptions or colDef is explicitly false
22400 colDef.enableColumnResizing = colDef.enableColumnResizing === undefined ? gridOptions.enableColumnResizing : colDef.enableColumnResizing;
22403 //legacy support of old option name
22404 if (colDef.enableColumnResize === false){
22405 colDef.enableColumnResizing = false;
22408 return $q.all(promises);
22411 registerPublicApi: function (grid) {
22414 * @name ui.grid.resizeColumns.api:PublicApi
22415 * @description Public Api for column resize feature.
22421 * @name columnSizeChanged
22422 * @eventOf ui.grid.resizeColumns.api:PublicApi
22423 * @description raised when column is resized
22425 * gridApi.colResizable.on.columnSizeChanged(scope,function(colDef, deltaChange){})
22427 * @param {object} colDef the column that was resized
22428 * @param {integer} delta of the column size change
22431 columnSizeChanged: function (colDef, deltaChange) {
22436 grid.api.registerEventsFromObject(publicApi.events);
22439 fireColumnSizeChanged: function (grid, colDef, deltaChange) {
22440 $timeout(function () {
22441 if ( grid.api.colResizable ){
22442 grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange);
22444 gridUtil.logError("The resizeable api is not registered, this may indicate that you've included the module but not added the 'ui-grid-resize-columns' directive to your grid definition. Cannot raise any events.");
22449 // get either this column, or the column next to this column, to resize,
22450 // returns the column we're going to resize
22451 findTargetCol: function(col, position, rtlMultiplier){
22452 var renderContainer = col.getRenderContainer();
22454 if (position === 'left') {
22455 // Get the column to the left of this one
22456 var colIndex = renderContainer.visibleColumnCache.indexOf(col);
22457 return renderContainer.visibleColumnCache[colIndex - 1 * rtlMultiplier];
22472 * @name ui.grid.resizeColumns.directive:uiGridResizeColumns
22476 * Enables resizing for all columns on the grid. If, for some reason, you want to use the ui-grid-resize-columns directive, but not allow column resizing, you can explicitly set the
22477 * option to false. This prevents resizing for the entire grid, regardless of individual columnDef options.
22480 <doc:example module="app">
22483 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
22485 app.controller('MainCtrl', ['$scope', function ($scope) {
22486 $scope.gridOpts = {
22488 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
22489 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
22490 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
22491 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
22497 <div ng-controller="MainCtrl">
22498 <div class="testGrid" ui-grid="gridOpts" ui-grid-resize-columns ></div>
22506 module.directive('uiGridResizeColumns', ['gridUtil', 'uiGridResizeColumnsService', function (gridUtil, uiGridResizeColumnsService) {
22510 require: '^uiGrid',
22512 compile: function () {
22514 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
22515 uiGridResizeColumnsService.defaultGridOptions(uiGridCtrl.grid.options);
22516 uiGridCtrl.grid.registerColumnBuilder( uiGridResizeColumnsService.colResizerColumnBuilder);
22517 uiGridResizeColumnsService.registerPublicApi(uiGridCtrl.grid);
22519 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22526 // Extend the uiGridHeaderCell directive
22527 module.directive('uiGridHeaderCell', ['gridUtil', '$templateCache', '$compile', '$q', 'uiGridResizeColumnsService', 'uiGridConstants', '$timeout', function (gridUtil, $templateCache, $compile, $q, uiGridResizeColumnsService, uiGridConstants, $timeout) {
22529 // Run after the original uiGridHeaderCell
22531 require: '^uiGrid',
22533 compile: function() {
22535 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22536 var grid = uiGridCtrl.grid;
22538 if (grid.options.enableColumnResizing) {
22539 var columnResizerElm = $templateCache.get('ui-grid/columnResizer');
22541 var rtlMultiplier = 1;
22542 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
22543 if (grid.isRTL()) {
22544 $scope.position = 'left';
22545 rtlMultiplier = -1;
22548 var displayResizers = function(){
22550 // remove any existing resizers.
22551 var resizers = $elm[0].getElementsByClassName('ui-grid-column-resizer');
22552 for ( var i = 0; i < resizers.length; i++ ){
22553 angular.element(resizers[i]).remove();
22556 // get the target column for the left resizer
22557 var otherCol = uiGridResizeColumnsService.findTargetCol($scope.col, 'left', rtlMultiplier);
22558 var renderContainer = $scope.col.getRenderContainer();
22560 // Don't append the left resizer if this is the first column or the column to the left of this one has resizing disabled
22561 if (otherCol && renderContainer.visibleColumnCache.indexOf($scope.col) !== 0 && otherCol.colDef.enableColumnResizing !== false) {
22562 var resizerLeft = angular.element(columnResizerElm).clone();
22563 resizerLeft.attr('position', 'left');
22565 $elm.prepend(resizerLeft);
22566 $compile(resizerLeft)($scope);
22569 // Don't append the right resizer if this column has resizing disabled
22570 if ($scope.col.colDef.enableColumnResizing !== false) {
22571 var resizerRight = angular.element(columnResizerElm).clone();
22572 resizerRight.attr('position', 'right');
22574 $elm.append(resizerRight);
22575 $compile(resizerRight)($scope);
22581 var waitDisplay = function(){
22582 $timeout(displayResizers);
22585 var dataChangeDereg = grid.registerDataChangeCallback( waitDisplay, [uiGridConstants.dataChange.COLUMN] );
22587 $scope.$on( '$destroy', dataChangeDereg );
22599 * @name ui.grid.resizeColumns.directive:uiGridColumnResizer
22604 * Draggable handle that controls column resizing.
22607 <doc:example module="app">
22610 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
22612 app.controller('MainCtrl', ['$scope', function ($scope) {
22613 $scope.gridOpts = {
22614 enableColumnResizing: true,
22616 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
22617 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
22618 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
22619 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
22625 <div ng-controller="MainCtrl">
22626 <div class="testGrid" ui-grid="gridOpts"></div>
22630 // TODO: e2e specs?
22632 // TODO: post-resize a horizontal scroll event should be fired
22636 module.directive('uiGridColumnResizer', ['$document', 'gridUtil', 'uiGridConstants', 'uiGridResizeColumnsService', function ($document, gridUtil, uiGridConstants, uiGridResizeColumnsService) {
22637 var resizeOverlay = angular.element('<div class="ui-grid-resize-overlay"></div>');
22646 require: '?^uiGrid',
22647 link: function ($scope, $elm, $attrs, uiGridCtrl) {
22653 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
22654 if (uiGridCtrl.grid.isRTL()) {
22655 $scope.position = 'left';
22656 rtlMultiplier = -1;
22659 if ($scope.position === 'left') {
22660 $elm.addClass('left');
22662 else if ($scope.position === 'right') {
22663 $elm.addClass('right');
22666 // Refresh the grid canvas
22667 // takes an argument representing the diff along the X-axis that the resize had
22668 function refreshCanvas(xDiff) {
22669 // Then refresh the grid canvas, rebuilding the styles so that the scrollbar updates its size
22670 uiGridCtrl.grid.refreshCanvas(true).then( function() {
22671 uiGridCtrl.grid.queueGridRefresh();
22675 // Check that the requested width isn't wider than the maxWidth, or narrower than the minWidth
22676 // Returns the new recommended with, after constraints applied
22677 function constrainWidth(col, width){
22678 var newWidth = width;
22680 // If the new width would be less than the column's allowably minimum width, don't allow it
22681 if (col.minWidth && newWidth < col.minWidth) {
22682 newWidth = col.minWidth;
22684 else if (col.maxWidth && newWidth > col.maxWidth) {
22685 newWidth = col.maxWidth;
22693 * Our approach to event handling aims to deal with both touch devices and mouse devices
22694 * We register down handlers on both touch and mouse. When a touchstart or mousedown event
22695 * occurs, we register the corresponding touchmove/touchend, or mousemove/mouseend events.
22697 * This way we can listen for both without worrying about the fact many touch devices also emulate
22698 * mouse events - basically whichever one we hear first is what we'll go with.
22700 function moveFunction(event, args) {
22701 if (event.originalEvent) { event = event.originalEvent; }
22702 event.preventDefault();
22704 x = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22706 if (x < 0) { x = 0; }
22707 else if (x > uiGridCtrl.grid.gridWidth) { x = uiGridCtrl.grid.gridWidth; }
22709 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22711 // Don't resize if it's disabled on this column
22712 if (col.colDef.enableColumnResizing === false) {
22716 if (!uiGridCtrl.grid.element.hasClass('column-resizing')) {
22717 uiGridCtrl.grid.element.addClass('column-resizing');
22720 // Get the diff along the X axis
22721 var xDiff = x - startX;
22723 // Get the width that this mouse would give the column
22724 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22726 // check we're not outside the allowable bounds for this column
22727 x = x + ( constrainWidth(col, newWidth) - newWidth ) * rtlMultiplier;
22729 resizeOverlay.css({ left: x + 'px' });
22731 uiGridCtrl.fireEvent(uiGridConstants.events.ITEM_DRAGGING);
22735 function upFunction(event, args) {
22736 if (event.originalEvent) { event = event.originalEvent; }
22737 event.preventDefault();
22739 uiGridCtrl.grid.element.removeClass('column-resizing');
22741 resizeOverlay.remove();
22743 // Resize the column
22744 x = (event.changedTouches ? event.changedTouches[0] : event).clientX - gridLeft;
22745 var xDiff = x - startX;
22748 // no movement, so just reset event handlers, including turning back on both
22749 // down events - we turned one off when this event started
22755 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22757 // Don't resize if it's disabled on this column
22758 if (col.colDef.enableColumnResizing === false) {
22762 // Get the new width
22763 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22765 // check we're not outside the allowable bounds for this column
22766 col.width = constrainWidth(col, newWidth);
22767 col.hasCustomWidth = true;
22769 refreshCanvas(xDiff);
22771 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff);
22773 // stop listening of up and move events - wait for next down
22774 // reset the down events - we will have turned one off when this event started
22780 var downFunction = function(event, args) {
22781 if (event.originalEvent) { event = event.originalEvent; }
22782 event.stopPropagation();
22784 // Get the left offset of the grid
22785 // gridLeft = uiGridCtrl.grid.element[0].offsetLeft;
22786 gridLeft = uiGridCtrl.grid.element[0].getBoundingClientRect().left;
22788 // Get the starting X position, which is the X coordinate of the click minus the grid's offset
22789 startX = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22791 // Append the resizer overlay
22792 uiGridCtrl.grid.element.append(resizeOverlay);
22794 // Place the resizer overlay at the start position
22795 resizeOverlay.css({ left: startX });
22797 // Add handlers for move and up events - if we were mousedown then we listen for mousemove and mouseup, if
22798 // we were touchdown then we listen for touchmove and touchup. Also remove the handler for the equivalent
22799 // down event - so if we're touchdown, then remove the mousedown handler until this event is over, if we're
22800 // mousedown then remove the touchdown handler until this event is over, this avoids processing duplicate events
22801 if ( event.type === 'touchstart' ){
22802 $document.on('touchend', upFunction);
22803 $document.on('touchmove', moveFunction);
22804 $elm.off('mousedown', downFunction);
22806 $document.on('mouseup', upFunction);
22807 $document.on('mousemove', moveFunction);
22808 $elm.off('touchstart', downFunction);
22812 var onDownEvents = function() {
22813 $elm.on('mousedown', downFunction);
22814 $elm.on('touchstart', downFunction);
22817 var offAllEvents = function() {
22818 $document.off('mouseup', upFunction);
22819 $document.off('touchend', upFunction);
22820 $document.off('mousemove', moveFunction);
22821 $document.off('touchmove', moveFunction);
22822 $elm.off('mousedown', downFunction);
22823 $elm.off('touchstart', downFunction);
22829 // On doubleclick, resize to fit all rendered cells
22830 var dblClickFn = function(event, args){
22831 event.stopPropagation();
22833 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22835 // Don't resize if it's disabled on this column
22836 if (col.colDef.enableColumnResizing === false) {
22840 // Go through the rendered rows and find out the max size for the data in this column
22844 // Get the parent render container element
22845 var renderContainerElm = gridUtil.closestElm($elm, '.ui-grid-render-container');
22847 // Get the cell contents so we measure correctly. For the header cell we have to account for the sort icon and the menu buttons, if present
22848 var cells = renderContainerElm.querySelectorAll('.' + uiGridConstants.COL_CLASS_PREFIX + col.uid + ' .ui-grid-cell-contents');
22849 Array.prototype.forEach.call(cells, function (cell) {
22850 // Get the cell width
22851 // gridUtil.logDebug('width', gridUtil.elementWidth(cell));
22853 // Account for the menu button if it exists
22855 if (angular.element(cell).parent().hasClass('ui-grid-header-cell')) {
22856 menuButton = angular.element(cell).parent()[0].querySelectorAll('.ui-grid-column-menu-button');
22859 gridUtil.fakeElement(cell, {}, function(newElm) {
22860 // Make the element float since it's a div and can expand to fill its container
22861 var e = angular.element(newElm);
22862 e.attr('style', 'float: left');
22864 var width = gridUtil.elementWidth(e);
22867 var menuButtonWidth = gridUtil.elementWidth(menuButton);
22868 width = width + menuButtonWidth;
22871 if (width > maxWidth) {
22873 xDiff = maxWidth - width;
22878 // check we're not outside the allowable bounds for this column
22879 col.width = constrainWidth(col, maxWidth);
22880 col.hasCustomWidth = true;
22882 refreshCanvas(xDiff);
22884 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); };
22885 $elm.on('dblclick', dblClickFn);
22887 $elm.on('$destroy', function() {
22888 $elm.off('dblclick', dblClickFn);
22904 * @name ui.grid.rowEdit
22907 * # ui.grid.rowEdit
22909 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
22911 * This module extends the edit feature to provide tracking and saving of rows
22912 * of data. The tutorial provides more information on how this feature is best
22913 * used {@link tutorial/205_row_editable here}.
22915 * This feature depends on usage of the ui-grid-edit feature, and also benefits
22916 * from use of ui-grid-cellNav to provide the full spreadsheet-like editing
22921 var module = angular.module('ui.grid.rowEdit', ['ui.grid', 'ui.grid.edit', 'ui.grid.cellNav']);
22925 * @name ui.grid.rowEdit.constant:uiGridRowEditConstants
22927 * @description constants available in row edit module
22929 module.constant('uiGridRowEditConstants', {
22934 * @name ui.grid.rowEdit.service:uiGridRowEditService
22936 * @description Services for row editing features
22938 module.service('uiGridRowEditService', ['$interval', '$q', 'uiGridConstants', 'uiGridRowEditConstants', 'gridUtil',
22939 function ($interval, $q, uiGridConstants, uiGridRowEditConstants, gridUtil) {
22943 initializeGrid: function (scope, grid) {
22946 * @name ui.grid.rowEdit.api:PublicApi
22948 * @description Public Api for rowEdit feature
22958 * @eventOf ui.grid.rowEdit.api:PublicApi
22960 * @description raised when a row is ready for saving. Once your
22961 * row has saved you may need to use angular.extend to update the
22962 * data entity with any changed data from your save (for example,
22963 * lock version information if you're using optimistic locking,
22964 * or last update time/user information).
22966 * Your method should call setSavePromise somewhere in the body before
22967 * returning control. The feature will then wait, with the gridRow greyed out
22968 * whilst this promise is being resolved.
22971 * gridApi.rowEdit.on.saveRow(scope,function(rowEntity){})
22973 * and somewhere within the event handler:
22975 * gridApi.rowEdit.setSavePromise( rowEntity, savePromise)
22977 * @param {object} rowEntity the options.data element that was edited
22978 * @returns {promise} Your saveRow method should return a promise, the
22979 * promise should either be resolved (implying successful save), or
22980 * rejected (implying an error).
22982 saveRow: function (rowEntity) {
22990 * @methodOf ui.grid.rowEdit.api:PublicApi
22991 * @name setSavePromise
22992 * @description Sets the promise associated with the row save, mandatory that
22993 * the saveRow event handler calls this method somewhere before returning.
22995 * gridApi.rowEdit.setSavePromise(rowEntity, savePromise)
22997 * @param {object} rowEntity a data row from the grid for which a save has
22999 * @param {promise} savePromise the promise that will be resolved when the
23000 * save is successful, or rejected if the save fails
23003 setSavePromise: function ( rowEntity, savePromise) {
23004 service.setSavePromise(grid, rowEntity, savePromise);
23008 * @methodOf ui.grid.rowEdit.api:PublicApi
23009 * @name getDirtyRows
23010 * @description Returns all currently dirty rows
23012 * gridApi.rowEdit.getDirtyRows(grid)
23014 * @returns {array} An array of gridRows that are currently dirty
23017 getDirtyRows: function () {
23018 return grid.rowEdit.dirtyRows ? grid.rowEdit.dirtyRows : [];
23022 * @methodOf ui.grid.rowEdit.api:PublicApi
23023 * @name getErrorRows
23024 * @description Returns all currently errored rows
23026 * gridApi.rowEdit.getErrorRows(grid)
23028 * @returns {array} An array of gridRows that are currently in error
23031 getErrorRows: function () {
23032 return grid.rowEdit.errorRows ? grid.rowEdit.errorRows : [];
23036 * @methodOf ui.grid.rowEdit.api:PublicApi
23037 * @name flushDirtyRows
23038 * @description Triggers a save event for all currently dirty rows, could
23039 * be used where user presses a save button or navigates away from the page
23041 * gridApi.rowEdit.flushDirtyRows(grid)
23043 * @returns {promise} a promise that represents the aggregate of all
23044 * of the individual save promises - i.e. it will be resolved when all
23045 * the individual save promises have been resolved.
23048 flushDirtyRows: function () {
23049 return service.flushDirtyRows(grid);
23054 * @methodOf ui.grid.rowEdit.api:PublicApi
23055 * @name setRowsDirty
23056 * @description Sets each of the rows passed in dataRows
23057 * to be dirty. note that if you have only just inserted the
23058 * rows into your data you will need to wait for a $digest cycle
23059 * before the gridRows are present - so often you would wrap this
23060 * call in a $interval or $timeout
23062 * $interval( function() {
23063 * gridApi.rowEdit.setRowsDirty(myDataRows);
23066 * @param {array} dataRows the data entities for which the gridRows
23067 * should be set dirty.
23070 setRowsDirty: function ( dataRows) {
23071 service.setRowsDirty(grid, dataRows);
23076 * @methodOf ui.grid.rowEdit.api:PublicApi
23077 * @name setRowsClean
23078 * @description Sets each of the rows passed in dataRows
23079 * to be clean, removing them from the dirty cache and the error cache,
23080 * and clearing the error flag and the dirty flag
23082 * var gridRows = $scope.gridApi.rowEdit.getDirtyRows();
23083 * var dataRows = gridRows.map( function( gridRow ) { return gridRow.entity; });
23084 * $scope.gridApi.rowEdit.setRowsClean( dataRows );
23086 * @param {array} dataRows the data entities for which the gridRows
23087 * should be set clean.
23090 setRowsClean: function ( dataRows) {
23091 service.setRowsClean(grid, dataRows);
23097 grid.api.registerEventsFromObject(publicApi.events);
23098 grid.api.registerMethodsFromObject(publicApi.methods);
23100 grid.api.core.on.renderingComplete( scope, function ( gridApi ) {
23101 grid.api.edit.on.afterCellEdit( scope, service.endEditCell );
23102 grid.api.edit.on.beginCellEdit( scope, service.beginEditCell );
23103 grid.api.edit.on.cancelCellEdit( scope, service.cancelEditCell );
23105 if ( grid.api.cellNav ) {
23106 grid.api.cellNav.on.navigate( scope, service.navigate );
23112 defaultGridOptions: function (gridOptions) {
23116 * @name ui.grid.rowEdit.api:GridOptions
23118 * @description Options for configuring the rowEdit feature, these are available to be
23119 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
23127 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23129 * @description Returns a function that saves the specified row from the grid,
23130 * and returns a promise
23131 * @param {object} grid the grid for which dirty rows should be flushed
23132 * @param {GridRow} gridRow the row that should be saved
23133 * @returns {function} the saveRow function returns a function. That function
23134 * in turn, when called, returns a promise relating to the save callback
23136 saveRow: function ( grid, gridRow ) {
23139 return function() {
23140 gridRow.isSaving = true;
23142 if ( gridRow.rowEditSavePromise ){
23143 // don't save the row again if it's already saving - that causes stale object exceptions
23144 return gridRow.rowEditSavePromise;
23147 var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity );
23149 if ( gridRow.rowEditSavePromise ){
23150 gridRow.rowEditSavePromise.then( self.processSuccessPromise( grid, gridRow ), self.processErrorPromise( grid, gridRow ));
23152 gridUtil.logError( 'A promise was not returned when saveRow event was raised, either nobody is listening to event, or event handler did not return a promise' );
23161 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23162 * @name setSavePromise
23163 * @description Sets the promise associated with the row save, mandatory that
23164 * the saveRow event handler calls this method somewhere before returning.
23166 * gridApi.rowEdit.setSavePromise(grid, rowEntity)
23168 * @param {object} grid the grid for which dirty rows should be returned
23169 * @param {object} rowEntity a data row from the grid for which a save has
23171 * @param {promise} savePromise the promise that will be resolved when the
23172 * save is successful, or rejected if the save fails
23175 setSavePromise: function (grid, rowEntity, savePromise) {
23176 var gridRow = grid.getRow( rowEntity );
23177 gridRow.rowEditSavePromise = savePromise;
23183 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23184 * @name processSuccessPromise
23185 * @description Returns a function that processes the successful
23186 * resolution of a save promise
23187 * @param {object} grid the grid for which the promise should be processed
23188 * @param {GridRow} gridRow the row that has been saved
23189 * @returns {function} the success handling function
23191 processSuccessPromise: function ( grid, gridRow ) {
23194 return function() {
23195 delete gridRow.isSaving;
23196 delete gridRow.isDirty;
23197 delete gridRow.isError;
23198 delete gridRow.rowEditSaveTimer;
23199 delete gridRow.rowEditSavePromise;
23200 self.removeRow( grid.rowEdit.errorRows, gridRow );
23201 self.removeRow( grid.rowEdit.dirtyRows, gridRow );
23208 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23209 * @name processErrorPromise
23210 * @description Returns a function that processes the failed
23211 * resolution of a save promise
23212 * @param {object} grid the grid for which the promise should be processed
23213 * @param {GridRow} gridRow the row that is now in error
23214 * @returns {function} the error handling function
23216 processErrorPromise: function ( grid, gridRow ) {
23217 return function() {
23218 delete gridRow.isSaving;
23219 delete gridRow.rowEditSaveTimer;
23220 delete gridRow.rowEditSavePromise;
23222 gridRow.isError = true;
23224 if (!grid.rowEdit.errorRows){
23225 grid.rowEdit.errorRows = [];
23227 if (!service.isRowPresent( grid.rowEdit.errorRows, gridRow ) ){
23228 grid.rowEdit.errorRows.push( gridRow );
23236 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23238 * @description Removes a row from a cache of rows - either
23239 * grid.rowEdit.errorRows or grid.rowEdit.dirtyRows. If the row
23240 * is not present silently does nothing.
23241 * @param {array} rowArray the array from which to remove the row
23242 * @param {GridRow} gridRow the row that should be removed
23244 removeRow: function( rowArray, removeGridRow ){
23245 if (typeof(rowArray) === 'undefined' || rowArray === null){
23249 rowArray.forEach( function( gridRow, index ){
23250 if ( gridRow.uid === removeGridRow.uid ){
23251 rowArray.splice( index, 1);
23259 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23260 * @name isRowPresent
23261 * @description Checks whether a row is already present
23262 * in the given array
23263 * @param {array} rowArray the array in which to look for the row
23264 * @param {GridRow} gridRow the row that should be looked for
23266 isRowPresent: function( rowArray, removeGridRow ){
23267 var present = false;
23268 rowArray.forEach( function( gridRow, index ){
23269 if ( gridRow.uid === removeGridRow.uid ){
23279 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23280 * @name flushDirtyRows
23281 * @description Triggers a save event for all currently dirty rows, could
23282 * be used where user presses a save button or navigates away from the page
23284 * gridApi.rowEdit.flushDirtyRows(grid)
23286 * @param {object} grid the grid for which dirty rows should be flushed
23287 * @returns {promise} a promise that represents the aggregate of all
23288 * of the individual save promises - i.e. it will be resolved when all
23289 * the individual save promises have been resolved.
23292 flushDirtyRows: function(grid){
23294 grid.api.rowEdit.getDirtyRows().forEach( function( gridRow ){
23295 service.saveRow( grid, gridRow )();
23296 promises.push( gridRow.rowEditSavePromise );
23299 return $q.all( promises );
23305 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23306 * @name endEditCell
23307 * @description Receives an afterCellEdit event from the edit function,
23308 * and sets flags as appropriate. Only the rowEntity parameter
23309 * is processed, although other params are available. Grid
23310 * is automatically provided by the gridApi.
23311 * @param {object} rowEntity the data entity for which the cell
23314 endEditCell: function( rowEntity, colDef, newValue, previousValue ){
23315 var grid = this.grid;
23316 var gridRow = grid.getRow( rowEntity );
23317 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, dirty flag cannot be set' ); return; }
23319 if ( newValue !== previousValue || gridRow.isDirty ){
23320 if ( !grid.rowEdit.dirtyRows ){
23321 grid.rowEdit.dirtyRows = [];
23324 if ( !gridRow.isDirty ){
23325 gridRow.isDirty = true;
23326 grid.rowEdit.dirtyRows.push( gridRow );
23329 delete gridRow.isError;
23331 service.considerSetTimer( grid, gridRow );
23338 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23339 * @name beginEditCell
23340 * @description Receives a beginCellEdit event from the edit function,
23341 * and cancels any rowEditSaveTimers if present, as the user is still editing
23342 * this row. Only the rowEntity parameter
23343 * is processed, although other params are available. Grid
23344 * is automatically provided by the gridApi.
23345 * @param {object} rowEntity the data entity for which the cell
23346 * editing has commenced
23348 beginEditCell: function( rowEntity, colDef ){
23349 var grid = this.grid;
23350 var gridRow = grid.getRow( rowEntity );
23351 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be cancelled' ); return; }
23353 service.cancelTimer( grid, gridRow );
23359 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23360 * @name cancelEditCell
23361 * @description Receives a cancelCellEdit event from the edit function,
23362 * and if the row was already dirty, restarts the save timer. If the row
23363 * was not already dirty, then it's not dirty now either and does nothing.
23365 * Only the rowEntity parameter
23366 * is processed, although other params are available. Grid
23367 * is automatically provided by the gridApi.
23369 * @param {object} rowEntity the data entity for which the cell
23370 * editing was cancelled
23372 cancelEditCell: function( rowEntity, colDef ){
23373 var grid = this.grid;
23374 var gridRow = grid.getRow( rowEntity );
23375 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be set' ); return; }
23377 service.considerSetTimer( grid, gridRow );
23383 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23385 * @description cellNav tells us that the selected cell has changed. If
23386 * the new row had a timer running, then stop it similar to in a beginCellEdit
23387 * call. If the old row is dirty and not the same as the new row, then
23388 * start a timer on it.
23389 * @param {object} newRowCol the row and column that were selected
23390 * @param {object} oldRowCol the row and column that was left
23393 navigate: function( newRowCol, oldRowCol ){
23394 var grid = this.grid;
23395 if ( newRowCol.row.rowEditSaveTimer ){
23396 service.cancelTimer( grid, newRowCol.row );
23399 if ( oldRowCol && oldRowCol.row && oldRowCol.row !== newRowCol.row ){
23400 service.considerSetTimer( grid, oldRowCol.row );
23407 * @propertyOf ui.grid.rowEdit.api:GridOptions
23408 * @name rowEditWaitInterval
23409 * @description How long the grid should wait for another change on this row
23410 * before triggering a save (in milliseconds). If set to -1, then saves are
23411 * never triggered by timer (implying that the user will call flushDirtyRows()
23415 * Setting the wait interval to 4 seconds
23417 * $scope.gridOptions = { rowEditWaitInterval: 4000 }
23423 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23424 * @name considerSetTimer
23425 * @description Consider setting a timer on this row (if it is dirty). if there is a timer running
23426 * on the row and the row isn't currently saving, cancel it, using cancelTimer, then if the row is
23427 * dirty and not currently saving then set a new timer
23428 * @param {object} grid the grid for which we are processing
23429 * @param {GridRow} gridRow the row for which the timer should be adjusted
23432 considerSetTimer: function( grid, gridRow ){
23433 service.cancelTimer( grid, gridRow );
23435 if ( gridRow.isDirty && !gridRow.isSaving ){
23436 if ( grid.options.rowEditWaitInterval !== -1 ){
23437 var waitTime = grid.options.rowEditWaitInterval ? grid.options.rowEditWaitInterval : 2000;
23438 gridRow.rowEditSaveTimer = $interval( service.saveRow( grid, gridRow ), waitTime, 1);
23446 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23447 * @name cancelTimer
23448 * @description cancel the $interval for any timer running on this row
23449 * then delete the timer itself
23450 * @param {object} grid the grid for which we are processing
23451 * @param {GridRow} gridRow the row for which the timer should be adjusted
23454 cancelTimer: function( grid, gridRow ){
23455 if ( gridRow.rowEditSaveTimer && !gridRow.isSaving ){
23456 $interval.cancel(gridRow.rowEditSaveTimer);
23457 delete gridRow.rowEditSaveTimer;
23464 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23465 * @name setRowsDirty
23466 * @description Sets each of the rows passed in dataRows
23467 * to be dirty. note that if you have only just inserted the
23468 * rows into your data you will need to wait for a $digest cycle
23469 * before the gridRows are present - so often you would wrap this
23470 * call in a $interval or $timeout
23472 * $interval( function() {
23473 * gridApi.rowEdit.setRowsDirty( myDataRows);
23476 * @param {object} grid the grid for which rows should be set dirty
23477 * @param {array} dataRows the data entities for which the gridRows
23478 * should be set dirty.
23481 setRowsDirty: function( grid, myDataRows ) {
23483 myDataRows.forEach( function( value, index ){
23484 gridRow = grid.getRow( value );
23486 if ( !grid.rowEdit.dirtyRows ){
23487 grid.rowEdit.dirtyRows = [];
23490 if ( !gridRow.isDirty ){
23491 gridRow.isDirty = true;
23492 grid.rowEdit.dirtyRows.push( gridRow );
23495 delete gridRow.isError;
23497 service.considerSetTimer( grid, gridRow );
23499 gridUtil.logError( "requested row not found in rowEdit.setRowsDirty, row was: " + value );
23507 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23508 * @name setRowsClean
23509 * @description Sets each of the rows passed in dataRows
23510 * to be clean, clearing the dirty flag and the error flag, and removing
23511 * the rows from the dirty and error caches.
23512 * @param {object} grid the grid for which rows should be set clean
23513 * @param {array} dataRows the data entities for which the gridRows
23514 * should be set clean.
23517 setRowsClean: function( grid, myDataRows ) {
23520 myDataRows.forEach( function( value, index ){
23521 gridRow = grid.getRow( value );
23523 delete gridRow.isDirty;
23524 service.removeRow( grid.rowEdit.dirtyRows, gridRow );
23525 service.cancelTimer( grid, gridRow );
23527 delete gridRow.isError;
23528 service.removeRow( grid.rowEdit.errorRows, gridRow );
23530 gridUtil.logError( "requested row not found in rowEdit.setRowsClean, row was: " + value );
23543 * @name ui.grid.rowEdit.directive:uiGridEdit
23547 * @description Adds row editing features to the ui-grid-edit directive.
23550 module.directive('uiGridRowEdit', ['gridUtil', 'uiGridRowEditService', 'uiGridEditConstants',
23551 function (gridUtil, uiGridRowEditService, uiGridEditConstants) {
23555 require: '^uiGrid',
23557 compile: function () {
23559 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
23560 uiGridRowEditService.initializeGrid($scope, uiGridCtrl.grid);
23562 post: function ($scope, $elm, $attrs, uiGridCtrl) {
23572 * @name ui.grid.rowEdit.directive:uiGridViewport
23575 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
23576 * for the grid row to allow coloring of saving and error rows
23578 module.directive('uiGridViewport',
23579 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
23580 function ($compile, uiGridConstants, gridUtil, $parse) {
23582 priority: -200, // run after default directive
23584 compile: function ($elm, $attrs) {
23585 var rowRepeatDiv = angular.element($elm.children().children()[0]);
23587 var existingNgClass = rowRepeatDiv.attr("ng-class");
23588 var newNgClass = '';
23589 if ( existingNgClass ) {
23590 newNgClass = existingNgClass.slice(0, -1) + ", 'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
23592 newNgClass = "{'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
23594 rowRepeatDiv.attr("ng-class", newNgClass);
23597 pre: function ($scope, $elm, $attrs, controllers) {
23600 post: function ($scope, $elm, $attrs, controllers) {
23614 * @name ui.grid.saveState
23617 * # ui.grid.saveState
23619 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
23621 * This module provides the ability to save the grid state, and restore
23622 * it when the user returns to the page.
23624 * No UI is provided, the caller should provide their own UI/buttons
23625 * as appropriate. Usually the navigate events would be used to save
23626 * the grid state and restore it.
23631 * <div doc-module-components="ui.grid.save-state"></div>
23634 var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning', 'ui.grid.treeView']);
23638 * @name ui.grid.saveState.constant:uiGridSaveStateConstants
23640 * @description constants available in save state module
23643 module.constant('uiGridSaveStateConstants', {
23644 featureName: 'saveState'
23649 * @name ui.grid.saveState.service:uiGridSaveStateService
23651 * @description Services for saveState feature
23653 module.service('uiGridSaveStateService', ['$q', 'uiGridSaveStateConstants', 'gridUtil', '$compile', '$interval', 'uiGridConstants',
23654 function ($q, uiGridSaveStateConstants, gridUtil, $compile, $interval, uiGridConstants ) {
23658 initializeGrid: function (grid) {
23660 //add feature namespace and any properties to grid for needed state
23661 grid.saveState = {};
23662 this.defaultGridOptions(grid.options);
23666 * @name ui.grid.saveState.api:PublicApi
23668 * @description Public Api for saveState feature
23680 * @methodOf ui.grid.saveState.api:PublicApi
23681 * @description Packages the current state of the grid into
23682 * an object, and provides it to the user for saving
23683 * @returns {object} the state as a javascript object that can be saved
23685 save: function () {
23686 return service.save(grid);
23691 * @methodOf ui.grid.saveState.api:PublicApi
23692 * @description Restores the provided state into the grid
23693 * @param {scope} $scope a scope that we can broadcast on
23694 * @param {object} state the state that should be restored into the grid
23696 restore: function ( $scope, state) {
23697 service.restore(grid, $scope, state);
23703 grid.api.registerEventsFromObject(publicApi.events);
23705 grid.api.registerMethodsFromObject(publicApi.methods);
23709 defaultGridOptions: function (gridOptions) {
23710 //default option to true unless it was explicitly set to false
23713 * @name ui.grid.saveState.api:GridOptions
23715 * @description GridOptions for saveState feature, these are available to be
23716 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
23721 * @propertyOf ui.grid.saveState.api:GridOptions
23722 * @description Save the current column widths. Note that unless
23723 * you've provided the user with some way to resize their columns (say
23724 * the resize columns feature), then this makes little sense.
23725 * <br/>Defaults to true
23727 gridOptions.saveWidths = gridOptions.saveWidths !== false;
23731 * @propertyOf ui.grid.saveState.api:GridOptions
23732 * @description Restore the current column order. Note that unless
23733 * you've provided the user with some way to reorder their columns (for
23734 * example the move columns feature), this makes little sense.
23735 * <br/>Defaults to true
23737 gridOptions.saveOrder = gridOptions.saveOrder !== false;
23741 * @propertyOf ui.grid.saveState.api:GridOptions
23742 * @description Save the current scroll position. Note that this
23743 * is saved as the percentage of the grid scrolled - so if your
23744 * user returns to a grid with a significantly different number of
23745 * rows (perhaps some data has been deleted) then the scroll won't
23746 * actually show the same rows as before. If you want to scroll to
23747 * a specific row then you should instead use the saveFocus option, which
23750 * Note that this element will only be saved if the cellNav feature is
23752 * <br/>Defaults to false
23754 gridOptions.saveScroll = gridOptions.saveScroll === true;
23758 * @propertyOf ui.grid.saveState.api:GridOptions
23759 * @description Save the current focused cell. On returning
23760 * to this focused cell we'll also scroll. This option is
23761 * preferred to the saveScroll option, so is set to true by
23762 * default. If saveScroll is set to true then this option will
23765 * By default this option saves the current row number and column
23766 * number, and returns to that row and column. However, if you define
23767 * a saveRowIdentity function, then it will return you to the currently
23768 * selected column within that row (in a business sense - so if some
23769 * rows have been deleted, it will still find the same data, presuming it
23770 * still exists in the list. If it isn't in the list then it will instead
23771 * return to the same row number - i.e. scroll percentage)
23773 * Note that this option will do nothing if the cellNav
23774 * feature is not enabled.
23776 * <br/>Defaults to true (unless saveScroll is true)
23778 gridOptions.saveFocus = gridOptions.saveScroll !== true && gridOptions.saveFocus !== false;
23781 * @name saveRowIdentity
23782 * @propertyOf ui.grid.saveState.api:GridOptions
23783 * @description A function that can be called, passing in a rowEntity,
23784 * and that will return a unique id for that row. This might simply
23785 * return the `id` field from that row (if you have one), or it might
23786 * concatenate some fields within the row to make a unique value.
23788 * This value will be used to find the same row again and set the focus
23789 * to it, if it exists when we return.
23791 * <br/>Defaults to undefined
23795 * @name saveVisible
23796 * @propertyOf ui.grid.saveState.api:GridOptions
23797 * @description Save whether or not columns are visible.
23799 * <br/>Defaults to true
23801 gridOptions.saveVisible = gridOptions.saveVisible !== false;
23805 * @propertyOf ui.grid.saveState.api:GridOptions
23806 * @description Save the current sort state for each column
23808 * <br/>Defaults to true
23810 gridOptions.saveSort = gridOptions.saveSort !== false;
23814 * @propertyOf ui.grid.saveState.api:GridOptions
23815 * @description Save the current filter state for each column
23817 * <br/>Defaults to true
23819 gridOptions.saveFilter = gridOptions.saveFilter !== false;
23822 * @name saveSelection
23823 * @propertyOf ui.grid.saveState.api:GridOptions
23824 * @description Save the currently selected rows. If the `saveRowIdentity` callback
23825 * is defined, then it will save the id of the row and select that. If not, then
23826 * it will attempt to select the rows by row number, which will give the wrong results
23827 * if the data set has changed in the mean-time.
23829 * Note that this option only does anything
23830 * if the selection feature is enabled.
23832 * <br/>Defaults to true
23834 gridOptions.saveSelection = gridOptions.saveSelection !== false;
23837 * @name saveGrouping
23838 * @propertyOf ui.grid.saveState.api:GridOptions
23839 * @description Save the grouping configuration. If set to true and the
23840 * grouping feature is not enabled then does nothing.
23842 * <br/>Defaults to true
23844 gridOptions.saveGrouping = gridOptions.saveGrouping !== false;
23847 * @name saveGroupingExpandedStates
23848 * @propertyOf ui.grid.saveState.api:GridOptions
23849 * @description Save the grouping row expanded states. If set to true and the
23850 * grouping feature is not enabled then does nothing.
23852 * This can be quite a bit of data, in many cases you wouldn't want to save this
23855 * <br/>Defaults to false
23857 gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true;
23860 * @name savePinning
23861 * @propertyOf ui.grid.saveState.api:GridOptions
23862 * @description Save pinning state for columns.
23864 * <br/>Defaults to true
23866 gridOptions.savePinning = gridOptions.savePinning !== false;
23869 * @name saveTreeView
23870 * @propertyOf ui.grid.saveState.api:GridOptions
23871 * @description Save the treeView configuration. If set to true and the
23872 * treeView feature is not enabled then does nothing.
23874 * <br/>Defaults to true
23876 gridOptions.saveTreeView = gridOptions.saveTreeView !== false;
23884 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23885 * @description Saves the current grid state into an object, and
23886 * passes that object back to the caller
23887 * @param {Grid} grid the grid whose state we'd like to save
23888 * @returns {object} the state ready to be saved
23890 save: function (grid) {
23891 var savedState = {};
23893 savedState.columns = service.saveColumns( grid );
23894 savedState.scrollFocus = service.saveScrollFocus( grid );
23895 savedState.selection = service.saveSelection( grid );
23896 savedState.grouping = service.saveGrouping( grid );
23897 savedState.treeView = service.saveTreeView( grid );
23898 savedState.pagination = service.savePagination( grid );
23907 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23908 * @description Applies the provided state to the grid
23910 * @param {Grid} grid the grid whose state we'd like to restore
23911 * @param {scope} $scope a scope that we can broadcast on
23912 * @param {object} state the state we'd like to restore
23914 restore: function( grid, $scope, state ){
23915 if ( state.columns ) {
23916 service.restoreColumns( grid, state.columns );
23919 if ( state.scrollFocus ){
23920 service.restoreScrollFocus( grid, $scope, state.scrollFocus );
23923 if ( state.selection ){
23924 service.restoreSelection( grid, state.selection );
23927 if ( state.grouping ){
23928 service.restoreGrouping( grid, state.grouping );
23931 if ( state.treeView ){
23932 service.restoreTreeView( grid, state.treeView );
23935 if ( state.pagination ){
23936 service.restorePagination( grid, state.pagination );
23945 * @name saveColumns
23946 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23947 * @description Saves the column setup, including sort, filters, ordering,
23948 * pinning and column widths.
23950 * Works through the current columns, storing them in order. Stores the
23951 * column name, then the visible flag, width, sort and filters for each column.
23953 * @param {Grid} grid the grid whose state we'd like to save
23954 * @returns {array} the columns state ready to be saved
23956 saveColumns: function( grid ) {
23958 grid.getOnlyDataColumns().forEach( function( column ) {
23959 var savedColumn = {};
23960 savedColumn.name = column.name;
23962 if ( grid.options.saveVisible ){
23963 savedColumn.visible = column.visible;
23966 if ( grid.options.saveWidths ){
23967 savedColumn.width = column.width;
23970 // these two must be copied, not just pointed too - otherwise our saved state is pointing to the same object as current state
23971 if ( grid.options.saveSort ){
23972 savedColumn.sort = angular.copy( column.sort );
23975 if ( grid.options.saveFilter ){
23976 savedColumn.filters = [];
23977 column.filters.forEach( function( filter ){
23978 var copiedFilter = {};
23979 angular.forEach( filter, function( value, key) {
23980 if ( key !== 'condition' && key !== '$$hashKey' && key !== 'placeholder'){
23981 copiedFilter[key] = value;
23984 savedColumn.filters.push(copiedFilter);
23988 if ( !!grid.api.pinning && grid.options.savePinning ){
23989 savedColumn.pinned = column.renderContainer ? column.renderContainer : '';
23992 columns.push( savedColumn );
24001 * @name saveScrollFocus
24002 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24003 * @description Saves the currently scroll or focus.
24005 * If cellNav isn't present then does nothing - we can't return
24006 * to the scroll position without cellNav anyway.
24008 * If the cellNav module is present, and saveFocus is true, then
24009 * it saves the currently focused cell. If rowIdentity is present
24010 * then saves using rowIdentity, otherwise saves visibleRowNum.
24012 * If the cellNav module is not present, and saveScroll is true, then
24013 * it approximates the current scroll row and column, and saves that.
24015 * @param {Grid} grid the grid whose state we'd like to save
24016 * @returns {object} the selection state ready to be saved
24018 saveScrollFocus: function( grid ){
24019 if ( !grid.api.cellNav ){
24023 var scrollFocus = {};
24024 if ( grid.options.saveFocus ){
24025 scrollFocus.focus = true;
24026 var rowCol = grid.api.cellNav.getFocusedCell();
24027 if ( rowCol !== null ) {
24028 if ( rowCol.col !== null ){
24029 scrollFocus.colName = rowCol.col.colDef.name;
24031 if ( rowCol.row !== null ){
24032 scrollFocus.rowVal = service.getRowVal( grid, rowCol.row );
24037 if ( grid.options.saveScroll || grid.options.saveFocus && !scrollFocus.colName && !scrollFocus.rowVal ) {
24038 scrollFocus.focus = false;
24039 if ( grid.renderContainers.body.prevRowScrollIndex ){
24040 scrollFocus.rowVal = service.getRowVal( grid, grid.renderContainers.body.visibleRowCache[ grid.renderContainers.body.prevRowScrollIndex ]);
24043 if ( grid.renderContainers.body.prevColScrollIndex ){
24044 scrollFocus.colName = grid.renderContainers.body.visibleColumnCache[ grid.renderContainers.body.prevColScrollIndex ].name;
24048 return scrollFocus;
24054 * @name saveSelection
24055 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24056 * @description Saves the currently selected rows, if the selection feature is enabled
24057 * @param {Grid} grid the grid whose state we'd like to save
24058 * @returns {array} the selection state ready to be saved
24060 saveSelection: function( grid ){
24061 if ( !grid.api.selection || !grid.options.saveSelection ){
24065 var selection = grid.api.selection.getSelectedGridRows().map( function( gridRow ) {
24066 return service.getRowVal( grid, gridRow );
24075 * @name saveGrouping
24076 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24077 * @description Saves the grouping state, if the grouping feature is enabled
24078 * @param {Grid} grid the grid whose state we'd like to save
24079 * @returns {object} the grouping state ready to be saved
24081 saveGrouping: function( grid ){
24082 if ( !grid.api.grouping || !grid.options.saveGrouping ){
24086 return grid.api.grouping.getGrouping( grid.options.saveGroupingExpandedStates );
24092 * @name savePagination
24093 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24094 * @description Saves the pagination state, if the pagination feature is enabled
24095 * @param {Grid} grid the grid whose state we'd like to save
24096 * @returns {object} the pagination state ready to be saved
24098 savePagination: function( grid ) {
24099 if ( !grid.api.pagination || !grid.options.paginationPageSize ){
24104 paginationCurrentPage: grid.options.paginationCurrentPage,
24105 paginationPageSize: grid.options.paginationPageSize
24112 * @name saveTreeView
24113 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24114 * @description Saves the tree view state, if the tree feature is enabled
24115 * @param {Grid} grid the grid whose state we'd like to save
24116 * @returns {object} the tree view state ready to be saved
24118 saveTreeView: function( grid ){
24119 if ( !grid.api.treeView || !grid.options.saveTreeView ){
24123 return grid.api.treeView.getTreeView();
24130 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24131 * @description Helper function that gets either the rowNum or
24132 * the saveRowIdentity, given a gridRow
24133 * @param {Grid} grid the grid the row is in
24134 * @param {GridRow} gridRow the row we want the rowNum for
24135 * @returns {object} an object containing { identity: true/false, row: rowNumber/rowIdentity }
24138 getRowVal: function( grid, gridRow ){
24144 if ( grid.options.saveRowIdentity ){
24145 rowVal.identity = true;
24146 rowVal.row = grid.options.saveRowIdentity( gridRow.entity );
24148 rowVal.identity = false;
24149 rowVal.row = grid.renderContainers.body.visibleRowCache.indexOf( gridRow );
24157 * @name restoreColumns
24158 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24159 * @description Restores the columns, including order, visible, width,
24160 * pinning, sort and filters.
24162 * @param {Grid} grid the grid whose state we'd like to restore
24163 * @param {object} columnsState the list of columns we had before, with their state
24165 restoreColumns: function( grid, columnsState ){
24166 var isSortChanged = false;
24168 columnsState.forEach( function( columnState, index ) {
24169 var currentCol = grid.getColumn( columnState.name );
24171 if ( currentCol && !grid.isRowHeaderColumn(currentCol) ){
24172 if ( grid.options.saveVisible &&
24173 ( currentCol.visible !== columnState.visible ||
24174 currentCol.colDef.visible !== columnState.visible ) ){
24175 currentCol.visible = columnState.visible;
24176 currentCol.colDef.visible = columnState.visible;
24177 grid.api.core.raise.columnVisibilityChanged(currentCol);
24180 if ( grid.options.saveWidths && currentCol.width !== columnState.width){
24181 currentCol.width = columnState.width;
24182 currentCol.hasCustomWidth = true;
24185 if ( grid.options.saveSort &&
24186 !angular.equals(currentCol.sort, columnState.sort) &&
24187 !( currentCol.sort === undefined && angular.isEmpty(columnState.sort) ) ){
24188 currentCol.sort = angular.copy( columnState.sort );
24189 isSortChanged = true;
24192 if ( grid.options.saveFilter &&
24193 !angular.equals(currentCol.filters, columnState.filters ) ){
24194 columnState.filters.forEach( function( filter, index ){
24195 angular.extend( currentCol.filters[index], filter );
24196 if ( typeof(filter.term) === 'undefined' || filter.term === null ){
24197 delete currentCol.filters[index].term;
24200 grid.api.core.raise.filterChanged();
24203 if ( !!grid.api.pinning && grid.options.savePinning && currentCol.renderContainer !== columnState.pinned ){
24204 grid.api.pinning.pinColumn(currentCol, columnState.pinned);
24207 var currentIndex = grid.getOnlyDataColumns().indexOf( currentCol );
24208 if (currentIndex !== -1) {
24209 if (grid.options.saveOrder && currentIndex !== index) {
24210 var column = grid.columns.splice(currentIndex + grid.rowHeaderColumns.length, 1)[0];
24211 grid.columns.splice(index + grid.rowHeaderColumns.length, 0, column);
24217 if ( isSortChanged ) {
24218 grid.api.core.raise.sortChanged( grid, grid.getColumnSorting() );
24225 * @name restoreScrollFocus
24226 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24227 * @description Scrolls to the position that was saved. If focus is true, then
24228 * sets focus to the specified row/col. If focus is false, then scrolls to the
24229 * specified row/col.
24231 * @param {Grid} grid the grid whose state we'd like to restore
24232 * @param {scope} $scope a scope that we can broadcast on
24233 * @param {object} scrollFocusState the scroll/focus state ready to be restored
24235 restoreScrollFocus: function( grid, $scope, scrollFocusState ){
24236 if ( !grid.api.cellNav ){
24241 if ( scrollFocusState.colName ){
24242 var colDefs = grid.options.columnDefs.filter( function( colDef ) { return colDef.name === scrollFocusState.colName; });
24243 if ( colDefs.length > 0 ){
24244 colDef = colDefs[0];
24248 if ( scrollFocusState.rowVal && scrollFocusState.rowVal.row ){
24249 if ( scrollFocusState.rowVal.identity ){
24250 row = service.findRowByIdentity( grid, scrollFocusState.rowVal );
24252 row = grid.renderContainers.body.visibleRowCache[ scrollFocusState.rowVal.row ];
24256 var entity = row && row.entity ? row.entity : null ;
24258 if ( colDef || entity ) {
24259 if (scrollFocusState.focus ){
24260 grid.api.cellNav.scrollToFocus( entity, colDef );
24262 grid.scrollTo( entity, colDef );
24270 * @name restoreSelection
24271 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24272 * @description Selects the rows that are provided in the selection
24273 * state. If you are using `saveRowIdentity` and more than one row matches the identity
24274 * function then only the first is selected.
24275 * @param {Grid} grid the grid whose state we'd like to restore
24276 * @param {object} selectionState the selection state ready to be restored
24278 restoreSelection: function( grid, selectionState ){
24279 if ( !grid.api.selection ){
24283 grid.api.selection.clearSelectedRows();
24285 selectionState.forEach( function( rowVal ) {
24286 if ( rowVal.identity ){
24287 var foundRow = service.findRowByIdentity( grid, rowVal );
24290 grid.api.selection.selectRow( foundRow.entity );
24294 grid.api.selection.selectRowByVisibleIndex( rowVal.row );
24302 * @name restoreGrouping
24303 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24304 * @description Restores the grouping configuration, if the grouping feature
24306 * @param {Grid} grid the grid whose state we'd like to restore
24307 * @param {object} groupingState the grouping state ready to be restored
24309 restoreGrouping: function( grid, groupingState ){
24310 if ( !grid.api.grouping || typeof(groupingState) === 'undefined' || groupingState === null || angular.equals(groupingState, {}) ){
24314 grid.api.grouping.setGrouping( groupingState );
24319 * @name restoreTreeView
24320 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24321 * @description Restores the tree view configuration, if the tree view feature
24323 * @param {Grid} grid the grid whose state we'd like to restore
24324 * @param {object} treeViewState the tree view state ready to be restored
24326 restoreTreeView: function( grid, treeViewState ){
24327 if ( !grid.api.treeView || typeof(treeViewState) === 'undefined' || treeViewState === null || angular.equals(treeViewState, {}) ){
24331 grid.api.treeView.setTreeView( treeViewState );
24336 * @name restorePagination
24337 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24338 * @description Restores the pagination information, if pagination is enabled.
24339 * @param {Grid} grid the grid whose state we'd like to restore
24340 * @param {object} pagination the pagination object to be restored
24341 * @param {number} pagination.paginationCurrentPage the page number to restore
24342 * @param {number} pagination.paginationPageSize the number of items displayed per page
24344 restorePagination: function( grid, pagination ){
24345 if ( !grid.api.pagination || !grid.options.paginationPageSize ){
24349 grid.options.paginationCurrentPage = pagination.paginationCurrentPage;
24350 grid.options.paginationPageSize = pagination.paginationPageSize;
24355 * @name findRowByIdentity
24356 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24357 * @description Finds a row given it's identity value, returns the first found row
24358 * if any are found, otherwise returns null if no rows are found.
24359 * @param {Grid} grid the grid whose state we'd like to restore
24360 * @param {object} rowVal the row we'd like to find
24361 * @returns {gridRow} the found row, or null if none found
24363 findRowByIdentity: function( grid, rowVal ){
24364 if ( !grid.options.saveRowIdentity ){
24368 var filteredRows = grid.rows.filter( function( gridRow ) {
24369 if ( grid.options.saveRowIdentity( gridRow.entity ) === rowVal.row ){
24376 if ( filteredRows.length > 0 ){
24377 return filteredRows[0];
24391 * @name ui.grid.saveState.directive:uiGridSaveState
24395 * @description Adds saveState features to grid
24398 <example module="app">
24399 <file name="app.js">
24400 var app = angular.module('app', ['ui.grid', 'ui.grid.saveState']);
24402 app.controller('MainCtrl', ['$scope', function ($scope) {
24404 { name: 'Bob', title: 'CEO' },
24405 { name: 'Frank', title: 'Lowly Developer' }
24408 $scope.gridOptions = {
24411 {name: 'title', enableCellEdit: true}
24417 <file name="index.html">
24418 <div ng-controller="MainCtrl">
24419 <div ui-grid="gridOptions" ui-grid-save-state></div>
24424 module.directive('uiGridSaveState', ['uiGridSaveStateConstants', 'uiGridSaveStateService', 'gridUtil', '$compile',
24425 function (uiGridSaveStateConstants, uiGridSaveStateService, gridUtil, $compile) {
24429 require: '^uiGrid',
24431 link: function ($scope, $elm, $attrs, uiGridCtrl) {
24432 uiGridSaveStateService.initializeGrid(uiGridCtrl.grid);
24444 * @name ui.grid.selection
24447 * # ui.grid.selection
24448 * This module provides row selection
24450 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
24452 * <div doc-module-components="ui.grid.selection"></div>
24455 var module = angular.module('ui.grid.selection', ['ui.grid']);
24459 * @name ui.grid.selection.constant:uiGridSelectionConstants
24461 * @description constants available in selection module
24463 module.constant('uiGridSelectionConstants', {
24464 featureName: "selection",
24465 selectionRowHeaderColName: 'selectionRowHeaderCol'
24468 //add methods to GridRow
24469 angular.module('ui.grid').config(['$provide', function($provide) {
24470 $provide.decorator('GridRow', ['$delegate', function($delegate) {
24474 * @name ui.grid.selection.api:GridRow
24476 * @description GridRow prototype functions added for selection
24481 * @name enableSelection
24482 * @propertyOf ui.grid.selection.api:GridRow
24483 * @description Enable row selection for this row, only settable by internal code.
24485 * The grouping feature, for example, might set group header rows to not be selectable.
24486 * <br/>Defaults to true
24492 * @propertyOf ui.grid.selection.api:GridRow
24493 * @description Selected state of row. Should be readonly. Make any changes to selected state using setSelected().
24494 * <br/>Defaults to false
24500 * @name setSelected
24501 * @methodOf ui.grid.selection.api:GridRow
24502 * @description Sets the isSelected property and updates the selectedCount
24503 * Changes to isSelected state should only be made via this function
24504 * @param {bool} selected value to set
24506 $delegate.prototype.setSelected = function(selected) {
24507 this.isSelected = selected;
24509 this.grid.selection.selectedCount++;
24512 this.grid.selection.selectedCount--;
24522 * @name ui.grid.selection.service:uiGridSelectionService
24524 * @description Services for selection features
24526 module.service('uiGridSelectionService', ['$q', '$templateCache', 'uiGridSelectionConstants', 'gridUtil',
24527 function ($q, $templateCache, uiGridSelectionConstants, gridUtil) {
24531 initializeGrid: function (grid) {
24533 //add feature namespace and any properties to grid for needed
24536 * @name ui.grid.selection.grid:selection
24538 * @description Grid properties and functions added for selection
24540 grid.selection = {};
24541 grid.selection.lastSelectedRow = null;
24542 grid.selection.selectAll = false;
24547 * @name selectedCount
24548 * @propertyOf ui.grid.selection.grid:selection
24549 * @description Current count of selected rows
24551 * var count = grid.selection.selectedCount
24553 grid.selection.selectedCount = 0;
24555 service.defaultGridOptions(grid.options);
24559 * @name ui.grid.selection.api:PublicApi
24561 * @description Public Api for selection feature
24568 * @name rowSelectionChanged
24569 * @eventOf ui.grid.selection.api:PublicApi
24570 * @description is raised after the row.isSelected state is changed
24571 * @param {GridRow} row the row that was selected/deselected
24572 * @param {Event} event object if raised from an event
24574 rowSelectionChanged: function (scope, row, evt) {
24578 * @name rowSelectionChangedBatch
24579 * @eventOf ui.grid.selection.api:PublicApi
24580 * @description is raised after the row.isSelected state is changed
24581 * in bulk, if the `enableSelectionBatchEvent` option is set to true
24582 * (which it is by default). This allows more efficient processing
24584 * @param {array} rows the rows that were selected/deselected
24585 * @param {Event} event object if raised from an event
24587 rowSelectionChangedBatch: function (scope, rows, evt) {
24595 * @name toggleRowSelection
24596 * @methodOf ui.grid.selection.api:PublicApi
24597 * @description Toggles data row as selected or unselected
24598 * @param {object} rowEntity gridOptions.data[] array instance
24599 * @param {Event} event object if raised from an event
24601 toggleRowSelection: function (rowEntity, evt) {
24602 var row = grid.getRow(rowEntity);
24603 if (row !== null) {
24604 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24610 * @methodOf ui.grid.selection.api:PublicApi
24611 * @description Select the data row
24612 * @param {object} rowEntity gridOptions.data[] array instance
24613 * @param {Event} event object if raised from an event
24615 selectRow: function (rowEntity, evt) {
24616 var row = grid.getRow(rowEntity);
24617 if (row !== null && !row.isSelected) {
24618 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24623 * @name selectRowByVisibleIndex
24624 * @methodOf ui.grid.selection.api:PublicApi
24625 * @description Select the specified row by visible index (i.e. if you
24626 * specify row 0 you'll get the first visible row selected). In this context
24627 * visible means of those rows that are theoretically visible (i.e. not filtered),
24628 * rather than rows currently rendered on the screen.
24629 * @param {number} index index within the rowsVisible array
24630 * @param {Event} event object if raised from an event
24632 selectRowByVisibleIndex: function ( rowNum, evt ) {
24633 var row = grid.renderContainers.body.visibleRowCache[rowNum];
24634 if (row !== null && typeof(row) !== 'undefined' && !row.isSelected) {
24635 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24640 * @name unSelectRow
24641 * @methodOf ui.grid.selection.api:PublicApi
24642 * @description UnSelect the data row
24643 * @param {object} rowEntity gridOptions.data[] array instance
24644 * @param {Event} event object if raised from an event
24646 unSelectRow: function (rowEntity, evt) {
24647 var row = grid.getRow(rowEntity);
24648 if (row !== null && row.isSelected) {
24649 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24654 * @name selectAllRows
24655 * @methodOf ui.grid.selection.api:PublicApi
24656 * @description Selects all rows. Does nothing if multiSelect = false
24657 * @param {Event} event object if raised from an event
24659 selectAllRows: function (evt) {
24660 if (grid.options.multiSelect === false) {
24664 var changedRows = [];
24665 grid.rows.forEach(function (row) {
24666 if ( !row.isSelected && row.enableSelection !== false ){
24667 row.setSelected(true);
24668 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24671 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24672 grid.selection.selectAll = true;
24676 * @name selectAllVisibleRows
24677 * @methodOf ui.grid.selection.api:PublicApi
24678 * @description Selects all visible rows. Does nothing if multiSelect = false
24679 * @param {Event} event object if raised from an event
24681 selectAllVisibleRows: function (evt) {
24682 if (grid.options.multiSelect === false) {
24686 var changedRows = [];
24687 grid.rows.forEach(function (row) {
24689 if (!row.isSelected && row.enableSelection !== false){
24690 row.setSelected(true);
24691 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24694 if (row.isSelected){
24695 row.setSelected(false);
24696 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24700 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24701 grid.selection.selectAll = true;
24705 * @name clearSelectedRows
24706 * @methodOf ui.grid.selection.api:PublicApi
24707 * @description Unselects all rows
24708 * @param {Event} event object if raised from an event
24710 clearSelectedRows: function (evt) {
24711 service.clearSelectedRows(grid, evt);
24715 * @name getSelectedRows
24716 * @methodOf ui.grid.selection.api:PublicApi
24717 * @description returns all selectedRow's entity references
24719 getSelectedRows: function () {
24720 return service.getSelectedRows(grid).map(function (gridRow) {
24721 return gridRow.entity;
24726 * @name getSelectedGridRows
24727 * @methodOf ui.grid.selection.api:PublicApi
24728 * @description returns all selectedRow's as gridRows
24730 getSelectedGridRows: function () {
24731 return service.getSelectedRows(grid);
24735 * @name getSelectedCount
24736 * @methodOf ui.grid.selection.api:PublicApi
24737 * @description returns the number of rows selected
24739 getSelectedCount: function () {
24740 return grid.selection.selectedCount;
24744 * @name setMultiSelect
24745 * @methodOf ui.grid.selection.api:PublicApi
24746 * @description Sets the current gridOption.multiSelect to true or false
24747 * @param {bool} multiSelect true to allow multiple rows
24749 setMultiSelect: function (multiSelect) {
24750 grid.options.multiSelect = multiSelect;
24754 * @name setModifierKeysToMultiSelect
24755 * @methodOf ui.grid.selection.api:PublicApi
24756 * @description Sets the current gridOption.modifierKeysToMultiSelect to true or false
24757 * @param {bool} modifierKeysToMultiSelect true to only allow multiple rows when using ctrlKey or shiftKey is used
24759 setModifierKeysToMultiSelect: function (modifierKeysToMultiSelect) {
24760 grid.options.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
24764 * @name getSelectAllState
24765 * @methodOf ui.grid.selection.api:PublicApi
24766 * @description Returns whether or not the selectAll checkbox is currently ticked. The
24767 * grid doesn't automatically select rows when you add extra data - so when you add data
24768 * you need to explicitly check whether the selectAll is set, and then call setVisible rows
24771 getSelectAllState: function () {
24772 return grid.selection.selectAll;
24779 grid.api.registerEventsFromObject(publicApi.events);
24781 grid.api.registerMethodsFromObject(publicApi.methods);
24785 defaultGridOptions: function (gridOptions) {
24786 //default option to true unless it was explicitly set to false
24789 * @name ui.grid.selection.api:GridOptions
24791 * @description GridOptions for selection feature, these are available to be
24792 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
24797 * @name enableRowSelection
24798 * @propertyOf ui.grid.selection.api:GridOptions
24799 * @description Enable row selection for entire grid.
24800 * <br/>Defaults to true
24802 gridOptions.enableRowSelection = gridOptions.enableRowSelection !== false;
24805 * @name multiSelect
24806 * @propertyOf ui.grid.selection.api:GridOptions
24807 * @description Enable multiple row selection for entire grid
24808 * <br/>Defaults to true
24810 gridOptions.multiSelect = gridOptions.multiSelect !== false;
24814 * @propertyOf ui.grid.selection.api:GridOptions
24815 * @description Prevent a row from being unselected. Works in conjunction
24816 * with `multiselect = false` and `gridApi.selection.selectRow()` to allow
24817 * you to create a single selection only grid - a row is always selected, you
24818 * can only select different rows, you can't unselect the row.
24819 * <br/>Defaults to false
24821 gridOptions.noUnselect = gridOptions.noUnselect === true;
24824 * @name modifierKeysToMultiSelect
24825 * @propertyOf ui.grid.selection.api:GridOptions
24826 * @description Enable multiple row selection only when using the ctrlKey or shiftKey. Requires multiSelect to be true.
24827 * <br/>Defaults to false
24829 gridOptions.modifierKeysToMultiSelect = gridOptions.modifierKeysToMultiSelect === true;
24832 * @name enableRowHeaderSelection
24833 * @propertyOf ui.grid.selection.api:GridOptions
24834 * @description Enable a row header to be used for selection
24835 * <br/>Defaults to true
24837 gridOptions.enableRowHeaderSelection = gridOptions.enableRowHeaderSelection !== false;
24840 * @name enableFullRowSelection
24841 * @propertyOf ui.grid.selection.api:GridOptions
24842 * @description Enable selection by clicking anywhere on the row. Defaults to
24843 * false if `enableRowHeaderSelection` is true, otherwise defaults to false.
24845 if ( typeof(gridOptions.enableFullRowSelection) === 'undefined' ){
24846 gridOptions.enableFullRowSelection = !gridOptions.enableRowHeaderSelection;
24850 * @name enableSelectAll
24851 * @propertyOf ui.grid.selection.api:GridOptions
24852 * @description Enable the select all checkbox at the top of the selectionRowHeader
24853 * <br/>Defaults to true
24855 gridOptions.enableSelectAll = gridOptions.enableSelectAll !== false;
24858 * @name enableSelectionBatchEvent
24859 * @propertyOf ui.grid.selection.api:GridOptions
24860 * @description If selected rows are changed in bulk, either via the API or
24861 * via the selectAll checkbox, then a separate event is fired. Setting this
24862 * option to false will cause the rowSelectionChanged event to be called multiple times
24864 * <br/>Defaults to true
24866 gridOptions.enableSelectionBatchEvent = gridOptions.enableSelectionBatchEvent !== false;
24869 * @name selectionRowHeaderWidth
24870 * @propertyOf ui.grid.selection.api:GridOptions
24871 * @description can be used to set a custom width for the row header selection column
24872 * <br/>Defaults to 30px
24874 gridOptions.selectionRowHeaderWidth = angular.isDefined(gridOptions.selectionRowHeaderWidth) ? gridOptions.selectionRowHeaderWidth : 30;
24878 * @name enableFooterTotalSelected
24879 * @propertyOf ui.grid.selection.api:GridOptions
24880 * @description Shows the total number of selected items in footer if true.
24881 * <br/>Defaults to true.
24882 * <br/>GridOptions.showGridFooter must also be set to true.
24884 gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false;
24888 * @name isRowSelectable
24889 * @propertyOf ui.grid.selection.api:GridOptions
24890 * @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property.
24893 gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop;
24898 * @name toggleRowSelection
24899 * @methodOf ui.grid.selection.service:uiGridSelectionService
24900 * @description Toggles row as selected or unselected
24901 * @param {Grid} grid grid object
24902 * @param {GridRow} row row to select or deselect
24903 * @param {Event} event object if resulting from event
24904 * @param {bool} multiSelect if false, only one row at time can be selected
24905 * @param {bool} noUnselect if true then rows cannot be unselected
24907 toggleRowSelection: function (grid, row, evt, multiSelect, noUnselect) {
24908 var selected = row.isSelected;
24910 if ( row.enableSelection === false && !selected ){
24915 if (!multiSelect && !selected) {
24916 service.clearSelectedRows(grid, evt);
24917 } else if (!multiSelect && selected) {
24918 selectedRows = service.getSelectedRows(grid);
24919 if (selectedRows.length > 1) {
24920 selected = false; // Enable reselect of the row
24921 service.clearSelectedRows(grid, evt);
24925 if (selected && noUnselect){
24926 // don't deselect the row
24928 row.setSelected(!selected);
24929 if (row.isSelected === true) {
24930 grid.selection.lastSelectedRow = row;
24933 selectedRows = service.getSelectedRows(grid);
24934 grid.selection.selectAll = grid.rows.length === selectedRows.length;
24936 grid.api.selection.raise.rowSelectionChanged(row, evt);
24941 * @name shiftSelect
24942 * @methodOf ui.grid.selection.service:uiGridSelectionService
24943 * @description selects a group of rows from the last selected row using the shift key
24944 * @param {Grid} grid grid object
24945 * @param {GridRow} clicked row
24946 * @param {Event} event object if raised from an event
24947 * @param {bool} multiSelect if false, does nothing this is for multiSelect only
24949 shiftSelect: function (grid, row, evt, multiSelect) {
24950 if (!multiSelect) {
24953 var selectedRows = service.getSelectedRows(grid);
24954 var fromRow = selectedRows.length > 0 ? grid.renderContainers.body.visibleRowCache.indexOf(grid.selection.lastSelectedRow) : 0;
24955 var toRow = grid.renderContainers.body.visibleRowCache.indexOf(row);
24956 //reverse select direction
24957 if (fromRow > toRow) {
24963 var changedRows = [];
24964 for (var i = fromRow; i <= toRow; i++) {
24965 var rowToSelect = grid.renderContainers.body.visibleRowCache[i];
24967 if ( !rowToSelect.isSelected && rowToSelect.enableSelection !== false ){
24968 rowToSelect.setSelected(true);
24969 grid.selection.lastSelectedRow = rowToSelect;
24970 service.decideRaiseSelectionEvent( grid, rowToSelect, changedRows, evt );
24974 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24978 * @name getSelectedRows
24979 * @methodOf ui.grid.selection.service:uiGridSelectionService
24980 * @description Returns all the selected rows
24981 * @param {Grid} grid grid object
24983 getSelectedRows: function (grid) {
24984 return grid.rows.filter(function (row) {
24985 return row.isSelected;
24991 * @name clearSelectedRows
24992 * @methodOf ui.grid.selection.service:uiGridSelectionService
24993 * @description Clears all selected rows
24994 * @param {Grid} grid grid object
24995 * @param {Event} event object if raised from an event
24997 clearSelectedRows: function (grid, evt) {
24998 var changedRows = [];
24999 service.getSelectedRows(grid).forEach(function (row) {
25000 if ( row.isSelected ){
25001 row.setSelected(false);
25002 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
25005 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
25006 grid.selection.selectAll = false;
25007 grid.selection.selectedCount = 0;
25012 * @name decideRaiseSelectionEvent
25013 * @methodOf ui.grid.selection.service:uiGridSelectionService
25014 * @description Decides whether to raise a single event or a batch event
25015 * @param {Grid} grid grid object
25016 * @param {GridRow} row row that has changed
25017 * @param {array} changedRows an array to which we can append the changed
25018 * @param {Event} event object if raised from an event
25019 * row if we're doing batch events
25021 decideRaiseSelectionEvent: function( grid, row, changedRows, evt ){
25022 if ( !grid.options.enableSelectionBatchEvent ){
25023 grid.api.selection.raise.rowSelectionChanged(row, evt);
25025 changedRows.push(row);
25031 * @name raiseSelectionEvent
25032 * @methodOf ui.grid.selection.service:uiGridSelectionService
25033 * @description Decides whether we need to raise a batch event, and
25034 * raises it if we do.
25035 * @param {Grid} grid grid object
25036 * @param {array} changedRows an array of changed rows, only populated
25037 * @param {Event} event object if raised from an event
25038 * if we're doing batch events
25040 decideRaiseSelectionBatchEvent: function( grid, changedRows, evt ){
25041 if ( changedRows.length > 0 ){
25042 grid.api.selection.raise.rowSelectionChangedBatch(changedRows, evt);
25053 * @name ui.grid.selection.directive:uiGridSelection
25057 * @description Adds selection features to grid
25060 <example module="app">
25061 <file name="app.js">
25062 var app = angular.module('app', ['ui.grid', 'ui.grid.selection']);
25064 app.controller('MainCtrl', ['$scope', function ($scope) {
25066 { name: 'Bob', title: 'CEO' },
25067 { name: 'Frank', title: 'Lowly Developer' }
25070 $scope.columnDefs = [
25071 {name: 'name', enableCellEdit: true},
25072 {name: 'title', enableCellEdit: true}
25076 <file name="index.html">
25077 <div ng-controller="MainCtrl">
25078 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-selection></div>
25083 module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', 'uiGridConstants',
25084 function (uiGridSelectionConstants, uiGridSelectionService, $templateCache, uiGridConstants) {
25088 require: '^uiGrid',
25090 compile: function () {
25092 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
25093 uiGridSelectionService.initializeGrid(uiGridCtrl.grid);
25094 if (uiGridCtrl.grid.options.enableRowHeaderSelection) {
25095 var selectionRowHeaderDef = {
25096 name: uiGridSelectionConstants.selectionRowHeaderColName,
25098 width: uiGridCtrl.grid.options.selectionRowHeaderWidth,
25100 cellTemplate: 'ui-grid/selectionRowHeader',
25101 headerCellTemplate: 'ui-grid/selectionHeaderCell',
25102 enableColumnResizing: false,
25103 enableColumnMenu: false,
25104 exporterSuppressExport: true,
25105 allowCellFocus: true
25108 uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef);
25111 var processorSet = false;
25113 var processSelectableRows = function( rows ){
25114 rows.forEach(function(row){
25115 row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row);
25120 var updateOptions = function(){
25121 if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) {
25122 uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500);
25123 processorSet = true;
25129 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback( updateOptions, [uiGridConstants.dataChange.OPTIONS] );
25131 $scope.$on( '$destroy', dataChangeDereg);
25133 post: function ($scope, $elm, $attrs, uiGridCtrl) {
25141 module.directive('uiGridSelectionRowHeaderButtons', ['$templateCache', 'uiGridSelectionService', 'gridUtil',
25142 function ($templateCache, uiGridSelectionService, gridUtil) {
25146 template: $templateCache.get('ui-grid/selectionRowHeaderButtons'),
25148 require: '^uiGrid',
25149 link: function($scope, $elm, $attrs, uiGridCtrl) {
25150 var self = uiGridCtrl.grid;
25151 $scope.selectButtonClick = selectButtonClick;
25153 // On IE, prevent mousedowns on the select button from starting a selection.
25154 // If this is not done and you shift+click on another row, the browser will select a big chunk of text
25155 if (gridUtil.detectBrowser() === 'ie') {
25156 $elm.on('mousedown', selectButtonMouseDown);
25160 function selectButtonClick(row, evt) {
25161 evt.stopPropagation();
25163 if (evt.shiftKey) {
25164 uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
25166 else if (evt.ctrlKey || evt.metaKey) {
25167 uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect);
25170 uiGridSelectionService.toggleRowSelection(self, row, evt, (self.options.multiSelect && !self.options.modifierKeysToMultiSelect), self.options.noUnselect);
25174 function selectButtonMouseDown(evt) {
25175 if (evt.ctrlKey || evt.shiftKey) {
25176 evt.target.onselectstart = function () { return false; };
25177 window.setTimeout(function () { evt.target.onselectstart = null; }, 0);
25184 module.directive('uiGridSelectionSelectAllButtons', ['$templateCache', 'uiGridSelectionService',
25185 function ($templateCache, uiGridSelectionService) {
25189 template: $templateCache.get('ui-grid/selectionSelectAllButtons'),
25191 link: function($scope, $elm, $attrs, uiGridCtrl) {
25192 var self = $scope.col.grid;
25194 $scope.headerButtonClick = function(row, evt) {
25195 if ( self.selection.selectAll ){
25196 uiGridSelectionService.clearSelectedRows(self, evt);
25197 if ( self.options.noUnselect ){
25198 self.api.selection.selectRowByVisibleIndex(0, evt);
25200 self.selection.selectAll = false;
25202 if ( self.options.multiSelect ){
25203 self.api.selection.selectAllVisibleRows(evt);
25204 self.selection.selectAll = true;
25214 * @name ui.grid.selection.directive:uiGridViewport
25217 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
25220 module.directive('uiGridViewport',
25221 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService',
25222 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService) {
25224 priority: -200, // run after default directive
25226 compile: function ($elm, $attrs) {
25227 var rowRepeatDiv = angular.element($elm.children().children()[0]);
25229 var existingNgClass = rowRepeatDiv.attr("ng-class");
25230 var newNgClass = '';
25231 if ( existingNgClass ) {
25232 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-row-selected': row.isSelected}";
25234 newNgClass = "{'ui-grid-row-selected': row.isSelected}";
25236 rowRepeatDiv.attr("ng-class", newNgClass);
25239 pre: function ($scope, $elm, $attrs, controllers) {
25242 post: function ($scope, $elm, $attrs, controllers) {
25251 * @name ui.grid.selection.directive:uiGridCell
25255 * @description Stacks on top of ui.grid.uiGridCell to provide selection feature
25257 module.directive('uiGridCell',
25258 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', '$timeout',
25259 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService, $timeout) {
25261 priority: -200, // run after default uiGridCell directive
25263 require: '?^uiGrid',
25265 link: function ($scope, $elm, $attrs, uiGridCtrl) {
25267 var touchStartTime = 0;
25268 var touchTimeout = 300;
25270 // Bind to keydown events in the render container
25271 if (uiGridCtrl.grid.api.cellNav) {
25273 uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
25274 if (rowCol === null ||
25275 rowCol.row !== $scope.row ||
25276 rowCol.col !== $scope.col) {
25280 if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
25281 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
25285 // uiGridCellNavService.scrollToIfNecessary(uiGridCtrl.grid, rowCol.row, rowCol.col);
25289 //$elm.bind('keydown', function (evt) {
25290 // if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
25291 // uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
25292 // $scope.$apply();
25296 var selectCells = function(evt){
25297 // if we get a click, then stop listening for touchend
25298 $elm.off('touchend', touchEnd);
25300 if (evt.shiftKey) {
25301 uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect);
25303 else if (evt.ctrlKey || evt.metaKey) {
25304 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect, $scope.grid.options.noUnselect);
25307 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
25311 // don't re-enable the touchend handler for a little while - some devices generate both, and it will
25312 // take a little while to move your hand from the mouse to the screen if you have both modes of input
25313 $timeout(function() {
25314 $elm.on('touchend', touchEnd);
25318 var touchStart = function(evt){
25319 touchStartTime = (new Date()).getTime();
25321 // if we get a touch event, then stop listening for click
25322 $elm.off('click', selectCells);
25325 var touchEnd = function(evt) {
25326 var touchEndTime = (new Date()).getTime();
25327 var touchTime = touchEndTime - touchStartTime;
25329 if (touchTime < touchTimeout ) {
25334 // don't re-enable the click handler for a little while - some devices generate both, and it will
25335 // take a little while to move your hand from the screen to the mouse if you have both modes of input
25336 $timeout(function() {
25337 $elm.on('click', selectCells);
25341 function registerRowSelectionEvents() {
25342 if ($scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection) {
25343 $elm.addClass('ui-grid-disable-selection');
25344 $elm.on('touchstart', touchStart);
25345 $elm.on('touchend', touchEnd);
25346 $elm.on('click', selectCells);
25348 $scope.registered = true;
25352 function deregisterRowSelectionEvents() {
25353 if ($scope.registered){
25354 $elm.removeClass('ui-grid-disable-selection');
25356 $elm.off('touchstart', touchStart);
25357 $elm.off('touchend', touchEnd);
25358 $elm.off('click', selectCells);
25360 $scope.registered = false;
25364 registerRowSelectionEvents();
25365 // register a dataChange callback so that we can change the selection configuration dynamically
25366 // if the user changes the options
25367 var dataChangeDereg = $scope.grid.registerDataChangeCallback( function() {
25368 if ( $scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection &&
25369 !$scope.registered ){
25370 registerRowSelectionEvents();
25371 } else if ( ( !$scope.grid.options.enableRowSelection || !$scope.grid.options.enableFullRowSelection ) &&
25372 $scope.registered ){
25373 deregisterRowSelectionEvents();
25375 }, [uiGridConstants.dataChange.OPTIONS] );
25377 $elm.on( '$destroy', dataChangeDereg);
25382 module.directive('uiGridGridFooter', ['$compile', 'uiGridConstants', 'gridUtil', function ($compile, uiGridConstants, gridUtil) {
25387 require: '^uiGrid',
25389 compile: function ($elm, $attrs) {
25391 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
25393 if (!uiGridCtrl.grid.options.showGridFooter) {
25398 gridUtil.getTemplate('ui-grid/gridFooterSelectedItems')
25399 .then(function (contents) {
25400 var template = angular.element(contents);
25402 var newElm = $compile(template)($scope);
25404 angular.element($elm[0].getElementsByClassName('ui-grid-grid-footer')[0]).append(newElm);
25408 post: function ($scope, $elm, $attrs, controllers) {
25423 * @name ui.grid.treeBase
25426 * # ui.grid.treeBase
25428 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
25430 * This module provides base tree handling functions that are shared by other features, notably grouping
25431 * and treeView. It provides a tree view of the data, with nodes in that
25434 * Design information:
25435 * -------------------
25437 * The raw data that is provided must come with a $$treeLevel on any non-leaf node. Grouping will create
25438 * these on all the group header rows, treeView will expect these to be set in the raw data by the user.
25439 * TreeBase will run a rowsProcessor that:
25440 * - builds `treeBase.tree` out of the provided rows
25441 * - permits a recursive sort of the tree
25442 * - maintains the expand/collapse state of each node
25443 * - provides the expand/collapse all button and the expand/collapse buttons
25444 * - maintains the count of children for each node
25446 * Each row is updated with a link to the tree node that represents it. Refer {@link ui.grid.treeBase.grid:treeBase.tree tree documentation}
25449 * TreeBase adds information to the rows
25450 * - treeLevel: if present and > -1 tells us the level (level 0 is the top level)
25451 * - treeNode: pointer to the node in the grid.treeBase.tree that refers
25452 * to this row, allowing us to manipulate the state
25454 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
25455 * row order or filtering or anything like that is changed. We recall the expanded state
25456 * across invocations of the rowsProcessors by the reference to the treeNode on the individual
25457 * rows. We rebuild the tree itself quite frequently, when we do this we use the saved treeNodes to
25458 * get the state, but we overwrite the other data in that treeNode.
25460 * By default rows are collapsed, which means all data rows have their visible property
25461 * set to false, and only level 0 group rows are set to visible.
25463 * We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into
25464 * grid.treeBase.tree, then call refresh. This is because we can't easily change the visible
25465 * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as
25466 * well use it all the time.
25468 * Tree base provides sorting (on non-grouped columns).
25470 * Sorting works in two passes. The standard sorting is performed for any columns that are important to building
25471 * the tree (for example, any grouped columns). Then after the tree is built, a recursive tree sort is performed
25472 * for the remaining sort columns (including the original sort) - these columns are sorted within each tree level
25473 * (so all the level 1 nodes are sorted, then all the level 2 nodes within each level 1 node etc).
25475 * To achieve this we make use of the `ignoreSort` property on the sort configuration. The parent feature (treeView or grouping)
25476 * must provide a rowsProcessor that runs with very low priority (typically in the 60-65 range), and that sets
25477 * the `ignoreSort`on any sort that it wants to run on the tree. TreeBase will clear the ignoreSort on all sorts - so it
25478 * will turn on any sorts that haven't run. It will then call a recursive sort on the tree.
25480 * Tree base provides treeAggregation. It checks the treeAggregation configuration on each column, and aggregates based on
25481 * the logic provided as it builds the tree. Footer aggregation from the uiGrid core should not be used with treeBase aggregation,
25482 * since it operates on all visible rows, as opposed to to leaf nodes only. Setting `showColumnFooter: true` will show the
25483 * treeAggregations in the column footer. Aggregation information will be collected in the format:
25489 * label: 'count: ',
25490 * rendered: 'count: 4'
25494 * A callback is provided to format the value once it is finalised (aka a valueFilter).
25499 * <div doc-module-components="ui.grid.treeBase"></div>
25502 var module = angular.module('ui.grid.treeBase', ['ui.grid']);
25506 * @name ui.grid.treeBase.constant:uiGridTreeBaseConstants
25508 * @description constants available in treeBase module.
25510 * These constants are manually copied into grouping and treeView,
25511 * as I haven't found a way to simply include them, and it's not worth
25512 * investing time in for something that changes very infrequently.
25515 module.constant('uiGridTreeBaseConstants', {
25516 featureName: "treeBase",
25517 rowHeaderColName: 'treeBaseRowHeaderCol',
25518 EXPANDED: 'expanded',
25519 COLLAPSED: 'collapsed',
25531 * @name ui.grid.treeBase.service:uiGridTreeBaseService
25533 * @description Services for treeBase feature
25537 * @name ui.grid.treeBase.api:ColumnDef
25539 * @description ColumnDef for tree feature, these are available to be
25540 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
25543 module.service('uiGridTreeBaseService', ['$q', 'uiGridTreeBaseConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'rowSorter',
25544 function ($q, uiGridTreeBaseConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants, rowSorter) {
25548 initializeGrid: function (grid, $scope) {
25550 //add feature namespace and any properties to grid for needed
25553 * @name ui.grid.treeBase.grid:treeBase
25555 * @description Grid properties and functions added for treeBase
25557 grid.treeBase = {};
25561 * @propertyOf ui.grid.treeBase.grid:treeBase
25562 * @name numberLevels
25564 * @description Total number of tree levels currently used, calculated by the rowsProcessor by
25565 * retaining the highest tree level it sees
25567 grid.treeBase.numberLevels = 0;
25571 * @propertyOf ui.grid.treeBase.grid:treeBase
25574 * @description Whether or not the expandAll box is selected
25576 grid.treeBase.expandAll = false;
25580 * @propertyOf ui.grid.treeBase.grid:treeBase
25583 * @description Tree represented as a nested array that holds the state of each node, along with a
25584 * pointer to the row. The array order is material - we will display the children in the order
25585 * they are stored in the array
25587 * Each node stores:
25589 * - the state of this node
25590 * - an array of children of this node
25591 * - a pointer to the parent of this node (reverse pointer, allowing us to walk up the tree)
25592 * - the number of children of this node
25593 * - aggregation information calculated from the nodes
25597 * state: 'expanded',
25598 * row: <reference to row>,
25604 * label: 'count: ',
25605 * rendered: 'count: 2'
25609 * state: 'expanded',
25610 * row: <reference to row>,
25611 * parentRow: <reference to row>,
25616 * label: 'count: ',
25617 * rendered: 'count: 4'
25620 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
25621 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
25622 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
25623 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> }
25627 * state: 'collapsed',
25628 * row: <reference to row>,
25629 * parentRow: <reference to row>,
25634 * label: 'count: ',
25635 * rendered: 'count: 3'
25638 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
25639 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
25640 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> }
25644 * }, {<another level 0 node maybe>} ]
25646 * Missing state values are false - meaning they aren't expanded.
25648 * This is used because the rowProcessors run every time the grid is refreshed, so
25649 * we'd lose the expanded state every time the grid was refreshed. This instead gives
25650 * us a reliable lookup that persists across rowProcessors.
25652 * This tree is rebuilt every time we run the rowsProcessors. Since each row holds a pointer
25653 * to it's tree node we can persist expand/collapse state across calls to rowsProcessor, we discard
25654 * all transient information on the tree (children, childCount) and recalculate it
25657 grid.treeBase.tree = [];
25659 service.defaultGridOptions(grid.options);
25661 grid.registerRowsProcessor(service.treeRows, 410);
25663 grid.registerColumnBuilder( service.treeBaseColumnBuilder );
25665 service.createRowHeader( grid );
25669 * @name ui.grid.treeBase.api:PublicApi
25671 * @description Public Api for treeBase feature
25678 * @eventOf ui.grid.treeBase.api:PublicApi
25679 * @name rowExpanded
25680 * @description raised whenever a row is expanded. If you are dynamically
25681 * rendering your tree you can listen to this event, and then retrieve
25682 * the children of this row and load them into the grid data.
25684 * When the data is loaded the grid will automatically refresh to show these new rows
25687 * gridApi.treeBase.on.rowExpanded(scope,function(row){})
25689 * @param {gridRow} row the row that was expanded. You can also
25690 * retrieve the grid from this row with row.grid
25696 * @eventOf ui.grid.treeBase.api:PublicApi
25697 * @name rowCollapsed
25698 * @description raised whenever a row is collapsed. Doesn't really have
25699 * a purpose at the moment, included for symmetry
25702 * gridApi.treeBase.on.rowCollapsed(scope,function(row){})
25704 * @param {gridRow} row the row that was collapsed. You can also
25705 * retrieve the grid from this row with row.grid
25715 * @name expandAllRows
25716 * @methodOf ui.grid.treeBase.api:PublicApi
25717 * @description Expands all tree rows
25719 expandAllRows: function () {
25720 service.expandAllRows(grid);
25725 * @name collapseAllRows
25726 * @methodOf ui.grid.treeBase.api:PublicApi
25727 * @description collapse all tree rows
25729 collapseAllRows: function () {
25730 service.collapseAllRows(grid);
25735 * @name toggleRowTreeState
25736 * @methodOf ui.grid.treeBase.api:PublicApi
25737 * @description call expand if the row is collapsed, collapse if it is expanded
25738 * @param {gridRow} row the row you wish to toggle
25740 toggleRowTreeState: function (row) {
25741 service.toggleRowTreeState(grid, row);
25747 * @methodOf ui.grid.treeBase.api:PublicApi
25748 * @description expand the immediate children of the specified row
25749 * @param {gridRow} row the row you wish to expand
25751 expandRow: function (row) {
25752 service.expandRow(grid, row);
25757 * @name expandRowChildren
25758 * @methodOf ui.grid.treeBase.api:PublicApi
25759 * @description expand all children of the specified row
25760 * @param {gridRow} row the row you wish to expand
25762 expandRowChildren: function (row) {
25763 service.expandRowChildren(grid, row);
25768 * @name collapseRow
25769 * @methodOf ui.grid.treeBase.api:PublicApi
25770 * @description collapse the specified row. When
25771 * you expand the row again, all grandchildren will retain their state
25772 * @param {gridRow} row the row you wish to collapse
25774 collapseRow: function ( row ) {
25775 service.collapseRow(grid, row);
25780 * @name collapseRowChildren
25781 * @methodOf ui.grid.treeBase.api:PublicApi
25782 * @description collapse all children of the specified row. When
25783 * you expand the row again, all grandchildren will be collapsed
25784 * @param {gridRow} row the row you wish to collapse children for
25786 collapseRowChildren: function ( row ) {
25787 service.collapseRowChildren(grid, row);
25792 * @name getTreeState
25793 * @methodOf ui.grid.treeBase.api:PublicApi
25794 * @description Get the tree state for this grid,
25795 * used by the saveState feature
25796 * Returned treeState as an object
25797 * `{ expandedState: { uid: 'expanded', uid: 'collapsed' } }`
25798 * where expandedState is a hash of row uid and the current expanded state
25800 * @returns {object} tree state
25802 * TODO - this needs work - we need an identifier that persists across instantiations,
25803 * not uid. This really means we need a row identity defined, but that won't work for
25804 * grouping. Perhaps this needs to be moved up to treeView and grouping, rather than
25807 getTreeExpandedState: function () {
25808 return { expandedState: service.getTreeState(grid) };
25813 * @name setTreeState
25814 * @methodOf ui.grid.treeBase.api:PublicApi
25815 * @description Set the expanded states of the tree
25816 * @param {object} config the config you want to apply, in the format
25817 * provided by getTreeState
25819 setTreeState: function ( config ) {
25820 service.setTreeState( grid, config );
25825 * @name getRowChildren
25826 * @methodOf ui.grid.treeBase.api:PublicApi
25827 * @description Get the children of the specified row
25828 * @param {GridRow} row the row you want the children of
25829 * @returns {Array} array of children of this row, the children
25832 getRowChildren: function ( row ){
25833 return row.treeNode.children.map( function( childNode ){
25834 return childNode.row;
25841 grid.api.registerEventsFromObject(publicApi.events);
25843 grid.api.registerMethodsFromObject(publicApi.methods);
25847 defaultGridOptions: function (gridOptions) {
25848 //default option to true unless it was explicitly set to false
25851 * @name ui.grid.treeBase.api:GridOptions
25853 * @description GridOptions for treeBase feature, these are available to be
25854 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
25859 * @name treeRowHeaderBaseWidth
25860 * @propertyOf ui.grid.treeBase.api:GridOptions
25861 * @description Base width of the tree header, provides for a single level of tree. This
25862 * is incremented by `treeIndent` for each extra level
25863 * <br/>Defaults to 30
25865 gridOptions.treeRowHeaderBaseWidth = gridOptions.treeRowHeaderBaseWidth || 30;
25870 * @propertyOf ui.grid.treeBase.api:GridOptions
25871 * @description Number of pixels of indent for the icon at each tree level, wider indents are visually more pleasing,
25872 * but will make the tree row header wider
25873 * <br/>Defaults to 10
25875 gridOptions.treeIndent = gridOptions.treeIndent || 10;
25879 * @name showTreeRowHeader
25880 * @propertyOf ui.grid.treeBase.api:GridOptions
25881 * @description If set to false, don't create the row header. Youll need to programatically control the expand
25883 * <br/>Defaults to true
25885 gridOptions.showTreeRowHeader = gridOptions.showTreeRowHeader !== false;
25889 * @name showTreeExpandNoChildren
25890 * @propertyOf ui.grid.treeBase.api:GridOptions
25891 * @description If set to true, show the expand/collapse button even if there are no
25892 * children of a node. You'd use this if you're planning to dynamically load the children
25894 * <br/>Defaults to true, grouping overrides to false
25896 gridOptions.showTreeExpandNoChildren = gridOptions.showTreeExpandNoChildren !== false;
25900 * @name treeRowHeaderAlwaysVisible
25901 * @propertyOf ui.grid.treeBase.api:GridOptions
25902 * @description If set to true, row header even if there are no tree nodes
25904 * <br/>Defaults to true
25906 gridOptions.treeRowHeaderAlwaysVisible = gridOptions.treeRowHeaderAlwaysVisible !== false;
25910 * @name treeCustomAggregations
25911 * @propertyOf ui.grid.treeBase.api:GridOptions
25912 * @description Define custom aggregation functions. The properties of this object will be
25913 * aggregation types available for use on columnDef with {@link ui.grid.treeBase.api:ColumnDef treeAggregationType} or through the column menu.
25914 * If a function defined here uses the same name as one of the native aggregations, this one will take precedence.
25915 * The object format is:
25919 * aggregationName: {
25920 * label: (optional) string,
25921 * aggregationFn: function( aggregation, fieldValue, numValue, row ){...},
25922 * finalizerFn: (optional) function( aggregation ){...}
25926 * aggregationFn: function( aggregation, fieldValue, numValue ){
25927 * aggregation.count = (aggregation.count || 1) + 1;
25928 * aggregation.sum = (aggregation.sum || 0) + numValue;
25930 * finalizerFn: function( aggregation ){
25931 * aggregation.value = aggregation.sum / aggregation.count
25937 * <br/>The `finalizerFn` may be used to manipulate the value before rendering, or to
25938 * apply a custom rendered value. If `aggregation.rendered` is left undefined, the value will be
25939 * rendered. Note that the native aggregation functions use an `finalizerFn` to concatenate
25940 * the label and the value.
25942 * <br/>Defaults to {}
25944 gridOptions.treeCustomAggregations = gridOptions.treeCustomAggregations || {};
25948 * @name enableExpandAll
25949 * @propertyOf ui.grid.treeBase.api:GridOptions
25950 * @description Enable the expand all button at the top of the row header
25952 * <br/>Defaults to true
25954 gridOptions.enableExpandAll = gridOptions.enableExpandAll !== false;
25960 * @name treeBaseColumnBuilder
25961 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25962 * @description Sets the tree defaults based on the columnDefs
25964 * @param {object} colDef columnDef we're basing on
25965 * @param {GridCol} col the column we're to update
25966 * @param {object} gridOptions the options we should use
25967 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
25969 treeBaseColumnBuilder: function (colDef, col, gridOptions) {
25974 * @name customTreeAggregationFn
25975 * @propertyOf ui.grid.treeBase.api:ColumnDef
25976 * @description A custom function that aggregates rows into some form of
25977 * total. Aggregations run row-by-row, the function needs to be capable of
25978 * creating a running total.
25980 * The function will be provided the aggregation item (in which you can store running
25981 * totals), the row value that is to be aggregated, and that same row value converted to
25982 * a number (most aggregations work on numbers)
25985 * customTreeAggregationFn = function ( aggregation, fieldValue, numValue, row ){
25986 * // calculates the average of the squares of the values
25987 * if ( typeof(aggregation.count) === 'undefined' ){
25988 * aggregation.count = 0;
25990 * aggregation.count++;
25992 * if ( !isNaN(numValue) ){
25993 * if ( typeof(aggregation.total) === 'undefined' ){
25994 * aggregation.total = 0;
25996 * aggregation.total = aggregation.total + numValue * numValue;
25999 * aggregation.value = aggregation.total / aggregation.count;
26002 * <br/>Defaults to undefined. May be overwritten by treeAggregationType, the two options should not be used together.
26004 if ( typeof(colDef.customTreeAggregationFn) !== 'undefined' ){
26005 col.treeAggregationFn = colDef.customTreeAggregationFn;
26010 * @name treeAggregationType
26011 * @propertyOf ui.grid.treeBase.api:ColumnDef
26012 * @description Use one of the native or grid-level aggregation methods for calculating aggregations on this column.
26013 * Native method are in the constants file and include: SUM, COUNT, MIN, MAX, AVG. This may also be the property the
26014 * name of an aggregation function defined with {@link ui.grid.treeBase.api:GridOptions treeCustomAggregations}.
26017 * treeAggregationType = uiGridTreeBaseConstants.aggregation.SUM,
26021 * If you are using aggregations you should either:
26023 * - also use grouping, in which case the aggregations are displayed in the group header, OR
26024 * - use treeView, in which case you can set `treeAggregationUpdateEntity: true` in the colDef, and
26025 * treeBase will store the aggregation information in the entity, or you can set `treeAggregationUpdateEntity: false`
26026 * in the colDef, and you need to manual retrieve the calculated aggregations from the row.treeNode.aggregations
26028 * <br/>Takes precendence over a treeAggregationFn, the two options should not be used together.
26029 * <br/>Defaults to undefined.
26031 if ( typeof(colDef.treeAggregationType) !== 'undefined' ){
26032 col.treeAggregation = { type: colDef.treeAggregationType };
26033 if ( typeof(gridOptions.treeCustomAggregations[colDef.treeAggregationType]) !== 'undefined' ){
26034 col.treeAggregationFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].aggregationFn;
26035 col.treeAggregationFinalizerFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].finalizerFn;
26036 col.treeAggregation.label = gridOptions.treeCustomAggregations[colDef.treeAggregationType].label;
26037 } else if ( typeof(service.nativeAggregations()[colDef.treeAggregationType]) !== 'undefined' ){
26038 col.treeAggregationFn = service.nativeAggregations()[colDef.treeAggregationType].aggregationFn;
26039 col.treeAggregation.label = service.nativeAggregations()[colDef.treeAggregationType].label;
26045 * @name treeAggregationLabel
26046 * @propertyOf ui.grid.treeBase.api:ColumnDef
26047 * @description A custom label to use for this aggregation. If provided we don't use native i18n.
26049 if ( typeof(colDef.treeAggregationLabel) !== 'undefined' ){
26050 if (typeof(col.treeAggregation) === 'undefined' ){
26051 col.treeAggregation = {};
26053 col.treeAggregation.label = colDef.treeAggregationLabel;
26058 * @name treeAggregationUpdateEntity
26059 * @propertyOf ui.grid.treeBase.api:ColumnDef
26060 * @description Store calculated aggregations into the entity, allowing them
26061 * to be displayed in the grid using a standard cellTemplate. This defaults to true,
26062 * if you are using grouping then you shouldn't set it to false, as then the aggregations won't
26065 * If you are using treeView in most cases you'll want to set this to true. This will result in
26066 * getCellValue returning the aggregation rather than whatever was stored in the cell attribute on
26067 * the entity. If you want to render the underlying entity value (and do something else with the aggregation)
26068 * then you could use a custom cellTemplate to display `row.entity.myAttribute`, rather than using getCellValue.
26070 * <br/>Defaults to true
26074 * gridOptions.columns = [{
26076 * treeAggregation: { type: uiGridTreeBaseConstants.aggregation.SUM },
26077 * treeAggregationUpdateEntity: true
26078 * cellTemplate: '<div>{{row.entity.myCol + " " + row.treeNode.aggregations[0].rendered}}</div>'
26082 col.treeAggregationUpdateEntity = colDef.treeAggregationUpdateEntity !== false;
26086 * @name customTreeAggregationFinalizerFn
26087 * @propertyOf ui.grid.treeBase.api:ColumnDef
26088 * @description A custom function that populates aggregation.rendered, this is called when
26089 * a particular aggregation has been fully calculated, and we want to render the value.
26091 * With the native aggregation options we just concatenate `aggregation.label` and
26092 * `aggregation.value`, but if you wanted to apply a filter or otherwise manipulate the label
26093 * or the value, you can do so with this function. This function will be called after the
26094 * the default `finalizerFn`.
26098 * customTreeAggregationFinalizerFn = function ( aggregation ){
26099 * aggregation.rendered = aggregation.label + aggregation.value / 100 + '%';
26102 * <br/>Defaults to undefined.
26104 if ( typeof(col.customTreeAggregationFinalizerFn) === 'undefined' ){
26105 col.customTreeAggregationFinalizerFn = colDef.customTreeAggregationFinalizerFn;
26113 * @name createRowHeader
26114 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26115 * @description Create the rowHeader. If treeRowHeaderAlwaysVisible then
26116 * set it to visible, otherwise set it to invisible
26118 * @param {Grid} grid grid object
26120 createRowHeader: function( grid ){
26121 var rowHeaderColumnDef = {
26122 name: uiGridTreeBaseConstants.rowHeaderColName,
26124 width: grid.options.treeRowHeaderBaseWidth,
26126 cellTemplate: 'ui-grid/treeBaseRowHeader',
26127 headerCellTemplate: 'ui-grid/treeBaseHeaderCell',
26128 enableColumnResizing: false,
26129 enableColumnMenu: false,
26130 exporterSuppressExport: true,
26131 allowCellFocus: true
26134 rowHeaderColumnDef.visible = grid.options.treeRowHeaderAlwaysVisible;
26135 grid.addRowHeaderColumn( rowHeaderColumnDef );
26141 * @name expandAllRows
26142 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26143 * @description Expands all nodes in the tree
26145 * @param {Grid} grid grid object
26147 expandAllRows: function (grid) {
26148 grid.treeBase.tree.forEach( function( node ) {
26149 service.setAllNodes( grid, node, uiGridTreeBaseConstants.EXPANDED);
26151 grid.treeBase.expandAll = true;
26152 grid.queueGridRefresh();
26158 * @name collapseAllRows
26159 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26160 * @description Collapses all nodes in the tree
26162 * @param {Grid} grid grid object
26164 collapseAllRows: function (grid) {
26165 grid.treeBase.tree.forEach( function( node ) {
26166 service.setAllNodes( grid, node, uiGridTreeBaseConstants.COLLAPSED);
26168 grid.treeBase.expandAll = false;
26169 grid.queueGridRefresh();
26175 * @name setAllNodes
26176 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26177 * @description Works through a subset of grid.treeBase.rowExpandedStates, setting
26178 * all child nodes (and their descendents) of the provided node to the given state.
26180 * Calls itself recursively on all nodes so as to achieve this.
26182 * @param {Grid} grid the grid we're operating on (so we can raise events)
26183 * @param {object} treeNode a node in the tree that we want to update
26184 * @param {string} targetState the state we want to set it to
26186 setAllNodes: function (grid, treeNode, targetState) {
26187 if ( typeof(treeNode.state) !== 'undefined' && treeNode.state !== targetState ){
26188 treeNode.state = targetState;
26190 if ( targetState === uiGridTreeBaseConstants.EXPANDED ){
26191 grid.api.treeBase.raise.rowExpanded(treeNode.row);
26193 grid.api.treeBase.raise.rowCollapsed(treeNode.row);
26197 // set all child nodes
26198 if ( treeNode.children ){
26199 treeNode.children.forEach(function( childNode ){
26200 service.setAllNodes(grid, childNode, targetState);
26208 * @name toggleRowTreeState
26209 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26210 * @description Toggles the expand or collapse state of this grouped row, if
26211 * it's a parent row
26213 * @param {Grid} grid grid object
26214 * @param {GridRow} row the row we want to toggle
26216 toggleRowTreeState: function ( grid, row ){
26217 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26221 if (row.treeNode.state === uiGridTreeBaseConstants.EXPANDED){
26222 service.collapseRow(grid, row);
26224 service.expandRow(grid, row);
26227 grid.queueGridRefresh();
26234 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26235 * @description Expands this specific row, showing only immediate children.
26237 * @param {Grid} grid grid object
26238 * @param {GridRow} row the row we want to expand
26240 expandRow: function ( grid, row ){
26241 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26245 if ( row.treeNode.state !== uiGridTreeBaseConstants.EXPANDED ){
26246 row.treeNode.state = uiGridTreeBaseConstants.EXPANDED;
26247 grid.api.treeBase.raise.rowExpanded(row);
26248 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
26249 grid.queueGridRefresh();
26256 * @name expandRowChildren
26257 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26258 * @description Expands this specific row, showing all children.
26260 * @param {Grid} grid grid object
26261 * @param {GridRow} row the row we want to expand
26263 expandRowChildren: function ( grid, row ){
26264 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26268 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.EXPANDED);
26269 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
26270 grid.queueGridRefresh();
26276 * @name collapseRow
26277 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26278 * @description Collapses this specific row
26280 * @param {Grid} grid grid object
26281 * @param {GridRow} row the row we want to collapse
26283 collapseRow: function( grid, row ){
26284 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26288 if ( row.treeNode.state !== uiGridTreeBaseConstants.COLLAPSED ){
26289 row.treeNode.state = uiGridTreeBaseConstants.COLLAPSED;
26290 grid.treeBase.expandAll = false;
26291 grid.api.treeBase.raise.rowCollapsed(row);
26292 grid.queueGridRefresh();
26299 * @name collapseRowChildren
26300 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26301 * @description Collapses this specific row and all children
26303 * @param {Grid} grid grid object
26304 * @param {GridRow} row the row we want to collapse
26306 collapseRowChildren: function( grid, row ){
26307 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26311 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.COLLAPSED);
26312 grid.treeBase.expandAll = false;
26313 grid.queueGridRefresh();
26319 * @name allExpanded
26320 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26321 * @description Returns true if all rows are expanded, false
26322 * if they're not. Walks the tree to determine this. Used
26323 * to set the expandAll state.
26325 * If the node has no children, then return true (it's immaterial
26326 * whether it is expanded). If the node has children, then return
26327 * false if this node is collapsed, or if any child node is not all expanded
26329 * @param {object} tree the grid to check
26330 * @returns {boolean} whether or not the tree is all expanded
26332 allExpanded: function( tree ){
26333 var allExpanded = true;
26334 tree.forEach( function( node ){
26335 if ( !service.allExpandedInternal( node ) ){
26336 allExpanded = false;
26339 return allExpanded;
26342 allExpandedInternal: function( treeNode ){
26343 if ( treeNode.children && treeNode.children.length > 0 ){
26344 if ( treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
26347 var allExpanded = true;
26348 treeNode.children.forEach( function( node ){
26349 if ( !service.allExpandedInternal( node ) ){
26350 allExpanded = false;
26353 return allExpanded;
26363 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26364 * @description The rowProcessor that adds the nodes to the tree, and sets the visible
26365 * state of each row based on it's parent state
26367 * Assumes it is always called after the sorting processor, and the grouping processor if there is one.
26368 * Performs any tree sorts itself after having built the tree
26370 * Processes all the rows in order, setting the group level based on the $$treeLevel in the associated
26371 * entity, and setting the visible state based on the parent's state.
26373 * Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly
26376 * Aggregates if necessary along the way.
26378 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
26379 * @returns {array} the updated rows
26381 treeRows: function( renderableRows ) {
26382 if (renderableRows.length === 0){
26383 return renderableRows;
26387 var currentLevel = 0;
26388 var currentState = uiGridTreeBaseConstants.EXPANDED;
26391 grid.treeBase.tree = service.createTree( grid, renderableRows );
26392 service.updateRowHeaderWidth( grid );
26394 service.sortTree( grid );
26395 service.fixFilter( grid );
26397 return service.renderTree( grid.treeBase.tree );
26403 * @name createOrUpdateRowHeaderWidth
26404 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26405 * @description Calculates the rowHeader width.
26407 * If rowHeader is always present, updates the width.
26409 * If rowHeader is only sometimes present (`treeRowHeaderAlwaysVisible: false`), determines whether there
26410 * should be one, then creates or removes it as appropriate, with the created rowHeader having the
26413 * If there's never a rowHeader then never creates one: `showTreeRowHeader: false`
26415 * @param {Grid} grid the grid we want to set the row header on
26417 updateRowHeaderWidth: function( grid ){
26418 var rowHeader = grid.getColumn(uiGridTreeBaseConstants.rowHeaderColName);
26420 var newWidth = grid.options.treeRowHeaderBaseWidth + grid.options.treeIndent * Math.max(grid.treeBase.numberLevels - 1, 0);
26421 if ( rowHeader && newWidth !== rowHeader.width ){
26422 rowHeader.width = newWidth;
26423 grid.queueRefresh();
26426 var newVisibility = true;
26427 if ( grid.options.showTreeRowHeader === false ){
26428 newVisibility = false;
26430 if ( grid.options.treeRowHeaderAlwaysVisible === false && grid.treeBase.numberLevels <= 0 ){
26431 newVisibility = false;
26433 if ( rowHeader.visible !== newVisibility ) {
26434 rowHeader.visible = newVisibility;
26435 rowHeader.colDef.visible = newVisibility;
26436 grid.queueGridRefresh();
26444 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26445 * @description Creates an array of rows based on the tree, exporting only
26446 * the visible nodes and leaves
26448 * @param {array} nodeList the list of nodes - can be grid.treeBase.tree, or can be node.children when
26449 * we're calling recursively
26450 * @returns {array} renderable rows
26452 renderTree: function( nodeList ){
26453 var renderableRows = [];
26455 nodeList.forEach( function ( node ){
26456 if ( node.row.visible ){
26457 renderableRows.push( node.row );
26459 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
26460 renderableRows = renderableRows.concat( service.renderTree( node.children ) );
26463 return renderableRows;
26470 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26471 * @description Creates a tree from the renderableRows
26473 * @param {Grid} grid the grid
26474 * @param {array} renderableRows the rows we want to create a tree from
26475 * @returns {object} the tree we've build
26477 createTree: function( grid, renderableRows ) {
26478 var currentLevel = -1;
26481 grid.treeBase.tree = [];
26482 grid.treeBase.numberLevels = 0;
26483 var aggregations = service.getAggregations( grid );
26485 var createNode = function( row ){
26486 if ( typeof(row.entity.$$treeLevel) !== 'undefined' && row.treeLevel !== row.entity.$$treeLevel ){
26487 row.treeLevel = row.entity.$$treeLevel;
26490 if ( row.treeLevel <= currentLevel ){
26491 // pop any levels that aren't parents of this level, formatting the aggregation at the same time
26492 while ( row.treeLevel <= currentLevel ){
26493 var lastParent = parents.pop();
26494 service.finaliseAggregations( lastParent );
26498 // reset our current state based on the new parent, set to expanded if this is a level 0 node
26499 if ( parents.length > 0 ){
26500 currentState = service.setCurrentState(parents);
26502 currentState = uiGridTreeBaseConstants.EXPANDED;
26506 // aggregate if this is a leaf node
26507 if ( ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) && row.visible ){
26508 service.aggregate( grid, row, parents );
26511 // add this node to the tree
26512 service.addOrUseNode(grid, row, parents, aggregations);
26514 if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel !== null && row.treeLevel >= 0 ){
26517 currentState = service.setCurrentState(parents);
26520 // update the tree number of levels, so we can set header width if we need to
26521 if ( grid.treeBase.numberLevels < row.treeLevel + 1){
26522 grid.treeBase.numberLevels = row.treeLevel + 1;
26526 renderableRows.forEach( createNode );
26528 // finalise remaining aggregations
26529 while ( parents.length > 0 ){
26530 var lastParent = parents.pop();
26531 service.finaliseAggregations( lastParent );
26534 return grid.treeBase.tree;
26540 * @name addOrUseNode
26541 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26542 * @description Creates a tree node for this row. If this row already has a treeNode
26543 * recorded against it, preserves the state, but otherwise overwrites the data.
26545 * @param {grid} grid the grid we're operating on
26546 * @param {gridRow} row the row we want to set
26547 * @param {array} parents an array of the parents this row should have
26548 * @param {array} aggregationBase empty aggregation information
26549 * @returns {undefined} updates the parents array, updates the row to have a treeNode, and updates the
26550 * grid.treeBase.tree
26552 addOrUseNode: function( grid, row, parents, aggregationBase ){
26553 var newAggregations = [];
26554 aggregationBase.forEach( function(aggregation){
26555 newAggregations.push(service.buildAggregationObject(aggregation.col));
26558 var newNode = { state: uiGridTreeBaseConstants.COLLAPSED, row: row, parentRow: null, aggregations: newAggregations, children: [] };
26559 if ( row.treeNode ){
26560 newNode.state = row.treeNode.state;
26562 if ( parents.length > 0 ){
26563 newNode.parentRow = parents[parents.length - 1];
26565 row.treeNode = newNode;
26567 if ( parents.length === 0 ){
26568 grid.treeBase.tree.push( newNode );
26570 parents[parents.length - 1].treeNode.children.push( newNode );
26577 * @name setCurrentState
26578 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26579 * @description Looks at the parents array to determine our current state.
26580 * If any node in the hierarchy is collapsed, then return collapsed, otherwise return
26583 * @param {array} parents an array of the parents this row should have
26584 * @returns {string} the state we should be setting to any nodes we see
26586 setCurrentState: function( parents ){
26587 var currentState = uiGridTreeBaseConstants.EXPANDED;
26588 parents.forEach( function(parent){
26589 if ( parent.treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
26590 currentState = uiGridTreeBaseConstants.COLLAPSED;
26593 return currentState;
26600 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26601 * @description Performs a recursive sort on the tree nodes, sorting the
26602 * children of each node and putting them back into the children array.
26604 * Before doing this it turns back on all the sortIgnore - things that were previously
26605 * ignored we process now. Since we're sorting within the nodes, presumably anything
26606 * that was already sorted is how we derived the nodes, we can keep those sorts too.
26608 * We only sort tree nodes that are expanded - no point in wasting effort sorting collapsed
26611 * @param {Grid} grid the grid to get the aggregation information from
26612 * @returns {array} the aggregation information
26614 sortTree: function( grid ){
26615 grid.columns.forEach( function( column ) {
26616 if ( column.sort && column.sort.ignoreSort ){
26617 delete column.sort.ignoreSort;
26621 grid.treeBase.tree = service.sortInternal( grid, grid.treeBase.tree );
26624 sortInternal: function( grid, treeList ){
26625 var rows = treeList.map( function( node ){
26629 rows = rowSorter.sort( grid, rows, grid.columns );
26631 var treeNodes = rows.map( function( row ){
26632 return row.treeNode;
26635 treeNodes.forEach( function( node ){
26636 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
26637 node.children = service.sortInternal( grid, node.children );
26647 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26648 * @description After filtering has run, we need to go back through the tree
26649 * and make sure the parent rows are always visible if any of the child rows
26650 * are visible (filtering may make a child visible, but the parent may not
26651 * match the filter criteria)
26653 * This has a risk of being computationally expensive, we do it by walking
26654 * the tree and remembering whether there are any invisible nodes on the
26657 * @param {Grid} grid the grid to fix filters on
26659 fixFilter: function( grid ){
26660 var parentsVisible;
26662 grid.treeBase.tree.forEach( function( node ){
26663 if ( node.children && node.children.length > 0 ){
26664 parentsVisible = node.row.visible;
26665 service.fixFilterInternal( node.children, parentsVisible );
26670 fixFilterInternal: function( nodes, parentsVisible) {
26671 nodes.forEach( function( node ){
26672 if ( node.row.visible && !parentsVisible ){
26673 service.setParentsVisible( node );
26674 parentsVisible = true;
26677 if ( node.children && node.children.length > 0 ){
26678 if ( service.fixFilterInternal( node.children, ( parentsVisible && node.row.visible ) ) ) {
26679 parentsVisible = true;
26684 return parentsVisible;
26687 setParentsVisible: function( node ){
26688 while ( node.parentRow ){
26689 node.parentRow.visible = true;
26690 node = node.parentRow.treeNode;
26696 * @name buildAggregationObject
26697 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26698 * @description Build the object which is stored on the column for holding meta-data about the aggregation.
26699 * This method should only be called with columns which have an aggregation.
26701 * @param {Column} the column which this object relates to
26702 * @returns {object} {col: Column object, label: string, type: string (optional)}
26704 buildAggregationObject: function( column ){
26705 var newAggregation = { col: column };
26707 if ( column.treeAggregation && column.treeAggregation.type ){
26708 newAggregation.type = column.treeAggregation.type;
26711 if ( column.treeAggregation && column.treeAggregation.label ){
26712 newAggregation.label = column.treeAggregation.label;
26715 return newAggregation;
26720 * @name getAggregations
26721 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26722 * @description Looks through the grid columns to find those with aggregations,
26723 * and collates the aggregation information into an array, returns that array
26725 * @param {Grid} grid the grid to get the aggregation information from
26726 * @returns {array} the aggregation information
26728 getAggregations: function( grid ){
26729 var aggregateArray = [];
26731 grid.columns.forEach( function(column){
26732 if ( typeof(column.treeAggregationFn) !== 'undefined' ){
26733 aggregateArray.push( service.buildAggregationObject(column) );
26735 if ( grid.options.showColumnFooter && typeof(column.colDef.aggregationType) === 'undefined' && column.treeAggregation ){
26736 // Add aggregation object for footer
26737 column.treeFooterAggregation = service.buildAggregationObject(column);
26738 column.aggregationType = service.treeFooterAggregationType;
26742 return aggregateArray;
26749 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26750 * @description Accumulate the data from this row onto the aggregations for each parent
26752 * Iterate over the parents, then iterate over the aggregations for each of those parents,
26753 * and perform the aggregation for each individual aggregation
26755 * @param {Grid} grid grid object
26756 * @param {GridRow} row the row we want to set grouping visibility on
26757 * @param {array} parents the parents that we would want to aggregate onto
26759 aggregate: function( grid, row, parents ){
26760 if ( parents.length === 0 && row.treeNode && row.treeNode.aggregations ){
26761 row.treeNode.aggregations.forEach(function(aggregation){
26762 // Calculate aggregations for footer even if there are no grouped rows
26763 if ( typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ) {
26764 var fieldValue = grid.getCellValue(row, aggregation.col);
26765 var numValue = Number(fieldValue);
26766 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26771 parents.forEach( function( parent, index ){
26772 if ( parent.treeNode.aggregations ){
26773 parent.treeNode.aggregations.forEach( function( aggregation ){
26774 var fieldValue = grid.getCellValue(row, aggregation.col);
26775 var numValue = Number(fieldValue);
26776 aggregation.col.treeAggregationFn(aggregation, fieldValue, numValue, row);
26778 if ( index === 0 && typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ){
26779 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26787 // Aggregation routines - no doco needed as self evident
26788 nativeAggregations: function() {
26789 var nativeAggregations = {
26791 label: i18nService.get().aggregation.count,
26792 menuTitle: i18nService.get().grouping.aggregate_count,
26793 aggregationFn: function (aggregation, fieldValue, numValue) {
26794 if (typeof(aggregation.value) === 'undefined') {
26795 aggregation.value = 1;
26797 aggregation.value++;
26803 label: i18nService.get().aggregation.sum,
26804 menuTitle: i18nService.get().grouping.aggregate_sum,
26805 aggregationFn: function( aggregation, fieldValue, numValue ) {
26806 if (!isNaN(numValue)) {
26807 if (typeof(aggregation.value) === 'undefined') {
26808 aggregation.value = numValue;
26810 aggregation.value += numValue;
26817 label: i18nService.get().aggregation.min,
26818 menuTitle: i18nService.get().grouping.aggregate_min,
26819 aggregationFn: function( aggregation, fieldValue, numValue ) {
26820 if (typeof(aggregation.value) === 'undefined') {
26821 aggregation.value = fieldValue;
26823 if (typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)) {
26824 aggregation.value = fieldValue;
26831 label: i18nService.get().aggregation.max,
26832 menuTitle: i18nService.get().grouping.aggregate_max,
26833 aggregationFn: function( aggregation, fieldValue, numValue ){
26834 if ( typeof(aggregation.value) === 'undefined' ){
26835 aggregation.value = fieldValue;
26837 if ( typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue > aggregation.value || aggregation.value === null)){
26838 aggregation.value = fieldValue;
26845 label: i18nService.get().aggregation.avg,
26846 menuTitle: i18nService.get().grouping.aggregate_avg,
26847 aggregationFn: function( aggregation, fieldValue, numValue ){
26848 if ( typeof(aggregation.count) === 'undefined' ){
26849 aggregation.count = 1;
26851 aggregation.count++;
26854 if ( isNaN(numValue) ){
26858 if ( typeof(aggregation.value) === 'undefined' || typeof(aggregation.sum) === 'undefined' ){
26859 aggregation.value = numValue;
26860 aggregation.sum = numValue;
26862 aggregation.sum += numValue;
26863 aggregation.value = aggregation.sum / aggregation.count;
26868 return nativeAggregations;
26873 * @name finaliseAggregation
26874 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26875 * @description Helper function used to finalize aggregation nodes and footer cells
26877 * @param {gridRow} row the parent we're finalising
26878 * @param {aggregation} the aggregation object manipulated by the aggregationFn
26880 finaliseAggregation: function(row, aggregation){
26881 if ( aggregation.col.treeAggregationUpdateEntity && typeof(row) !== 'undefined' && typeof(row.entity[ '$$' + aggregation.col.uid ]) !== 'undefined' ){
26882 angular.extend( aggregation, row.entity[ '$$' + aggregation.col.uid ]);
26885 if ( typeof(aggregation.col.treeAggregationFinalizerFn) === 'function' ){
26886 aggregation.col.treeAggregationFinalizerFn( aggregation );
26888 if ( typeof(aggregation.col.customTreeAggregationFinalizerFn) === 'function' ){
26889 aggregation.col.customTreeAggregationFinalizerFn( aggregation );
26891 if ( typeof(aggregation.rendered) === 'undefined' ){
26892 aggregation.rendered = aggregation.label ? aggregation.label + aggregation.value : aggregation.value;
26898 * @name finaliseAggregations
26899 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26900 * @description Format the data from the aggregation into the rendered text
26901 * e.g. if we had label: 'sum: ' and value: 25, we'd create 'sum: 25'.
26903 * As part of this we call any formatting callback routines we've been provided.
26905 * We write our aggregation out to the row.entity if treeAggregationUpdateEntity is
26906 * set on the column - we don't overwrite any information that's already there, we append
26907 * to it so that grouping can have set the groupVal beforehand without us overwriting it.
26909 * We need to copy the data from the row.entity first before we finalise the aggregation,
26910 * we need that information for the finaliserFn
26912 * @param {gridRow} row the parent we're finalising
26914 finaliseAggregations: function( row ){
26915 if ( typeof(row.treeNode.aggregations) === 'undefined' ){
26919 row.treeNode.aggregations.forEach( function( aggregation ) {
26920 service.finaliseAggregation(row, aggregation);
26922 if ( aggregation.col.treeAggregationUpdateEntity ){
26923 var aggregationCopy = {};
26924 angular.forEach( aggregation, function( value, key ){
26925 if ( aggregation.hasOwnProperty(key) && key !== 'col' ){
26926 aggregationCopy[key] = value;
26930 row.entity[ '$$' + aggregation.col.uid ] = aggregationCopy;
26937 * @name treeFooterAggregationType
26938 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26939 * @description Uses the tree aggregation functions and finalizers to set the
26940 * column footer aggregations.
26942 * @param {rows} visible rows. not used, but accepted to match signature of GridColumn.aggregationType
26943 * @param {gridColumn} the column we are finalizing
26945 treeFooterAggregationType: function( rows, column ) {
26946 service.finaliseAggregation(undefined, column.treeFooterAggregation);
26947 if ( typeof(column.treeFooterAggregation.value) === 'undefined' || column.treeFooterAggregation.rendered === null ){
26948 // The was apparently no aggregation performed (perhaps this is a grouped column
26951 return column.treeFooterAggregation.rendered;
26962 * @name ui.grid.treeBase.directive:uiGridTreeRowHeaderButtons
26965 * @description Provides the expand/collapse button on rows
26967 module.directive('uiGridTreeBaseRowHeaderButtons', ['$templateCache', 'uiGridTreeBaseService',
26968 function ($templateCache, uiGridTreeBaseService) {
26972 template: $templateCache.get('ui-grid/treeBaseRowHeaderButtons'),
26974 require: '^uiGrid',
26975 link: function($scope, $elm, $attrs, uiGridCtrl) {
26976 var self = uiGridCtrl.grid;
26977 $scope.treeButtonClick = function(row, evt) {
26978 uiGridTreeBaseService.toggleRowTreeState(self, row, evt);
26987 * @name ui.grid.treeBase.directive:uiGridTreeBaseExpandAllButtons
26990 * @description Provides the expand/collapse all button
26992 module.directive('uiGridTreeBaseExpandAllButtons', ['$templateCache', 'uiGridTreeBaseService',
26993 function ($templateCache, uiGridTreeBaseService) {
26997 template: $templateCache.get('ui-grid/treeBaseExpandAllButtons'),
26999 link: function($scope, $elm, $attrs, uiGridCtrl) {
27000 var self = $scope.col.grid;
27002 $scope.headerButtonClick = function(row, evt) {
27003 if ( self.treeBase.expandAll ){
27004 uiGridTreeBaseService.collapseAllRows(self, evt);
27006 uiGridTreeBaseService.expandAllRows(self, evt);
27016 * @name ui.grid.treeBase.directive:uiGridViewport
27019 * @description Stacks on top of ui.grid.uiGridViewport to set formatting on a tree header row
27021 module.directive('uiGridViewport',
27022 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
27023 function ($compile, uiGridConstants, gridUtil, $parse) {
27025 priority: -200, // run after default directive
27027 compile: function ($elm, $attrs) {
27028 var rowRepeatDiv = angular.element($elm.children().children()[0]);
27030 var existingNgClass = rowRepeatDiv.attr("ng-class");
27031 var newNgClass = '';
27032 if ( existingNgClass ) {
27033 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-tree-header-row': row.treeLevel > -1}";
27035 newNgClass = "{'ui-grid-tree-header-row': row.treeLevel > -1}";
27037 rowRepeatDiv.attr("ng-class", newNgClass);
27040 pre: function ($scope, $elm, $attrs, controllers) {
27043 post: function ($scope, $elm, $attrs, controllers) {
27056 * @name ui.grid.treeView
27059 * # ui.grid.treeView
27061 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
27063 * This module provides a tree view of the data that it is provided, with nodes in that
27064 * tree and leaves. Unlike grouping, the tree is an inherent property of the data and must
27065 * be provided with your data array.
27067 * Design information:
27068 * -------------------
27070 * TreeView uses treeBase for the underlying functionality, and is a very thin wrapper around
27071 * that logic. Most of the design information has now moved to treebase.
27075 * <div doc-module-components="ui.grid.treeView"></div>
27078 var module = angular.module('ui.grid.treeView', ['ui.grid', 'ui.grid.treeBase']);
27082 * @name ui.grid.treeView.constant:uiGridTreeViewConstants
27084 * @description constants available in treeView module, this includes
27085 * all the constants declared in the treeBase module (these are manually copied
27086 * as there isn't an easy way to include constants in another constants file, and
27087 * we don't want to make users include treeBase)
27090 module.constant('uiGridTreeViewConstants', {
27091 featureName: "treeView",
27092 rowHeaderColName: 'treeBaseRowHeaderCol',
27093 EXPANDED: 'expanded',
27094 COLLAPSED: 'collapsed',
27106 * @name ui.grid.treeView.service:uiGridTreeViewService
27108 * @description Services for treeView features
27110 module.service('uiGridTreeViewService', ['$q', 'uiGridTreeViewConstants', 'uiGridTreeBaseConstants', 'uiGridTreeBaseService', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants',
27111 function ($q, uiGridTreeViewConstants, uiGridTreeBaseConstants, uiGridTreeBaseService, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants) {
27115 initializeGrid: function (grid, $scope) {
27116 uiGridTreeBaseService.initializeGrid( grid, $scope );
27120 * @name ui.grid.treeView.grid:treeView
27122 * @description Grid properties and functions added for treeView
27124 grid.treeView = {};
27126 grid.registerRowsProcessor(service.adjustSorting, 60);
27130 * @name ui.grid.treeView.api:PublicApi
27132 * @description Public Api for treeView feature
27145 grid.api.registerEventsFromObject(publicApi.events);
27147 grid.api.registerMethodsFromObject(publicApi.methods);
27151 defaultGridOptions: function (gridOptions) {
27152 //default option to true unless it was explicitly set to false
27155 * @name ui.grid.treeView.api:GridOptions
27157 * @description GridOptions for treeView feature, these are available to be
27158 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
27160 * Many tree options are set on treeBase, make sure to look at that feature in
27161 * conjunction with these options.
27166 * @name enableTreeView
27167 * @propertyOf ui.grid.treeView.api:GridOptions
27168 * @description Enable row tree view for entire grid.
27169 * <br/>Defaults to true
27171 gridOptions.enableTreeView = gridOptions.enableTreeView !== false;
27178 * @name adjustSorting
27179 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
27180 * @description Trees cannot be sorted the same as flat lists of rows -
27181 * trees are sorted recursively within each level - so the children of each
27182 * node are sorted, but not the full set of rows.
27184 * To achieve this, we suppress the normal sorting by setting ignoreSort on
27185 * each of the sort columns. When the treeBase rowsProcessor runs it will then
27186 * unignore these, and will perform a recursive sort against the tree that it builds.
27188 * @param {array} renderableRows the rows that we need to pass on through
27189 * @returns {array} renderableRows that we passed on through
27191 adjustSorting: function( renderableRows ) {
27194 grid.columns.forEach( function( column ){
27195 if ( column.sort ){
27196 column.sort.ignoreSort = true;
27200 return renderableRows;
27211 * @name ui.grid.treeView.directive:uiGridTreeView
27215 * @description Adds treeView features to grid
27218 <example module="app">
27219 <file name="app.js">
27220 var app = angular.module('app', ['ui.grid', 'ui.grid.treeView']);
27222 app.controller('MainCtrl', ['$scope', function ($scope) {
27224 { name: 'Bob', title: 'CEO' },
27225 { name: 'Frank', title: 'Lowly Developer' }
27228 $scope.columnDefs = [
27229 {name: 'name', enableCellEdit: true},
27230 {name: 'title', enableCellEdit: true}
27233 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
27236 <file name="index.html">
27237 <div ng-controller="MainCtrl">
27238 <div ui-grid="gridOptions" ui-grid-tree-view></div>
27243 module.directive('uiGridTreeView', ['uiGridTreeViewConstants', 'uiGridTreeViewService', '$templateCache',
27244 function (uiGridTreeViewConstants, uiGridTreeViewService, $templateCache) {
27248 require: '^uiGrid',
27250 compile: function () {
27252 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
27253 if (uiGridCtrl.grid.options.enableTreeView !== false){
27254 uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope);
27257 post: function ($scope, $elm, $attrs, uiGridCtrl) {
27271 * @name ui.grid.validate
27274 * # ui.grid.validate
27276 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
27278 * This module provides the ability to validate cells upon change.
27280 * Design information:
27281 * -------------------
27283 * Validation is not based on angularjs validation, since it would work only when editing the field.
27285 * Instead it adds custom properties to any field considered as invalid.
27290 * <div doc-module-components="ui.grid.expandable"></div>
27293 var module = angular.module('ui.grid.validate', ['ui.grid']);
27298 * @name ui.grid.validate.service:uiGridValidateService
27300 * @description Services for validation features
27302 module.service('uiGridValidateService', ['$sce', '$q', '$http', 'i18nService', 'uiGridConstants', function ($sce, $q, $http, i18nService, uiGridConstants) {
27308 * @name validatorFactories
27309 * @propertyOf ui.grid.validate.service:uiGridValidateService
27310 * @description object containing all the factories used to validate data.<br/>
27311 * These factories will be in the form <br/>
27314 * validatorFactory: function(argument) {
27315 * return function(newValue, oldValue, rowEntity, colDef) {
27316 * return true || false || promise
27319 * messageFunction: function(argument) {
27325 * Promises should return true or false as result according to the result of validation.
27327 validatorFactories: {},
27332 * @name setExternalFactoryFunction
27333 * @methodOf ui.grid.validate.service:uiGridValidateService
27334 * @description Adds a way to retrieve validators from an external service
27335 * <p>Validators from this external service have a higher priority than default
27337 * @param {function} externalFactoryFunction a function that accepts name and argument to pass to a
27338 * validator factory and that returns an object with the same properties as
27339 * you can see in {@link ui.grid.validate.service:uiGridValidateService#properties_validatorFactories validatorFactories}
27341 setExternalFactoryFunction: function(externalFactoryFunction) {
27342 service.externalFactoryFunction = externalFactoryFunction;
27347 * @name clearExternalFactory
27348 * @methodOf ui.grid.validate.service:uiGridValidateService
27349 * @description Removes any link to external factory from this service
27351 clearExternalFactory: function() {
27352 delete service.externalFactoryFunction;
27357 * @name getValidatorFromExternalFactory
27358 * @methodOf ui.grid.validate.service:uiGridValidateService
27359 * @description Retrieves a validator by executing a validatorFactory
27360 * stored in an external service.
27361 * @param {string} name the name of the validator to retrieve
27362 * @param {object} argument an argument to pass to the validator factory
27364 getValidatorFromExternalFactory: function(name, argument) {
27365 return service.externalFactoryFunction(name, argument).validatorFactory(argument);
27370 * @name getMessageFromExternalFactory
27371 * @methodOf ui.grid.validate.service:uiGridValidateService
27372 * @description Retrieves a message stored in an external service.
27373 * @param {string} name the name of the validator
27374 * @param {object} argument an argument to pass to the message function
27376 getMessageFromExternalFactory: function(name, argument) {
27377 return service.externalFactoryFunction(name, argument).messageFunction(argument);
27382 * @name setValidator
27383 * @methodOf ui.grid.validate.service:uiGridValidateService
27384 * @description Adds a new validator to the service
27385 * @param {string} name the name of the validator, must be unique
27386 * @param {function} validatorFactory a factory that return a validatorFunction
27387 * @param {function} messageFunction a function that return the error message
27389 setValidator: function(name, validatorFactory, messageFunction) {
27390 service.validatorFactories[name] = {
27391 validatorFactory: validatorFactory,
27392 messageFunction: messageFunction
27398 * @name getValidator
27399 * @methodOf ui.grid.validate.service:uiGridValidateService
27400 * @description Returns a validator registered to the service
27401 * or retrieved from the external factory
27402 * @param {string} name the name of the validator to retrieve
27403 * @param {object} argument an argument to pass to the validator factory
27404 * @returns {object} the validator function
27406 getValidator: function(name, argument) {
27407 if (service.externalFactoryFunction) {
27408 var validator = service.getValidatorFromExternalFactory(name, argument);
27413 if (!service.validatorFactories[name]) {
27414 throw ("Invalid validator name: " + name);
27416 return service.validatorFactories[name].validatorFactory(argument);
27422 * @methodOf ui.grid.validate.service:uiGridValidateService
27423 * @description Returns the error message related to the validator
27424 * @param {string} name the name of the validator
27425 * @param {object} argument an argument to pass to the message function
27426 * @returns {string} the error message related to the validator
27428 getMessage: function(name, argument) {
27429 if (service.externalFactoryFunction) {
27430 var message = service.getMessageFromExternalFactory(name, argument);
27435 return service.validatorFactories[name].messageFunction(argument);
27441 * @methodOf ui.grid.validate.service:uiGridValidateService
27442 * @description Returns true if the cell (identified by rowEntity, colDef) is invalid
27443 * @param {object} rowEntity the row entity of the cell
27444 * @param {object} colDef the colDef of the cell
27445 * @returns {boolean} true if the cell is invalid
27447 isInvalid: function (rowEntity, colDef) {
27448 return rowEntity['$$invalid'+colDef.name];
27454 * @methodOf ui.grid.validate.service:uiGridValidateService
27455 * @description Makes the cell invalid by adding the proper field to the entity
27456 * @param {object} rowEntity the row entity of the cell
27457 * @param {object} colDef the colDef of the cell
27459 setInvalid: function (rowEntity, colDef) {
27460 rowEntity['$$invalid'+colDef.name] = true;
27466 * @methodOf ui.grid.validate.service:uiGridValidateService
27467 * @description Makes the cell valid by removing the proper error field from the entity
27468 * @param {object} rowEntity the row entity of the cell
27469 * @param {object} colDef the colDef of the cell
27471 setValid: function (rowEntity, colDef) {
27472 delete rowEntity['$$invalid'+colDef.name];
27478 * @methodOf ui.grid.validate.service:uiGridValidateService
27479 * @description Adds the proper error to the entity errors field
27480 * @param {object} rowEntity the row entity of the cell
27481 * @param {object} colDef the colDef of the cell
27482 * @param {string} validatorName the name of the validator that is failing
27484 setError: function(rowEntity, colDef, validatorName) {
27485 if (!rowEntity['$$errors'+colDef.name]) {
27486 rowEntity['$$errors'+colDef.name] = {};
27488 rowEntity['$$errors'+colDef.name][validatorName] = true;
27494 * @methodOf ui.grid.validate.service:uiGridValidateService
27495 * @description Removes the proper error from the entity errors field
27496 * @param {object} rowEntity the row entity of the cell
27497 * @param {object} colDef the colDef of the cell
27498 * @param {string} validatorName the name of the validator that is failing
27500 clearError: function(rowEntity, colDef, validatorName) {
27501 if (!rowEntity['$$errors'+colDef.name]) {
27504 if (validatorName in rowEntity['$$errors'+colDef.name]) {
27505 delete rowEntity['$$errors'+colDef.name][validatorName];
27511 * @name getErrorMessages
27512 * @methodOf ui.grid.validate.service:uiGridValidateService
27513 * @description returns an array of i18n-ed error messages.
27514 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27515 * @param {object} colDef the column whose errors we are looking for
27516 * @returns {array} An array of strings containing all the error messages for the cell
27518 getErrorMessages: function(rowEntity, colDef) {
27521 if (!rowEntity['$$errors'+colDef.name] || Object.keys(rowEntity['$$errors'+colDef.name]).length === 0) {
27525 Object.keys(rowEntity['$$errors'+colDef.name]).sort().forEach(function(validatorName) {
27526 errors.push(service.getMessage(validatorName, colDef.validators[validatorName]));
27534 * @name getFormattedErrors
27535 * @methodOf ui.grid.validate.service:uiGridValidateService
27536 * @description returns the error i18n-ed and formatted in html to be shown inside the page.
27537 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27538 * @param {object} colDef the column whose errors we are looking for
27539 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27540 * message inside the page (i.e. inside a div)
27542 getFormattedErrors: function(rowEntity, colDef) {
27544 var msgString = "";
27546 var errors = service.getErrorMessages(rowEntity, colDef);
27548 if (!errors.length) {
27552 errors.forEach(function(errorMsg) {
27553 msgString += errorMsg + "<br/>";
27556 return $sce.trustAsHtml('<p><b>' + i18nService.getSafeText('validate.error') + '</b></p>' + msgString );
27561 * @name getTitleFormattedErrors
27562 * @methodOf ui.grid.validate.service:uiGridValidateService
27563 * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html
27565 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27566 * @param {object} colDef the column whose errors we are looking for
27567 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27568 * message inside an html title attribute
27570 getTitleFormattedErrors: function(rowEntity, colDef) {
27572 var newLine = "\n";
27574 var msgString = "";
27576 var errors = service.getErrorMessages(rowEntity, colDef);
27578 if (!errors.length) {
27582 errors.forEach(function(errorMsg) {
27583 msgString += errorMsg + newLine;
27586 return $sce.trustAsHtml(i18nService.getSafeText('validate.error') + newLine + msgString);
27591 * @name getTitleFormattedErrors
27592 * @methodOf ui.grid.validate.service:uiGridValidateService
27593 * @description Executes all validators on a cell (identified by row entity and column definition) and sets or clears errors
27594 * @param {object} rowEntity the row entity of the cell we want to run the validators on
27595 * @param {object} colDef the column definition of the cell we want to run the validators on
27596 * @param {object} newValue the value the user just entered
27597 * @param {object} oldValue the value the field had before
27599 runValidators: function(rowEntity, colDef, newValue, oldValue, grid) {
27601 if (newValue === oldValue) {
27602 // If the value has not changed we perform no validation
27606 if (typeof(colDef.name) === 'undefined' || !colDef.name) {
27607 throw new Error('colDef.name is required to perform validation');
27610 service.setValid(rowEntity, colDef);
27612 var validateClosureFactory = function(rowEntity, colDef, validatorName) {
27613 return function(value) {
27615 service.setInvalid(rowEntity, colDef);
27616 service.setError(rowEntity, colDef, validatorName);
27618 grid.api.validate.raise.validationFailed(rowEntity, colDef, newValue, oldValue);
27624 for (var validatorName in colDef.validators) {
27625 service.clearError(rowEntity, colDef, validatorName);
27627 var validatorFunction = service.getValidator(validatorName, colDef.validators[validatorName]);
27628 // We pass the arguments as oldValue, newValue so they are in the same order
27629 // as ng-model validators (modelValue, viewValue)
27630 $q.when(validatorFunction(oldValue, newValue, rowEntity, colDef))
27631 .then(validateClosureFactory(rowEntity, colDef, validatorName)
27638 * @name createDefaultValidators
27639 * @methodOf ui.grid.validate.service:uiGridValidateService
27640 * @description adds the basic validators to the list of service validators
27642 createDefaultValidators: function() {
27643 service.setValidator('minLength',
27644 function (argument) {
27645 return function (oldValue, newValue, rowEntity, colDef) {
27646 if (newValue === undefined || newValue === null || newValue === '') {
27649 return newValue.length >= argument;
27652 function(argument) {
27653 return i18nService.getSafeText('validate.minLength').replace('THRESHOLD', argument);
27656 service.setValidator('maxLength',
27657 function (argument) {
27658 return function (oldValue, newValue, rowEntity, colDef) {
27659 if (newValue === undefined || newValue === null || newValue === '') {
27662 return newValue.length <= argument;
27665 function(threshold) {
27666 return i18nService.getSafeText('validate.maxLength').replace('THRESHOLD', threshold);
27669 service.setValidator('required',
27670 function (argument) {
27671 return function (oldValue, newValue, rowEntity, colDef) {
27673 return !(newValue === undefined || newValue === null || newValue === '');
27678 function(argument) {
27679 return i18nService.getSafeText('validate.required');
27683 initializeGrid: function (scope, grid) {
27686 isInvalid: service.isInvalid,
27688 getFormattedErrors: service.getFormattedErrors,
27690 getTitleFormattedErrors: service.getTitleFormattedErrors,
27692 runValidators: service.runValidators
27697 * @name ui.grid.validate.api:PublicApi
27699 * @description Public Api for validation feature
27706 * @name validationFailed
27707 * @eventOf ui.grid.validate.api:PublicApi
27708 * @description raised when one or more failure happened during validation
27710 * gridApi.validate.on.validationFailed(scope, function(rowEntity, colDef, newValue, oldValue){...})
27712 * @param {object} rowEntity the options.data element whose validation failed
27713 * @param {object} colDef the column whose validation failed
27714 * @param {object} newValue new value
27715 * @param {object} oldValue old value
27717 validationFailed: function (rowEntity, colDef, newValue, oldValue) {
27726 * @methodOf ui.grid.validate.api:PublicApi
27727 * @description checks if a cell (identified by rowEntity, colDef) is invalid
27728 * @param {object} rowEntity gridOptions.data[] array instance we want to check
27729 * @param {object} colDef the column whose errors we want to check
27730 * @returns {boolean} true if the cell value is not valid
27732 isInvalid: function(rowEntity, colDef) {
27733 return grid.validate.isInvalid(rowEntity, colDef);
27737 * @name getErrorMessages
27738 * @methodOf ui.grid.validate.api:PublicApi
27739 * @description returns an array of i18n-ed error messages.
27740 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27741 * @param {object} colDef the column whose errors we are looking for
27742 * @returns {array} An array of strings containing all the error messages for the cell
27744 getErrorMessages: function (rowEntity, colDef) {
27745 return grid.validate.getErrorMessages(rowEntity, colDef);
27749 * @name getFormattedErrors
27750 * @methodOf ui.grid.validate.api:PublicApi
27751 * @description returns the error i18n-ed and formatted in html to be shown inside the page.
27752 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27753 * @param {object} colDef the column whose errors we are looking for
27754 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27755 * message inside the page (i.e. inside a div)
27757 getFormattedErrors: function (rowEntity, colDef) {
27758 return grid.validate.getFormattedErrors(rowEntity, colDef);
27762 * @name getTitleFormattedErrors
27763 * @methodOf ui.grid.validate.api:PublicApi
27764 * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html
27766 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27767 * @param {object} colDef the column whose errors we are looking for
27768 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27769 * message inside an html title attribute
27771 getTitleFormattedErrors: function (rowEntity, colDef) {
27772 return grid.validate.getTitleFormattedErrors(rowEntity, colDef);
27778 grid.api.registerEventsFromObject(publicApi.events);
27779 grid.api.registerMethodsFromObject(publicApi.methods);
27782 grid.api.edit.on.afterCellEdit(scope, function(rowEntity, colDef, newValue, oldValue) {
27783 grid.validate.runValidators(rowEntity, colDef, newValue, oldValue, grid);
27787 service.createDefaultValidators();
27798 * @name ui.grid.validate.directive:uiGridValidate
27801 * @description Adds validating features to the ui-grid directive.
27803 <example module="app">
27804 <file name="app.js">
27805 var app = angular.module('app', ['ui.grid', 'ui.grid.edit', 'ui.grid.validate']);
27807 app.controller('MainCtrl', ['$scope', function ($scope) {
27809 { name: 'Bob', title: 'CEO' },
27810 { name: 'Frank', title: 'Lowly Developer' }
27813 $scope.columnDefs = [
27814 {name: 'name', enableCellEdit: true, validators: {minLength: 3, maxLength: 9}, cellTemplate: 'ui-grid/cellTitleValidator'},
27815 {name: 'title', enableCellEdit: true, validators: {required: true}, cellTemplate: 'ui-grid/cellTitleValidator'}
27819 <file name="index.html">
27820 <div ng-controller="MainCtrl">
27821 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit ui-grid-validate></div>
27827 module.directive('uiGridValidate', ['gridUtil', 'uiGridValidateService', function (gridUtil, uiGridValidateService) {
27831 require: '^uiGrid',
27833 compile: function () {
27835 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
27836 uiGridValidateService.initializeGrid($scope, uiGridCtrl.grid);
27838 post: function ($scope, $elm, $attrs, uiGridCtrl) {
27845 angular.module('ui.grid').run(['$templateCache', function($templateCache) {
27848 $templateCache.put('ui-grid/ui-grid-filter',
27849 "<div class=\"ui-grid-filter-container\" ng-repeat=\"colFilter in col.filters\" ng-class=\"{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }\"><div ng-if=\"colFilter.type !== 'select'\"><input type=\"text\" class=\"ui-grid-filter-input ui-grid-filter-input-{{$index}}\" ng-model=\"colFilter.term\" ng-attr-placeholder=\"{{colFilter.placeholder || ''}}\" aria-label=\"{{colFilter.ariaLabel || aria.defaultFilterLabel}}\"><div role=\"button\" class=\"ui-grid-filter-button\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term !== null && colFilter.term !== ''\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\"> </i></div></div><div ng-if=\"colFilter.type === 'select'\"><select class=\"ui-grid-filter-select ui-grid-filter-input-{{$index}}\" ng-model=\"colFilter.term\" ng-attr-placeholder=\"{{colFilter.placeholder || aria.defaultFilterLabel}}\" aria-label=\"{{colFilter.ariaLabel || ''}}\" ng-options=\"option.value as option.label for option in colFilter.selectOptions\"><option value=\"\"></option></select><div role=\"button\" class=\"ui-grid-filter-button-select\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term != null\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\"> </i></div></div></div>"
27853 $templateCache.put('ui-grid/ui-grid-footer',
27854 "<div class=\"ui-grid-footer-panel ui-grid-footer-aggregates-row\"><!-- tfooter --><div class=\"ui-grid-footer ui-grid-footer-viewport\"><div class=\"ui-grid-footer-canvas\"><div class=\"ui-grid-footer-cell-wrapper\" ng-style=\"colContainer.headerCellWrapperStyle()\"><div role=\"row\" class=\"ui-grid-footer-cell-row\"><div ui-grid-footer-cell role=\"gridcell\" ng-repeat=\"col in colContainer.renderedColumns track by col.uid\" col=\"col\" render-index=\"$index\" class=\"ui-grid-footer-cell ui-grid-clearfix\"></div></div></div></div></div></div>"
27858 $templateCache.put('ui-grid/ui-grid-grid-footer',
27859 "<div class=\"ui-grid-footer-info ui-grid-grid-footer\"><span>{{'search.totalItems' | t}} {{grid.rows.length}}</span> <span ng-if=\"grid.renderContainers.body.visibleRowCache.length !== grid.rows.length\" class=\"ngLabel\">({{\"search.showingItems\" | t}} {{grid.renderContainers.body.visibleRowCache.length}})</span></div>"
27863 $templateCache.put('ui-grid/ui-grid-group-panel',
27864 "<div class=\"ui-grid-group-panel\"><div ui-t=\"groupPanel.description\" class=\"description\" ng-show=\"groupings.length == 0\"></div><ul ng-show=\"groupings.length > 0\" class=\"ngGroupList\"><li class=\"ngGroupItem\" ng-repeat=\"group in configGroups\"><span class=\"ngGroupElement\"><span class=\"ngGroupName\">{{group.displayName}} <span ng-click=\"removeGroup($index)\" class=\"ngRemoveGroup\">x</span></span> <span ng-hide=\"$last\" class=\"ngGroupArrow\"></span></span></li></ul></div>"
27868 $templateCache.put('ui-grid/ui-grid-header',
27869 "<div role=\"rowgroup\" class=\"ui-grid-header\"><!-- theader --><div class=\"ui-grid-top-panel\"><div class=\"ui-grid-header-viewport\"><div class=\"ui-grid-header-canvas\"><div class=\"ui-grid-header-cell-wrapper\" ng-style=\"colContainer.headerCellWrapperStyle()\"><div role=\"row\" class=\"ui-grid-header-cell-row\"><div class=\"ui-grid-header-cell ui-grid-clearfix\" ng-repeat=\"col in colContainer.renderedColumns track by col.uid\" ui-grid-header-cell col=\"col\" render-index=\"$index\"></div></div></div></div></div></div></div>"
27873 $templateCache.put('ui-grid/ui-grid-menu-button',
27874 "<div class=\"ui-grid-menu-button\"><div role=\"button\" ui-grid-one-bind-id-grid=\"'grid-menu'\" class=\"ui-grid-icon-container\" ng-click=\"toggleMenu()\" aria-haspopup=\"true\"><i class=\"ui-grid-icon-menu\" ui-grid-one-bind-aria-label=\"i18n.aria.buttonLabel\"> </i></div><div ui-grid-menu menu-items=\"menuItems\"></div></div>"
27878 $templateCache.put('ui-grid/ui-grid-no-header',
27879 "<div class=\"ui-grid-top-panel\"></div>"
27883 $templateCache.put('ui-grid/ui-grid-row',
27884 "<div ng-repeat=\"(colRenderIndex, col) in colContainer.renderedColumns track by col.uid\" ui-grid-one-bind-id-grid=\"rowRenderIndex + '-' + col.uid + '-cell'\" class=\"ui-grid-cell\" ng-class=\"{ 'ui-grid-row-header-cell': col.isRowHeader }\" role=\"{{col.isRowHeader ? 'rowheader' : 'gridcell'}}\" ui-grid-cell></div>"
27888 $templateCache.put('ui-grid/ui-grid',
27889 "<div ui-i18n=\"en\" class=\"ui-grid\"><!-- TODO (c0bra): add \"scoped\" attr here, eventually? --><style ui-grid-style>.grid{{ grid.id }} {\n" +
27890 " /* Styles for the grid */\n" +
27893 " .grid{{ grid.id }} .ui-grid-row, .grid{{ grid.id }} .ui-grid-cell, .grid{{ grid.id }} .ui-grid-cell .ui-grid-vertical-bar {\n" +
27894 " height: {{ grid.options.rowHeight }}px;\n" +
27897 " .grid{{ grid.id }} .ui-grid-row:last-child .ui-grid-cell {\n" +
27898 " border-bottom-width: {{ ((grid.getTotalRowHeight() < grid.getViewportHeight()) && '1') || '0' }}px;\n" +
27901 " {{ grid.verticalScrollbarStyles }}\n" +
27902 " {{ grid.horizontalScrollbarStyles }}\n" +
27905 " .ui-grid[dir=rtl] .ui-grid-viewport {\n" +
27906 " padding-left: {{ grid.verticalScrollbarWidth }}px;\n" +
27910 " {{ grid.customStyles }}</style><div class=\"ui-grid-contents-wrapper\"><div ui-grid-menu-button ng-if=\"grid.options.enableGridMenu\"></div><div ng-if=\"grid.hasLeftContainer()\" style=\"width: 0\" ui-grid-pinned-container=\"'left'\"></div><div ui-grid-render-container container-id=\"'body'\" col-container-name=\"'body'\" row-container-name=\"'body'\" bind-scroll-horizontal=\"true\" bind-scroll-vertical=\"true\" enable-horizontal-scrollbar=\"grid.options.enableHorizontalScrollbar\" enable-vertical-scrollbar=\"grid.options.enableVerticalScrollbar\"></div><div ng-if=\"grid.hasRightContainer()\" style=\"width: 0\" ui-grid-pinned-container=\"'right'\"></div><div ui-grid-grid-footer ng-if=\"grid.options.showGridFooter\"></div><div ui-grid-column-menu ng-if=\"grid.options.enableColumnMenus\"></div><div ng-transclude></div></div></div>"
27914 $templateCache.put('ui-grid/uiGridCell',
27915 "<div class=\"ui-grid-cell-contents\" title=\"TOOLTIP\">{{COL_FIELD CUSTOM_FILTERS}}</div>"
27919 $templateCache.put('ui-grid/uiGridColumnMenu',
27920 "<div class=\"ui-grid-column-menu\"><div ui-grid-menu menu-items=\"menuItems\"><!-- <div class=\"ui-grid-column-menu\">\n" +
27921 " <div class=\"inner\" ng-show=\"menuShown\">\n" +
27923 " <div ng-show=\"grid.options.enableSorting\">\n" +
27924 " <li ng-click=\"sortColumn($event, asc)\" ng-class=\"{ 'selected' : col.sort.direction == asc }\"><i class=\"ui-grid-icon-sort-alt-up\"></i> Sort Ascending</li>\n" +
27925 " <li ng-click=\"sortColumn($event, desc)\" ng-class=\"{ 'selected' : col.sort.direction == desc }\"><i class=\"ui-grid-icon-sort-alt-down\"></i> Sort Descending</li>\n" +
27926 " <li ng-show=\"col.sort.direction\" ng-click=\"unsortColumn()\"><i class=\"ui-grid-icon-cancel\"></i> Remove Sort</li>\n" +
27930 " </div> --></div></div>"
27934 $templateCache.put('ui-grid/uiGridFooterCell',
27935 "<div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><div>{{ col.getAggregationText() + ( col.getAggregationValue() CUSTOM_FILTERS ) }}</div></div>"
27939 $templateCache.put('ui-grid/uiGridHeaderCell',
27940 "<div role=\"columnheader\" ng-class=\"{ 'sortable': sortable }\" ui-grid-one-bind-aria-labelledby-grid=\"col.uid + '-header-text ' + col.uid + '-sortdir-text'\" aria-sort=\"{{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending' : (!col.sort.direction ? 'none' : 'other'))}}\"><div role=\"button\" tabindex=\"0\" class=\"ui-grid-cell-contents ui-grid-header-cell-primary-focus\" col-index=\"renderIndex\" title=\"TOOLTIP\"><span class=\"ui-grid-header-cell-label\" ui-grid-one-bind-id-grid=\"col.uid + '-header-text'\">{{ col.displayName CUSTOM_FILTERS }}</span> <span ui-grid-one-bind-id-grid=\"col.uid + '-sortdir-text'\" ui-grid-visible=\"col.sort.direction\" aria-label=\"{{getSortDirectionAriaLabel()}}\"><i ng-class=\"{ 'ui-grid-icon-up-dir': col.sort.direction == asc, 'ui-grid-icon-down-dir': col.sort.direction == desc, 'ui-grid-icon-blank': !col.sort.direction }\" title=\"{{isSortPriorityVisible() ? i18n.headerCell.priority + ' ' + ( col.sort.priority + 1 ) : null}}\" aria-hidden=\"true\"></i> <sub ui-grid-visible=\"isSortPriorityVisible()\" class=\"ui-grid-sort-priority-number\">{{col.sort.priority + 1}}</sub></span></div><div role=\"button\" tabindex=\"0\" ui-grid-one-bind-id-grid=\"col.uid + '-menu-button'\" class=\"ui-grid-column-menu-button\" ng-if=\"grid.options.enableColumnMenus && !col.isRowHeader && col.colDef.enableColumnMenu !== false\" ng-click=\"toggleMenu($event)\" ng-class=\"{'ui-grid-column-menu-button-last-col': isLastCol}\" ui-grid-one-bind-aria-label=\"i18n.headerCell.aria.columnMenuButtonLabel\" aria-haspopup=\"true\"><i class=\"ui-grid-icon-angle-down\" aria-hidden=\"true\"> </i></div><div ui-grid-filter></div></div>"
27944 $templateCache.put('ui-grid/uiGridMenu',
27945 "<div class=\"ui-grid-menu\" ng-if=\"shown\"><style ui-grid-style>{{dynamicStyles}}</style><div class=\"ui-grid-menu-mid\" ng-show=\"shownMid\"><div class=\"ui-grid-menu-inner\"><ul role=\"menu\" class=\"ui-grid-menu-items\"><li ng-repeat=\"item in menuItems\" role=\"menuitem\" ui-grid-menu-item ui-grid-one-bind-id=\"'menuitem-'+$index\" action=\"item.action\" name=\"item.title\" active=\"item.active\" icon=\"item.icon\" shown=\"item.shown\" context=\"item.context\" template-url=\"item.templateUrl\" leave-open=\"item.leaveOpen\" screen-reader-only=\"item.screenReaderOnly\"></li></ul></div></div></div>"
27949 $templateCache.put('ui-grid/uiGridMenuItem',
27950 "<button type=\"button\" class=\"ui-grid-menu-item\" ng-click=\"itemAction($event, title)\" ng-show=\"itemShown()\" ng-class=\"{ 'ui-grid-menu-item-active': active(), 'ui-grid-sr-only': (!focus && screenReaderOnly) }\" aria-pressed=\"{{active()}}\" tabindex=\"0\" ng-focus=\"focus=true\" ng-blur=\"focus=false\"><i ng-class=\"icon\" aria-hidden=\"true\"> </i> {{ name }}</button>"
27954 $templateCache.put('ui-grid/uiGridRenderContainer',
27955 "<div role=\"grid\" ui-grid-one-bind-id-grid=\"'grid-container'\" class=\"ui-grid-render-container\" ng-style=\"{ 'margin-left': colContainer.getMargin('left') + 'px', 'margin-right': colContainer.getMargin('right') + 'px' }\"><!-- All of these dom elements are replaced in place --><div ui-grid-header></div><div ui-grid-viewport></div><div ng-if=\"colContainer.needsHScrollbarPlaceholder()\" class=\"ui-grid-scrollbar-placeholder\" ng-style=\"{height:colContainer.grid.scrollbarHeight + 'px'}\"></div><ui-grid-footer ng-if=\"grid.options.showColumnFooter\"></ui-grid-footer></div>"
27959 $templateCache.put('ui-grid/uiGridViewport',
27960 "<div role=\"rowgroup\" class=\"ui-grid-viewport\" ng-style=\"colContainer.getViewportStyle()\"><!-- tbody --><div class=\"ui-grid-canvas\"><div ng-repeat=\"(rowRenderIndex, row) in rowContainer.renderedRows track by $index\" class=\"ui-grid-row\" ng-style=\"Viewport.rowStyle(rowRenderIndex)\"><div role=\"row\" ui-grid-row=\"row\" row-render-index=\"rowRenderIndex\"></div></div></div></div>"
27964 $templateCache.put('ui-grid/cellEditor',
27965 "<div><form name=\"inputForm\"><input type=\"INPUT_TYPE\" ng-class=\"'colt' + col.uid\" ui-grid-editor ng-model=\"MODEL_COL_FIELD\"></form></div>"
27969 $templateCache.put('ui-grid/dropdownEditor',
27970 "<div><form name=\"inputForm\"><select ng-class=\"'colt' + col.uid\" ui-grid-edit-dropdown ng-model=\"MODEL_COL_FIELD\" ng-options=\"field[editDropdownIdLabel] as field[editDropdownValueLabel] CUSTOM_FILTERS for field in editDropdownOptionsArray\"></select></form></div>"
27974 $templateCache.put('ui-grid/fileChooserEditor',
27975 "<div><form name=\"inputForm\"><input ng-class=\"'colt' + col.uid\" ui-grid-edit-file-chooser type=\"file\" id=\"files\" name=\"files[]\" ng-model=\"MODEL_COL_FIELD\"></form></div>"
27979 $templateCache.put('ui-grid/expandableRow',
27980 "<div ui-grid-expandable-row ng-if=\"expandableRow.shouldRenderExpand()\" class=\"expandableRow\" style=\"float:left; margin-top: 1px; margin-bottom: 1px\" ng-style=\"{width: (grid.renderContainers.body.getCanvasWidth()) + 'px', height: row.expandedRowHeight + 'px'}\"></div>"
27984 $templateCache.put('ui-grid/expandableRowHeader',
27985 "<div class=\"ui-grid-row-header-cell ui-grid-expandable-buttons-cell\"><div class=\"ui-grid-cell-contents\"><i ng-class=\"{ 'ui-grid-icon-plus-squared' : !row.isExpanded, 'ui-grid-icon-minus-squared' : row.isExpanded }\" ng-click=\"grid.api.expandable.toggleRowExpansion(row.entity)\"></i></div></div>"
27989 $templateCache.put('ui-grid/expandableScrollFiller',
27990 "<div ng-if=\"expandableRow.shouldRenderFiller()\" ng-class=\"{scrollFiller:true, scrollFillerClass:(colContainer.name === 'body')}\" ng-style=\"{ width: (grid.getViewportWidth()) + 'px', height: row.expandedRowHeight + 2 + 'px', 'margin-left': grid.options.rowHeader.rowHeaderWidth + 'px' }\"><i class=\"ui-grid-icon-spin5 ui-grid-animate-spin\" ng-style=\"{'margin-top': ( row.expandedRowHeight/2 - 5) + 'px', 'margin-left' : ((grid.getViewportWidth() - grid.options.rowHeader.rowHeaderWidth)/2 - 5) + 'px'}\"></i></div>"
27994 $templateCache.put('ui-grid/expandableTopRowHeader',
27995 "<div class=\"ui-grid-row-header-cell ui-grid-expandable-buttons-cell\"><div class=\"ui-grid-cell-contents\"><i ng-class=\"{ 'ui-grid-icon-plus-squared' : !grid.expandable.expandedAll, 'ui-grid-icon-minus-squared' : grid.expandable.expandedAll }\" ng-click=\"grid.api.expandable.toggleAllRows()\"></i></div></div>"
27999 $templateCache.put('ui-grid/csvLink',
28000 "<span class=\"ui-grid-exporter-csv-link-span\"><a href=\"data:text/csv;charset=UTF-8,CSV_CONTENT\" download=\"FILE_NAME\">LINK_LABEL</a></span>"
28004 $templateCache.put('ui-grid/importerMenuItem',
28005 "<li class=\"ui-grid-menu-item\"><form><input class=\"ui-grid-importer-file-chooser\" type=\"file\" id=\"files\" name=\"files[]\"></form></li>"
28009 $templateCache.put('ui-grid/importerMenuItemContainer',
28010 "<div ui-grid-importer-menu-item></div>"
28014 $templateCache.put('ui-grid/pagination',
28015 "<div role=\"contentinfo\" class=\"ui-grid-pager-panel\" ui-grid-pager ng-show=\"grid.options.enablePaginationControls\"><div role=\"navigation\" class=\"ui-grid-pager-container\"><div role=\"menubar\" class=\"ui-grid-pager-control\"><button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-first\" ui-grid-one-bind-title=\"aria.pageToFirst\" ui-grid-one-bind-aria-label=\"aria.pageToFirst\" ng-click=\"pageFirstPageClick()\" ng-disabled=\"cantPageBackward()\"><div ng-class=\"grid.isRTL() ? 'last-triangle' : 'first-triangle'\"><div ng-class=\"grid.isRTL() ? 'last-bar-rtl' : 'first-bar'\"></div></div></button> <button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-previous\" ui-grid-one-bind-title=\"aria.pageBack\" ui-grid-one-bind-aria-label=\"aria.pageBack\" ng-click=\"pagePreviousPageClick()\" ng-disabled=\"cantPageBackward()\"><div ng-class=\"grid.isRTL() ? 'last-triangle prev-triangle' : 'first-triangle prev-triangle'\"></div></button> <input type=\"number\" ui-grid-one-bind-title=\"aria.pageSelected\" ui-grid-one-bind-aria-label=\"aria.pageSelected\" class=\"ui-grid-pager-control-input\" ng-model=\"grid.options.paginationCurrentPage\" min=\"1\" max=\"{{ paginationApi.getTotalPages() }}\" required> <span class=\"ui-grid-pager-max-pages-number\" ng-show=\"paginationApi.getTotalPages() > 0\"><abbr ui-grid-one-bind-title=\"paginationOf\">/</abbr> {{ paginationApi.getTotalPages() }}</span> <button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-next\" ui-grid-one-bind-title=\"aria.pageForward\" ui-grid-one-bind-aria-label=\"aria.pageForward\" ng-click=\"pageNextPageClick()\" ng-disabled=\"cantPageForward()\"><div ng-class=\"grid.isRTL() ? 'first-triangle next-triangle' : 'last-triangle next-triangle'\"></div></button> <button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-last\" ui-grid-one-bind-title=\"aria.pageToLast\" ui-grid-one-bind-aria-label=\"aria.pageToLast\" ng-click=\"pageLastPageClick()\" ng-disabled=\"cantPageToLast()\"><div ng-class=\"grid.isRTL() ? 'first-triangle' : 'last-triangle'\"><div ng-class=\"grid.isRTL() ? 'first-bar-rtl' : 'last-bar'\"></div></div></button></div><div class=\"ui-grid-pager-row-count-picker\" ng-if=\"grid.options.paginationPageSizes.length > 1\"><select ui-grid-one-bind-aria-labelledby-grid=\"'items-per-page-label'\" ng-model=\"grid.options.paginationPageSize\" ng-options=\"o as o for o in grid.options.paginationPageSizes\"></select><span ui-grid-one-bind-id-grid=\"'items-per-page-label'\" class=\"ui-grid-pager-row-count-label\"> {{sizesLabel}}</span></div><span ng-if=\"grid.options.paginationPageSizes.length <= 1\" class=\"ui-grid-pager-row-count-label\">{{grid.options.paginationPageSize}} {{sizesLabel}}</span></div><div class=\"ui-grid-pager-count-container\"><div class=\"ui-grid-pager-count\"><span ng-show=\"grid.options.totalItems > 0\">{{showingLow}} <abbr ui-grid-one-bind-title=\"paginationThrough\">-</abbr> {{showingHigh}} {{paginationOf}} {{grid.options.totalItems}} {{totalItemsLabel}}</span></div></div></div>"
28019 $templateCache.put('ui-grid/columnResizer',
28020 "<div ui-grid-column-resizer ng-if=\"grid.options.enableColumnResizing\" class=\"ui-grid-column-resizer\" col=\"col\" position=\"right\" render-index=\"renderIndex\" unselectable=\"on\"></div>"
28024 $templateCache.put('ui-grid/gridFooterSelectedItems',
28025 "<span ng-if=\"grid.selection.selectedCount !== 0 && grid.options.enableFooterTotalSelected\">({{\"search.selectedItems\" | t}} {{grid.selection.selectedCount}})</span>"
28029 $templateCache.put('ui-grid/selectionHeaderCell',
28030 "<div><!-- <div class=\"ui-grid-vertical-bar\"> </div> --><div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><ui-grid-selection-select-all-buttons ng-if=\"grid.options.enableSelectAll\"></ui-grid-selection-select-all-buttons></div></div>"
28034 $templateCache.put('ui-grid/selectionRowHeader',
28035 "<div class=\"ui-grid-disable-selection\"><div class=\"ui-grid-cell-contents\"><ui-grid-selection-row-header-buttons></ui-grid-selection-row-header-buttons></div></div>"
28039 $templateCache.put('ui-grid/selectionRowHeaderButtons',
28040 "<div class=\"ui-grid-selection-row-header-buttons ui-grid-icon-ok\" ng-class=\"{'ui-grid-row-selected': row.isSelected}\" ng-click=\"selectButtonClick(row, $event)\"> </div>"
28044 $templateCache.put('ui-grid/selectionSelectAllButtons',
28045 "<div class=\"ui-grid-selection-row-header-buttons ui-grid-icon-ok\" ng-class=\"{'ui-grid-all-selected': grid.selection.selectAll}\" ng-click=\"headerButtonClick($event)\"></div>"
28049 $templateCache.put('ui-grid/treeBaseExpandAllButtons',
28050 "<div class=\"ui-grid-tree-base-row-header-buttons\" ng-class=\"{'ui-grid-icon-minus-squared': grid.treeBase.numberLevels > 0 && grid.treeBase.expandAll, 'ui-grid-icon-plus-squared': grid.treeBase.numberLevels > 0 && !grid.treeBase.expandAll}\" ng-click=\"headerButtonClick($event)\"></div>"
28054 $templateCache.put('ui-grid/treeBaseHeaderCell',
28055 "<div><div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><ui-grid-tree-base-expand-all-buttons ng-if=\"grid.options.enableExpandAll\"></ui-grid-tree-base-expand-all-buttons></div></div>"
28059 $templateCache.put('ui-grid/treeBaseRowHeader',
28060 "<div class=\"ui-grid-cell-contents\"><ui-grid-tree-base-row-header-buttons></ui-grid-tree-base-row-header-buttons></div>"
28064 $templateCache.put('ui-grid/treeBaseRowHeaderButtons',
28065 "<div class=\"ui-grid-tree-base-row-header-buttons\" ng-class=\"{'ui-grid-tree-base-header': row.treeLevel > -1 }\" ng-click=\"treeButtonClick(row, $event)\"><i ng-class=\"{'ui-grid-icon-minus-squared': ( ( grid.options.showTreeExpandNoChildren && row.treeLevel > -1 ) || ( row.treeNode.children && row.treeNode.children.length > 0 ) ) && row.treeNode.state === 'expanded', 'ui-grid-icon-plus-squared': ( ( grid.options.showTreeExpandNoChildren && row.treeLevel > -1 ) || ( row.treeNode.children && row.treeNode.children.length > 0 ) ) && row.treeNode.state === 'collapsed'}\" ng-style=\"{'padding-left': grid.options.treeIndent * row.treeLevel + 'px'}\"></i> </div>"
28069 $templateCache.put('ui-grid/cellTitleValidator',
28070 "<div class=\"ui-grid-cell-contents\" ng-class=\"{invalid:grid.validate.isInvalid(row.entity,col.colDef)}\" title=\"{{grid.validate.getTitleFormattedErrors(row.entity,col.colDef)}}\">{{COL_FIELD CUSTOM_FILTERS}}</div>"
28074 $templateCache.put('ui-grid/cellTooltipValidator',
28075 "<div class=\"ui-grid-cell-contents\" ng-class=\"{invalid:grid.validate.isInvalid(row.entity,col.colDef)}\" tooltip-html-unsafe=\"{{grid.validate.getFormattedErrors(row.entity,col.colDef)}}\" tooltip-enable=\"grid.validate.isInvalid(row.entity,col.colDef)\" tooltip-append-to-body=\"true\" tooltip-placement=\"top\" title=\"TOOLTIP\">{{COL_FIELD CUSTOM_FILTERS}}</div>"