2 * ui-grid - v3.0.7 - 2015-10-06
3 * Copyright (c) 2015 ; 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, o) {
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();
455 title: i18nService.getSafeText('columnMenu.close'),
456 screenReaderOnly: true,
460 action: function($event){
461 $event.stopPropagation();
470 * @methodOf ui.grid.service:uiGridColumnMenuService
471 * @name getColumnElementPosition
472 * @description gets the position information needed to place the column
473 * menu below the column header
474 * @param {$scope} $scope the $scope from the uiGridColumnMenu
475 * @param {GridCol} column the column we want to position below
476 * @param {element} $columnElement the column element we want to position below
477 * @returns {hash} containing left, top, offset, height, width
480 getColumnElementPosition: function( $scope, column, $columnElement ){
481 var positionData = {};
482 positionData.left = $columnElement[0].offsetLeft;
483 positionData.top = $columnElement[0].offsetTop;
484 positionData.parentLeft = $columnElement[0].offsetParent.offsetLeft;
486 // Get the grid scrollLeft
487 positionData.offset = 0;
488 if (column.grid.options.offsetLeft) {
489 positionData.offset = column.grid.options.offsetLeft;
492 positionData.height = gridUtil.elementHeight($columnElement, true);
493 positionData.width = gridUtil.elementWidth($columnElement, true);
501 * @methodOf ui.grid.service:uiGridColumnMenuService
502 * @name repositionMenu
503 * @description Reposition the menu below the new column. If the menu has no child nodes
504 * (i.e. it's not currently visible) then we guess it's width at 100, we'll be called again
506 * @param {$scope} $scope the $scope from the uiGridColumnMenu
507 * @param {GridCol} column the column we want to position below
508 * @param {hash} positionData a hash containing left, top, offset, height, width
509 * @param {element} $elm the column menu element that we want to reposition
510 * @param {element} $columnElement the column element that we want to reposition underneath
513 repositionMenu: function( $scope, column, positionData, $elm, $columnElement ) {
514 var menu = $elm[0].querySelectorAll('.ui-grid-menu');
515 var containerId = column.renderContainer ? column.renderContainer : 'body';
516 var renderContainer = column.grid.renderContainers[containerId];
518 // It's possible that the render container of the column we're attaching to is
519 // offset from the grid (i.e. pinned containers), we need to get the difference in the offsetLeft
520 // between the render container and the grid
521 var renderContainerElm = gridUtil.closestElm($columnElement, '.ui-grid-render-container');
522 var renderContainerOffset = renderContainerElm.getBoundingClientRect().left - $scope.grid.element[0].getBoundingClientRect().left;
524 var containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].scrollLeft;
526 // default value the last width for _this_ column, otherwise last width for _any_ column, otherwise default to 170
527 var myWidth = column.lastMenuWidth ? column.lastMenuWidth : ( $scope.lastMenuWidth ? $scope.lastMenuWidth : 170);
528 var paddingRight = column.lastMenuPaddingRight ? column.lastMenuPaddingRight : ( $scope.lastMenuPaddingRight ? $scope.lastMenuPaddingRight : 10);
530 if ( menu.length !== 0 ){
531 var mid = menu[0].querySelectorAll('.ui-grid-menu-mid');
532 if ( mid.length !== 0 && !angular.element(mid).hasClass('ng-hide') ) {
533 myWidth = gridUtil.elementWidth(menu, true);
534 $scope.lastMenuWidth = myWidth;
535 column.lastMenuWidth = myWidth;
537 // TODO(c0bra): use padding-left/padding-right based on document direction (ltr/rtl), place menu on proper side
538 // Get the column menu right padding
539 paddingRight = parseInt(gridUtil.getStyles(angular.element(menu)[0])['paddingRight'], 10);
540 $scope.lastMenuPaddingRight = paddingRight;
541 column.lastMenuPaddingRight = paddingRight;
545 var left = positionData.left + renderContainerOffset - containerScrollLeft + positionData.parentLeft + positionData.width - myWidth + paddingRight;
546 if (left < positionData.offset){
547 left = positionData.offset;
550 $elm.css('left', left + 'px');
551 $elm.css('top', (positionData.top + positionData.height) + 'px');
560 .directive('uiGridColumnMenu', ['$timeout', 'gridUtil', 'uiGridConstants', 'uiGridColumnMenuService', '$document',
561 function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $document) {
564 * @name ui.grid.directive:uiGridColumnMenu
565 * @description Provides the column menu framework, leverages uiGridMenu underneath
569 var uiGridColumnMenu = {
573 templateUrl: 'ui-grid/uiGridColumnMenu',
575 link: function ($scope, $elm, $attrs, uiGridCtrl) {
578 uiGridColumnMenuService.initialize( $scope, uiGridCtrl );
580 $scope.defaultMenuItems = uiGridColumnMenuService.getDefaultMenuItems( $scope );
582 // Set the menu items for use with the column menu. The user can later add additional items via the watch
583 $scope.menuItems = $scope.defaultMenuItems;
584 uiGridColumnMenuService.setColMenuItemWatch( $scope );
589 * @methodOf ui.grid.directive:uiGridColumnMenu
591 * @description Shows the column menu. If the menu is already displayed it
592 * calls the menu to ask it to hide (it will animate), then it repositions the menu
593 * to the right place whilst hidden (it will make an assumption on menu width),
594 * then it asks the menu to show (it will animate), then it repositions the menu again
595 * once we can calculate it's size.
596 * @param {GridCol} column the column we want to position below
597 * @param {element} $columnElement the column element we want to position below
599 $scope.showMenu = function(column, $columnElement, event) {
600 // Swap to this column
603 // Get the position information for the column element
604 var colElementPosition = uiGridColumnMenuService.getColumnElementPosition( $scope, column, $columnElement );
606 if ($scope.menuShown) {
607 // we want to hide, then reposition, then show, but we want to wait for animations
608 // we set a variable, and then rely on the menu-hidden event to call the reposition and show
609 $scope.colElement = $columnElement;
610 $scope.colElementPosition = colElementPosition;
611 $scope.hideThenShow = true;
613 $scope.$broadcast('hide-menu', { originalEvent: event });
615 self.shown = $scope.menuShown = true;
616 uiGridColumnMenuService.repositionMenu( $scope, column, colElementPosition, $elm, $columnElement );
618 $scope.colElement = $columnElement;
619 $scope.colElementPosition = colElementPosition;
620 $scope.$broadcast('show-menu', { originalEvent: event });
627 * @methodOf ui.grid.directive:uiGridColumnMenu
629 * @description Hides the column menu.
630 * @param {boolean} broadcastTrigger true if we were triggered by a broadcast
631 * from the menu itself - in which case don't broadcast again as we'll get
634 $scope.hideMenu = function( broadcastTrigger ) {
635 $scope.menuShown = false;
636 if ( !broadcastTrigger ){
637 $scope.$broadcast('hide-menu');
642 $scope.$on('menu-hidden', function() {
643 if ( $scope.hideThenShow ){
644 delete $scope.hideThenShow;
646 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
647 $scope.$broadcast('show-menu');
649 $scope.menuShown = true;
651 $scope.hideMenu( true );
654 //Focus on the menu button
655 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false);
660 $scope.$on('menu-shown', function() {
661 $timeout( function() {
662 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
663 delete $scope.colElementPosition;
664 delete $scope.columnElement;
670 $scope.sortColumn = function (event, dir) {
671 event.stopPropagation();
673 $scope.grid.sortColumn($scope.col, dir, true)
675 $scope.grid.refresh();
680 $scope.unsortColumn = function () {
683 $scope.grid.refresh();
687 //Since we are hiding this column the default hide action will fail so we need to focus somewhere else.
688 var setFocusOnHideColumn = function(){
690 // Get the UID of the first
691 var focusToGridMenu = function(){
692 return gridUtil.focus.byId('grid-menu', $scope.grid);
696 $scope.grid.columns.some(function(element, index){
697 if (angular.equals(element, $scope.col)) {
703 var previousVisibleCol;
704 // Try and find the next lower or nearest column to focus on
705 $scope.grid.columns.some(function(element, index){
706 if (!element.visible){
708 } // This columns index is below the current column index
709 else if ( index < thisIndex){
710 previousVisibleCol = element;
711 } // This elements index is above this column index and we haven't found one that is lower
712 else if ( index > thisIndex && !previousVisibleCol) {
713 // This is the next best thing
714 previousVisibleCol = element;
715 // We've found one so use it.
717 } // We've reached an element with an index above this column and the previousVisibleCol variable has been set
718 else if (index > thisIndex && previousVisibleCol) {
723 // If found then focus on it
724 if (previousVisibleCol){
725 var colClass = previousVisibleCol.getColClass();
726 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + colClass+ ' .ui-grid-header-cell-primary-focus', true).then(angular.noop, function(reason){
727 if (reason !== 'canceled'){ // If this is canceled then don't perform the action
728 //The fallback action is to focus on the grid menu
729 return focusToGridMenu();
733 // Fallback action to focus on the grid menu
739 $scope.hideColumn = function () {
740 $scope.col.colDef.visible = false;
741 $scope.col.visible = false;
743 $scope.grid.queueGridRefresh();
745 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
746 $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col );
748 // We are hiding so the default action of focusing on the button that opened this menu will fail.
749 setFocusOnHideColumn();
755 controller: ['$scope', function ($scope) {
758 $scope.$watch('menuItems', function (n, o) {
764 return uiGridColumnMenu;
773 angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', 'i18nService', 'gridUtil', function ($compile, $templateCache, i18nService, gridUtil) {
776 compile: function() {
778 pre: function ($scope, $elm, $attrs, controllers) {
779 $scope.col.updateFilters = function( filterable ){
780 $elm.children().remove();
782 var template = $scope.col.filterHeaderTemplate;
784 $elm.append($compile(template)($scope));
788 $scope.$on( '$destroy', function() {
789 delete $scope.col.updateFilters;
792 post: function ($scope, $elm, $attrs, controllers){
793 $scope.aria = i18nService.getSafeText('headerCell.aria');
794 $scope.removeFilter = function(colFilter, index){
795 colFilter.term = null;
796 //Set the focus to the filter input after the action disables the button
797 gridUtil.focus.bySelector($elm, '.ui-grid-filter-input-' + index);
809 angular.module('ui.grid').directive('uiGridFooterCell', ['$timeout', 'gridUtil', 'uiGridConstants', '$compile',
810 function ($timeout, gridUtil, uiGridConstants, $compile) {
811 var uiGridFooterCell = {
820 compile: function compile(tElement, tAttrs, transclude) {
822 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
823 var cellFooter = $compile($scope.col.footerCellTemplate)($scope);
824 $elm.append(cellFooter);
826 post: function ($scope, $elm, $attrs, uiGridCtrl) {
827 //$elm.addClass($scope.col.getColClass(false));
828 $scope.grid = uiGridCtrl.grid;
830 var initColClass = $scope.col.getColClass(false);
831 $elm.addClass(initColClass);
833 // apply any footerCellClass
835 var updateClass = function( grid ){
838 contents.removeClass( classAdded );
842 if (angular.isFunction($scope.col.footerCellClass)) {
843 classAdded = $scope.col.footerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
846 classAdded = $scope.col.footerCellClass;
848 contents.addClass(classAdded);
851 if ($scope.col.footerCellClass) {
855 $scope.col.updateAggregationValue();
857 // Watch for column changes so we can alter the col cell class properly
858 /* shouldn't be needed any more, given track by col.name
859 $scope.$watch('col', function (n, o) {
861 // See if the column's internal class has changed
862 var newColClass = $scope.col.getColClass(false);
863 if (newColClass !== initColClass) {
864 $elm.removeClass(initColClass);
865 $elm.addClass(newColClass);
866 initColClass = newColClass;
873 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
874 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]);
875 // listen for visible rows change and update aggregation values
876 $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue );
877 $scope.grid.api.core.on.rowsRendered( $scope, updateClass );
878 $scope.$on( '$destroy', dataChangeDereg );
884 return uiGridFooterCell;
892 angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
898 require: ['^uiGrid', '^uiGridRenderContainer'],
900 compile: function ($elm, $attrs) {
902 pre: function ($scope, $elm, $attrs, controllers) {
903 var uiGridCtrl = controllers[0];
904 var containerCtrl = controllers[1];
906 $scope.grid = uiGridCtrl.grid;
907 $scope.colContainer = containerCtrl.colContainer;
909 containerCtrl.footer = $elm;
911 var footerTemplate = $scope.grid.options.footerTemplate;
912 gridUtil.getTemplate(footerTemplate)
913 .then(function (contents) {
914 var template = angular.element(contents);
916 var newElm = $compile(template)($scope);
920 // Inject a reference to the footer viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
921 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
923 if (footerViewport) {
924 containerCtrl.footerViewport = footerViewport;
930 post: function ($scope, $elm, $attrs, controllers) {
931 var uiGridCtrl = controllers[0];
932 var containerCtrl = controllers[1];
934 // gridUtil.logDebug('ui-grid-footer link');
936 var grid = uiGridCtrl.grid;
938 // Don't animate footer cells
939 gridUtil.disableAnimations($elm);
941 containerCtrl.footer = $elm;
943 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
944 if (footerViewport) {
945 containerCtrl.footerViewport = footerViewport;
957 angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
965 compile: function ($elm, $attrs) {
967 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
969 $scope.grid = uiGridCtrl.grid;
973 var footerTemplate = $scope.grid.options.gridFooterTemplate;
974 gridUtil.getTemplate(footerTemplate)
975 .then(function (contents) {
976 var template = angular.element(contents);
978 var newElm = $compile(template)($scope);
983 post: function ($scope, $elm, $attrs, controllers) {
995 angular.module('ui.grid').directive('uiGridGroupPanel', ["$compile", "uiGridConstants", "gridUtil", function($compile, uiGridConstants, gridUtil) {
996 var defaultTemplate = 'ui-grid/ui-grid-group-panel';
1001 require: '?^uiGrid',
1003 compile: function($elm, $attrs) {
1005 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
1006 var groupPanelTemplate = $scope.grid.options.groupPanelTemplate || defaultTemplate;
1008 gridUtil.getTemplate(groupPanelTemplate)
1009 .then(function (contents) {
1010 var template = angular.element(contents);
1012 var newElm = $compile(template)($scope);
1013 $elm.append(newElm);
1017 post: function ($scope, $elm, $attrs, uiGridCtrl) {
1018 $elm.bind('$destroy', function() {
1019 // scrollUnbinder();
1031 angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService',
1032 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) {
1033 // Do stuff after mouse has been down this many ms on the header cell
1034 var mousedownTimeout = 500;
1035 var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa
1037 var uiGridHeaderCell = {
1044 require: ['^uiGrid', '^uiGridRenderContainer'],
1046 compile: function() {
1048 pre: function ($scope, $elm, $attrs) {
1049 var cellHeader = $compile($scope.col.headerCellTemplate)($scope);
1050 $elm.append(cellHeader);
1053 post: function ($scope, $elm, $attrs, controllers) {
1054 var uiGridCtrl = controllers[0];
1055 var renderContainerCtrl = controllers[1];
1058 headerCell: i18nService.getSafeText('headerCell'),
1059 sort: i18nService.getSafeText('sort')
1061 $scope.getSortDirectionAriaLabel = function(){
1062 var col = $scope.col;
1063 //Trying to recreate this sort of thing but it was getting messy having it in the template.
1064 //Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''}
1065 var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none);
1066 var label = sortDirectionText;
1067 //Append the priority if it exists
1068 if (col.sort.priority) {
1069 label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority;
1074 $scope.grid = uiGridCtrl.grid;
1076 $scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId];
1078 var initColClass = $scope.col.getColClass(false);
1079 $elm.addClass(initColClass);
1081 // Hide the menu by default
1082 $scope.menuShown = false;
1084 // Put asc and desc sort directions in scope
1085 $scope.asc = uiGridConstants.ASC;
1086 $scope.desc = uiGridConstants.DESC;
1088 // Store a reference to menu element
1089 var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') );
1091 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
1094 // apply any headerCellClass
1099 var filterDeregisters = [];
1103 * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart).
1104 * Once we have a down event, we need to work out whether we have a click, a drag, or a
1105 * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
1106 * we ignore it. A hold would open the menu.
1108 * So, on down event, we put in place handlers for move and up events, and a timer. If the
1109 * timer expires before we see a move or up, then we have a long press and hence a column menu open.
1110 * If the up happens before the timer, then we have a click, and we sort if the column is sortable.
1111 * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature
1114 * To deal with touch enabled devices that also have mice, we only create our handlers when
1115 * we get the down event, and we create the corresponding handlers - if we're touchstart then
1116 * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup.
1118 * We also suppress the click action whilst this is happening - otherwise after the mouseup there
1119 * will be a click event and that can cause the column menu to close
1123 $scope.downFn = function( event ){
1124 event.stopPropagation();
1126 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
1127 event = event.originalEvent;
1130 // Don't show the menu if it's not the left button
1131 if (event.button && event.button !== 0) {
1134 previousMouseX = event.pageX;
1136 $scope.mousedownStartTime = (new Date()).getTime();
1137 $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout);
1139 $scope.mousedownTimeout.then(function () {
1140 if ( $scope.colMenu ) {
1141 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event);
1145 uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name});
1147 $scope.offAllEvents();
1148 if ( event.type === 'touchstart'){
1149 $document.on('touchend', $scope.upFn);
1150 $document.on('touchmove', $scope.moveFn);
1151 } else if ( event.type === 'mousedown' ){
1152 $document.on('mouseup', $scope.upFn);
1153 $document.on('mousemove', $scope.moveFn);
1157 $scope.upFn = function( event ){
1158 event.stopPropagation();
1159 $timeout.cancel($scope.mousedownTimeout);
1160 $scope.offAllEvents();
1161 $scope.onDownEvents(event.type);
1163 var mousedownEndTime = (new Date()).getTime();
1164 var mousedownTime = mousedownEndTime - $scope.mousedownStartTime;
1166 if (mousedownTime > mousedownTimeout) {
1167 // long click, handled above with mousedown
1171 if ( $scope.sortable ){
1172 $scope.handleClick(event);
1177 $scope.moveFn = function( event ){
1178 // Chrome is known to fire some bogus move events.
1179 var changeValue = event.pageX - previousMouseX;
1180 if ( changeValue === 0 ){ return; }
1182 // we're a move, so do nothing and leave for column move (if enabled) to take over
1183 $timeout.cancel($scope.mousedownTimeout);
1184 $scope.offAllEvents();
1185 $scope.onDownEvents(event.type);
1188 $scope.clickFn = function ( event ){
1189 event.stopPropagation();
1190 $contentsElm.off('click', $scope.clickFn);
1194 $scope.offAllEvents = function(){
1195 $contentsElm.off('touchstart', $scope.downFn);
1196 $contentsElm.off('mousedown', $scope.downFn);
1198 $document.off('touchend', $scope.upFn);
1199 $document.off('mouseup', $scope.upFn);
1201 $document.off('touchmove', $scope.moveFn);
1202 $document.off('mousemove', $scope.moveFn);
1204 $contentsElm.off('click', $scope.clickFn);
1207 $scope.onDownEvents = function( type ){
1208 // If there is a previous event, then wait a while before
1209 // activating the other mode - i.e. if the last event was a touch event then
1210 // don't enable mouse events for a wee while (500ms or so)
1211 // Avoids problems with devices that emulate mouse events when you have touch events
1216 $contentsElm.on('click', $scope.clickFn);
1217 $contentsElm.on('touchstart', $scope.downFn);
1218 $timeout(function(){
1219 $contentsElm.on('mousedown', $scope.downFn);
1220 }, changeModeTimeout);
1224 $contentsElm.on('click', $scope.clickFn);
1225 $contentsElm.on('mousedown', $scope.downFn);
1226 $timeout(function(){
1227 $contentsElm.on('touchstart', $scope.downFn);
1228 }, changeModeTimeout);
1231 $contentsElm.on('click', $scope.clickFn);
1232 $contentsElm.on('touchstart', $scope.downFn);
1233 $contentsElm.on('mousedown', $scope.downFn);
1238 var updateHeaderOptions = function( grid ){
1239 var contents = $elm;
1241 contents.removeClass( classAdded );
1245 if (angular.isFunction($scope.col.headerCellClass)) {
1246 classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
1249 classAdded = $scope.col.headerCellClass;
1251 contents.addClass(classAdded);
1253 var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body'];
1254 $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] );
1256 // Figure out whether this column is sortable or not
1257 if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) {
1258 $scope.sortable = true;
1261 $scope.sortable = false;
1264 // Figure out whether this column is filterable or not
1265 var oldFilterable = $scope.filterable;
1266 if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) {
1267 $scope.filterable = true;
1270 $scope.filterable = false;
1273 if ( oldFilterable !== $scope.filterable){
1274 if ( typeof($scope.col.updateFilters) !== 'undefined' ){
1275 $scope.col.updateFilters($scope.filterable);
1278 // if column is filterable add a filter watcher
1279 if ($scope.filterable) {
1280 $scope.col.filters.forEach( function(filter, i) {
1281 filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) {
1283 uiGridCtrl.grid.api.core.raise.filterChanged();
1284 uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1285 uiGridCtrl.grid.queueGridRefresh();
1289 $scope.$on('$destroy', function() {
1290 filterDeregisters.forEach( function(filterDeregister) {
1295 filterDeregisters.forEach( function(filterDeregister) {
1302 // figure out whether we support column menus
1303 if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
1304 $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){
1305 $scope.colMenu = true;
1307 $scope.colMenu = false;
1312 * @name enableColumnMenu
1313 * @propertyOf ui.grid.class:GridOptions.columnDef
1314 * @description if column menus are enabled, controls the column menus for this specific
1315 * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus
1316 * using this option. If gridOptions.enableColumnMenus === false then you get no column
1317 * menus irrespective of the value of this option ). Defaults to true.
1322 * @name enableColumnMenus
1323 * @propertyOf ui.grid.class:GridOptions.columnDef
1324 * @description Override for column menus everywhere - if set to false then you get no
1325 * column menus. Defaults to true.
1329 $scope.offAllEvents();
1331 if ($scope.sortable || $scope.colMenu) {
1332 $scope.onDownEvents();
1334 $scope.$on('$destroy', function () {
1335 $scope.offAllEvents();
1341 $scope.$watch('col', function (n, o) {
1343 // See if the column's internal class has changed
1344 var newColClass = $scope.col.getColClass(false);
1345 if (newColClass !== initColClass) {
1346 $elm.removeClass(initColClass);
1347 $elm.addClass(newColClass);
1348 initColClass = newColClass;
1353 updateHeaderOptions();
1355 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
1356 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]);
1358 $scope.$on( '$destroy', dataChangeDereg );
1360 $scope.handleClick = function(event) {
1361 // If the shift key is being held down, add this column to the sort
1363 if (event.shiftKey) {
1367 // Sort this column then rebuild the grid's rows
1368 uiGridCtrl.grid.sortColumn($scope.col, add)
1370 if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
1371 uiGridCtrl.grid.refresh();
1376 $scope.toggleMenu = function(event) {
1377 event.stopPropagation();
1379 // If the menu is already showing...
1380 if (uiGridCtrl.columnMenuScope.menuShown) {
1381 // ... and we're the column the menu is on...
1382 if (uiGridCtrl.columnMenuScope.col === $scope.col) {
1384 uiGridCtrl.columnMenuScope.hideMenu();
1386 // ... and we're NOT the column the menu is on
1388 // ... move the menu to our column
1389 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1392 // If the menu is NOT showing
1394 // ... show it on our column
1395 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1403 return uiGridHeaderCell;
1411 angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent',
1412 function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) {
1413 var defaultTemplate = 'ui-grid/ui-grid-header';
1414 var emptyTemplate = 'ui-grid/ui-grid-no-header';
1418 // templateUrl: 'ui-grid/ui-grid-header',
1421 require: ['^uiGrid', '^uiGridRenderContainer'],
1423 compile: function($elm, $attrs) {
1425 pre: function ($scope, $elm, $attrs, controllers) {
1426 var uiGridCtrl = controllers[0];
1427 var containerCtrl = controllers[1];
1429 $scope.grid = uiGridCtrl.grid;
1430 $scope.colContainer = containerCtrl.colContainer;
1432 updateHeaderReferences();
1435 if (!$scope.grid.options.showHeader) {
1436 headerTemplate = emptyTemplate;
1439 headerTemplate = ($scope.grid.options.headerTemplate) ? $scope.grid.options.headerTemplate : defaultTemplate;
1442 gridUtil.getTemplate(headerTemplate)
1443 .then(function (contents) {
1444 var template = angular.element(contents);
1446 var newElm = $compile(template)($scope);
1447 $elm.replaceWith(newElm);
1449 // And update $elm to be the new element
1452 updateHeaderReferences();
1454 if (containerCtrl) {
1455 // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
1456 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1459 if (headerViewport) {
1460 containerCtrl.headerViewport = headerViewport;
1461 angular.element(headerViewport).on('scroll', scrollHandler);
1462 $scope.$on('$destroy', function () {
1463 angular.element(headerViewport).off('scroll', scrollHandler);
1468 $scope.grid.queueRefresh();
1471 function updateHeaderReferences() {
1472 containerCtrl.header = containerCtrl.colContainer.header = $elm;
1474 var headerCanvases = $elm[0].getElementsByClassName('ui-grid-header-canvas');
1476 if (headerCanvases.length > 0) {
1477 containerCtrl.headerCanvas = containerCtrl.colContainer.headerCanvas = headerCanvases[0];
1480 containerCtrl.headerCanvas = null;
1484 function scrollHandler(evt) {
1485 if (uiGridCtrl.grid.isScrollingHorizontally) {
1488 var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid);
1489 var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft);
1491 var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll);
1492 scrollEvent.newScrollLeft = newScrollLeft;
1493 if ( horizScrollPercentage > -1 ){
1494 scrollEvent.x = { percentage: horizScrollPercentage };
1497 uiGridCtrl.grid.scrollContainers(null, scrollEvent);
1501 post: function ($scope, $elm, $attrs, controllers) {
1502 var uiGridCtrl = controllers[0];
1503 var containerCtrl = controllers[1];
1505 // gridUtil.logDebug('ui-grid-header link');
1507 var grid = uiGridCtrl.grid;
1509 // Don't animate header cells
1510 gridUtil.disableAnimations($elm);
1512 function updateColumnWidths() {
1513 // this styleBuilder always runs after the renderContainer, so we can rely on the column widths
1514 // already being populated correctly
1516 var columnCache = containerCtrl.colContainer.visibleColumnCache;
1519 // uiGridCtrl.grid.columns.forEach(function (column) {
1521 var canvasWidth = 0;
1522 columnCache.forEach(function (column) {
1523 ret = ret + column.getColClassDefinition();
1524 canvasWidth += column.drawnWidth;
1527 containerCtrl.colContainer.canvasWidth = canvasWidth;
1529 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
1533 containerCtrl.header = $elm;
1535 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1536 if (headerViewport) {
1537 containerCtrl.headerViewport = headerViewport;
1540 //todo: remove this if by injecting gridCtrl into unit tests
1542 uiGridCtrl.grid.registerStyleComputation({
1544 func: updateColumnWidths
1557 angular.module('ui.grid')
1558 .service('uiGridGridMenuService', [ 'gridUtil', 'i18nService', 'uiGridConstants', function( gridUtil, i18nService, uiGridConstants ) {
1561 * @name ui.grid.gridMenuService
1563 * @description Methods for working with the grid menu
1569 * @methodOf ui.grid.gridMenuService
1571 * @description Sets up the gridMenu. Most importantly, sets our
1572 * scope onto the grid object as grid.gridMenuScope, allowing us
1573 * to operate when passed only the grid. Second most importantly,
1574 * we register the 'addToGridMenu' and 'removeFromGridMenu' methods
1576 * @param {$scope} $scope the scope of this gridMenu
1577 * @param {Grid} grid the grid to which this gridMenu is associated
1579 initialize: function( $scope, grid ){
1580 grid.gridMenuScope = $scope;
1582 $scope.registeredMenuItems = [];
1584 // not certain this is needed, but would be bad to create a memory leak
1585 $scope.$on('$destroy', function() {
1586 if ( $scope.grid && $scope.grid.gridMenuScope ){
1587 $scope.grid.gridMenuScope = null;
1592 if ( $scope.registeredMenuItems ){
1593 $scope.registeredMenuItems = null;
1597 $scope.registeredMenuItems = [];
1601 * @name addToGridMenu
1602 * @methodOf ui.grid.core.api:PublicApi
1603 * @description add items to the grid menu. Used by features
1604 * to add their menu items if they are enabled, can also be used by
1605 * end users to add menu items. This method has the advantage of allowing
1606 * remove again, which can simplify management of which items are included
1607 * in the menu when. (Noting that in most cases the shown and active functions
1608 * provide a better way to handle visibility of menu items)
1609 * @param {Grid} grid the grid on which we are acting
1610 * @param {array} items menu items in the format as described in the tutorial, with
1611 * the added note that if you want to use remove you must also specify an `id` field,
1612 * which is provided when you want to remove an item. The id should be unique.
1615 grid.api.registerMethod( 'core', 'addToGridMenu', service.addToGridMenu );
1619 * @name removeFromGridMenu
1620 * @methodOf ui.grid.core.api:PublicApi
1621 * @description Remove an item from the grid menu based on a provided id. Assumes
1622 * that the id is unique, removes only the last instance of that id. Does nothing if
1623 * the specified id is not found
1624 * @param {Grid} grid the grid on which we are acting
1625 * @param {string} id the id we'd like to remove from the menu
1628 grid.api.registerMethod( 'core', 'removeFromGridMenu', service.removeFromGridMenu );
1634 * @name addToGridMenu
1635 * @propertyOf ui.grid.gridMenuService
1636 * @description add items to the grid menu. Used by features
1637 * to add their menu items if they are enabled, can also be used by
1638 * end users to add menu items. This method has the advantage of allowing
1639 * remove again, which can simplify management of which items are included
1640 * in the menu when. (Noting that in most cases the shown and active functions
1641 * provide a better way to handle visibility of menu items)
1642 * @param {Grid} grid the grid on which we are acting
1643 * @param {array} items menu items in the format as described in the tutorial, with
1644 * the added note that if you want to use remove you must also specify an `id` field,
1645 * which is provided when you want to remove an item. The id should be unique.
1648 addToGridMenu: function( grid, menuItems ) {
1649 if ( !angular.isArray( menuItems ) ) {
1650 gridUtil.logError( 'addToGridMenu: menuItems must be an array, and is not, not adding any items');
1652 if ( grid.gridMenuScope ){
1653 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems ? grid.gridMenuScope.registeredMenuItems : [];
1654 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems.concat( menuItems );
1656 gridUtil.logError( 'Asked to addToGridMenu, but gridMenuScope not present. Timing issue? Please log issue with ui-grid');
1664 * @name removeFromGridMenu
1665 * @methodOf ui.grid.gridMenuService
1666 * @description Remove an item from the grid menu based on a provided id. Assumes
1667 * that the id is unique, removes only the last instance of that id. Does nothing if
1668 * the specified id is not found. If there is no gridMenuScope or registeredMenuItems
1669 * then do nothing silently - the desired result is those menu items not be present and they
1671 * @param {Grid} grid the grid on which we are acting
1672 * @param {string} id the id we'd like to remove from the menu
1675 removeFromGridMenu: function( grid, id ){
1676 var foundIndex = -1;
1678 if ( grid && grid.gridMenuScope ){
1679 grid.gridMenuScope.registeredMenuItems.forEach( function( value, index ) {
1680 if ( value.id === id ){
1681 if (foundIndex > -1) {
1682 gridUtil.logError( 'removeFromGridMenu: found multiple items with the same id, removing only the last' );
1691 if ( foundIndex > -1 ){
1692 grid.gridMenuScope.registeredMenuItems.splice( foundIndex, 1 );
1699 * @name gridMenuCustomItems
1700 * @propertyOf ui.grid.class:GridOptions
1701 * @description (optional) An array of menu items that should be added to
1702 * the gridMenu. Follow the format documented in the tutorial for column
1703 * menu customisation. The context provided to the action function will
1704 * include context.grid. An alternative if working with dynamic menus is to use the
1705 * provided api - core.addToGridMenu and core.removeFromGridMenu, which handles
1706 * some of the management of items for you.
1711 * @name gridMenuShowHideColumns
1712 * @propertyOf ui.grid.class:GridOptions
1713 * @description true by default, whether the grid menu should allow hide/show
1719 * @methodOf ui.grid.gridMenuService
1720 * @name getMenuItems
1721 * @description Decides the menu items to show in the menu. This is a
1724 * - the default menu items that are always included,
1725 * - any menu items that have been provided through the addMenuItem api. These
1726 * are typically added by features within the grid
1727 * - any menu items included in grid.options.gridMenuCustomItems. These can be
1728 * changed dynamically, as they're always recalculated whenever we show the
1730 * @param {$scope} $scope the scope of this gridMenu, from which we can find all
1731 * the information that we need
1732 * @returns {array} an array of menu items that can be shown
1734 getMenuItems: function( $scope ) {
1736 // this is where we add any menu items we want to always include
1739 if ( $scope.grid.options.gridMenuCustomItems ){
1740 if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){
1741 gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not');
1743 menuItems = menuItems.concat( $scope.grid.options.gridMenuCustomItems );
1747 var clearFilters = [{
1748 title: i18nService.getSafeText('gridMenu.clearAllFilters'),
1749 action: function ($event) {
1750 $scope.grid.clearAllFilters(undefined, true, undefined);
1753 return $scope.grid.options.enableFiltering;
1757 menuItems = menuItems.concat( clearFilters );
1759 menuItems = menuItems.concat( $scope.registeredMenuItems );
1761 if ( $scope.grid.options.gridMenuShowHideColumns !== false ){
1762 menuItems = menuItems.concat( service.showHideColumns( $scope ) );
1765 menuItems.sort(function(a, b){
1766 return a.order - b.order;
1775 * @name gridMenuTitleFilter
1776 * @propertyOf ui.grid.class:GridOptions
1777 * @description (optional) A function that takes a title string
1778 * (usually the col.displayName), and converts it into a display value. The function
1779 * must return either a string or a promise.
1781 * Used for internationalization of the grid menu column names - for angular-translate
1782 * you can pass $translate as the function, for i18nService you can pass getSafeText as the
1787 * gridMenuTitleFilter: $translate
1793 * @methodOf ui.grid.gridMenuService
1794 * @name showHideColumns
1795 * @description Adds two menu items for each of the columns in columnDefs. One
1796 * menu item for hide, one menu item for show. Each is visible when appropriate
1797 * (show when column is not visible, hide when column is visible). Each toggles
1798 * the visible property on the columnDef using toggleColumnVisibility
1799 * @param {$scope} $scope of a gridMenu, which contains a reference to the grid
1801 showHideColumns: function( $scope ){
1802 var showHideColumns = [];
1803 if ( !$scope.grid.options.columnDefs || $scope.grid.options.columnDefs.length === 0 || $scope.grid.columns.length === 0 ) {
1804 return showHideColumns;
1807 // add header for columns
1808 showHideColumns.push({
1809 title: i18nService.getSafeText('gridMenu.columns'),
1813 $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; };
1815 $scope.grid.options.columnDefs.forEach( function( colDef, index ){
1816 if ( colDef.enableHiding !== false ){
1817 // add hide menu item - shows an OK icon as we only show when column is already visible
1819 icon: 'ui-grid-icon-ok',
1820 action: function($event) {
1821 $event.stopPropagation();
1822 service.toggleColumnVisibility( this.context.gridCol );
1825 return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined;
1827 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1829 order: 301 + index * 2
1831 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1832 showHideColumns.push( menuItem );
1834 // add show menu item - shows no icon as we only show when column is invisible
1836 icon: 'ui-grid-icon-cancel',
1837 action: function($event) {
1838 $event.stopPropagation();
1839 service.toggleColumnVisibility( this.context.gridCol );
1842 return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined);
1844 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1846 order: 301 + index * 2 + 1
1848 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1849 showHideColumns.push( menuItem );
1852 return showHideColumns;
1858 * @methodOf ui.grid.gridMenuService
1859 * @name setMenuItemTitle
1860 * @description Handles the response from gridMenuTitleFilter, adding it directly to the menu
1861 * item if it returns a string, otherwise waiting for the promise to resolve or reject then
1862 * putting the result into the title
1863 * @param {object} menuItem the menuItem we want to put the title on
1864 * @param {object} colDef the colDef from which we can get displayName, name or field
1865 * @param {Grid} grid the grid, from which we can get the options.gridMenuTitleFilter
1868 setMenuItemTitle: function( menuItem, colDef, grid ){
1869 var title = grid.options.gridMenuTitleFilter( colDef.displayName || gridUtil.readableColumnName(colDef.name) || colDef.field );
1871 if ( typeof(title) === 'string' ){
1872 menuItem.title = title;
1873 } else if ( title.then ){
1874 // must be a promise
1875 menuItem.title = "";
1876 title.then( function( successValue ) {
1877 menuItem.title = successValue;
1878 }, function( errorValue ) {
1879 menuItem.title = errorValue;
1882 gridUtil.logError('Expected gridMenuTitleFilter to return a string or a promise, it has returned neither, bad config');
1883 menuItem.title = 'badconfig';
1889 * @methodOf ui.grid.gridMenuService
1890 * @name toggleColumnVisibility
1891 * @description Toggles the visibility of an individual column. Expects to be
1892 * provided a context that has on it a gridColumn, which is the column that
1893 * we'll operate upon. We change the visibility, and refresh the grid as appropriate
1894 * @param {GridCol} gridCol the column that we want to toggle
1897 toggleColumnVisibility: function( gridCol ) {
1898 gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined );
1900 gridCol.grid.refresh();
1901 gridCol.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1902 gridCol.grid.api.core.raise.columnVisibilityChanged( gridCol );
1911 .directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 'i18nService',
1912 function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) {
1917 require: ['^uiGrid'],
1918 templateUrl: 'ui-grid/ui-grid-menu-button',
1921 link: function ($scope, $elm, $attrs, controllers) {
1922 var uiGridCtrl = controllers[0];
1924 // For the aria label
1926 aria: i18nService.getSafeText('gridMenu.aria')
1929 uiGridGridMenuService.initialize($scope, uiGridCtrl.grid);
1931 $scope.shown = false;
1933 $scope.toggleMenu = function () {
1934 if ( $scope.shown ){
1935 $scope.$broadcast('hide-menu');
1936 $scope.shown = false;
1938 $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope );
1939 $scope.$broadcast('show-menu');
1940 $scope.shown = true;
1944 $scope.$on('menu-hidden', function() {
1945 $scope.shown = false;
1946 gridUtil.focus.bySelector($elm, '.ui-grid-icon-container');
1959 * @name ui.grid.directive:uiGridMenu
1964 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
1967 <doc:example module="app">
1970 var app = angular.module('app', ['ui.grid']);
1972 app.controller('MainCtrl', ['$scope', function ($scope) {
1977 <div ng-controller="MainCtrl">
1978 <div ui-grid-menu shown="true" ></div>
1985 angular.module('ui.grid')
1987 .directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'i18nService',
1988 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18nService) {
1996 require: '?^uiGrid',
1997 templateUrl: 'ui-grid/uiGridMenu',
1999 link: function ($scope, $elm, $attrs, uiGridCtrl) {
2005 close: i18nService.getSafeText('columnMenu.close')
2008 // *** Show/Hide functions ******
2009 self.showMenu = $scope.showMenu = function(event, args) {
2010 if ( !$scope.shown ){
2013 * In order to animate cleanly we remove the ng-if, wait a digest cycle, then
2014 * animate the removal of the ng-hide. We can't successfully (so far as I can tell)
2015 * animate removal of the ng-if, as the menu items aren't there yet. And we don't want
2016 * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated
2019 * Note when testing animation that animations don't run on the tutorials. When debugging it looks
2020 * like they do, but angular has a default $animate provider that is just a stub, and that's what's
2021 * being called. ALso don't be fooled by the fact that your browser has actually loaded the
2022 * angular-translate.js, it's not using it. You need to test animations in an external application.
2024 $scope.shown = true;
2026 $timeout( function() {
2027 $scope.shownMid = true;
2028 $scope.$emit('menu-shown');
2030 } else if ( !$scope.shownMid ) {
2031 // we're probably doing a hide then show, so we don't need to wait for ng-if
2032 $scope.shownMid = true;
2033 $scope.$emit('menu-shown');
2036 var docEventType = 'click';
2037 if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') {
2038 docEventType = args.originalEvent.type;
2041 // Turn off an existing document click handler
2042 angular.element(document).off('click touchstart', applyHideMenu);
2044 // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one
2045 $timeout(function() {
2046 angular.element(document).on(docEventType, applyHideMenu);
2048 //automatically set the focus to the first button element in the now open menu.
2049 gridUtil.focus.bySelector($elm, 'button[type=button]', true);
2053 self.hideMenu = $scope.hideMenu = function(event, args) {
2054 if ( $scope.shown ){
2056 * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to
2057 * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the
2058 * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason.
2060 * The user may have clicked on the menu again whilst
2061 * we're waiting, so we check that the mid isn't shown before applying the ng-if.
2063 $scope.shownMid = false;
2064 $timeout( function() {
2065 if ( !$scope.shownMid ){
2066 $scope.shown = false;
2067 $scope.$emit('menu-hidden');
2072 angular.element(document).off('click touchstart', applyHideMenu);
2075 $scope.$on('hide-menu', function (event, args) {
2076 $scope.hideMenu(event, args);
2079 $scope.$on('show-menu', function (event, args) {
2080 $scope.showMenu(event, args);
2084 // *** Auto hide when click elsewhere ******
2085 var applyHideMenu = function(){
2087 $scope.$apply(function () {
2093 if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) {
2094 $scope.autoHide = true;
2097 if ($scope.autoHide) {
2098 angular.element($window).on('resize', applyHideMenu);
2101 $scope.$on('$destroy', function () {
2102 angular.element(document).off('click touchstart', applyHideMenu);
2106 $scope.$on('$destroy', function() {
2107 angular.element($window).off('resize', applyHideMenu);
2111 $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollBegin($scope, applyHideMenu ));
2114 $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu ));
2118 controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
2126 .directive('uiGridMenuItem', ['gridUtil', '$compile', 'i18nService', function (gridUtil, $compile, i18nService) {
2127 var uiGridMenuItem = {
2138 screenReaderOnly: '='
2140 require: ['?^uiGrid', '^uiGridMenu'],
2141 templateUrl: 'ui-grid/uiGridMenuItem',
2143 compile: function($elm, $attrs) {
2145 pre: function ($scope, $elm, $attrs, controllers) {
2146 var uiGridCtrl = controllers[0],
2147 uiGridMenuCtrl = controllers[1];
2149 if ($scope.templateUrl) {
2150 gridUtil.getTemplate($scope.templateUrl)
2151 .then(function (contents) {
2152 var template = angular.element(contents);
2154 var newElm = $compile(template)($scope);
2155 $elm.replaceWith(newElm);
2159 post: function ($scope, $elm, $attrs, controllers) {
2160 var uiGridCtrl = controllers[0],
2161 uiGridMenuCtrl = controllers[1];
2163 // TODO(c0bra): validate that shown and active are functions if they're defined. An exception is already thrown above this though
2164 // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') {
2165 // throw new TypeError("$scope.shown is defined but not a function");
2167 if (typeof($scope.shown) === 'undefined' || $scope.shown === null) {
2168 $scope.shown = function() { return true; };
2171 $scope.itemShown = function () {
2173 if ($scope.context) {
2174 context.context = $scope.context;
2177 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2178 context.grid = uiGridCtrl.grid;
2181 return $scope.shown.call(context);
2184 $scope.itemAction = function($event,title) {
2185 gridUtil.logDebug('itemAction');
2186 $event.stopPropagation();
2188 if (typeof($scope.action) === 'function') {
2191 if ($scope.context) {
2192 context.context = $scope.context;
2195 // Add the grid to the function call context if the uiGrid controller is present
2196 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2197 context.grid = uiGridCtrl.grid;
2200 $scope.action.call(context, $event, title);
2202 if ( !$scope.leaveOpen ){
2203 $scope.$emit('hide-menu');
2206 * XXX: Fix after column refactor
2207 * Ideally the focus would remain on the item.
2208 * However, since there are two menu items that have their 'show' property toggled instead. This is a quick fix.
2210 gridUtil.focus.bySelector(angular.element(gridUtil.closestElm($elm, ".ui-grid-menu-items")), 'button[type=button]', true);
2215 $scope.i18n = i18nService.get();
2221 return uiGridMenuItem;
2230 * @name ui.grid.directive:uiGridOneBind
2231 * @summary A group of directives that provide a one time bind to a dom element.
2232 * @description A group of directives that provide a one time bind to a dom element.
2233 * As one time bindings are not supported in Angular 1.2.* this directive provdes this capability.
2234 * This is done to reduce the number of watchers on the dom.
2236 * <h2>Short Example ({@link ui.grid.directive:uiGridOneBindSrc ui-grid-one-bind-src})</h2>
2238 <div ng-init="imageName = 'myImageDir.jpg'">
2239 <img ui-grid-one-bind-src="imageName"></img>
2244 <div ng-init="imageName = 'myImageDir.jpg'">
2245 <img ui-grid-one-bind-src="imageName" src="myImageDir.jpg"></img>
2249 <h2>Short Example ({@link ui.grid.directive:uiGridOneBindText ui-grid-one-bind-text})</h2>
2251 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text"></div>
2255 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text">Add this text</div>
2258 * <b>Note:</b> This behavior is slightly different for the {@link ui.grid.directive:uiGridOneBindIdGrid uiGridOneBindIdGrid}
2259 * and {@link ui.grid.directive:uiGridOneBindAriaLabelledbyGrid uiGridOneBindAriaLabelledbyGrid} directives.
2262 //https://github.com/joshkurz/Black-Belt-AngularJS-Directives/blob/master/directives/Optimization/oneBind.js
2263 var oneBinders = angular.module('ui.grid');
2267 * @name ui.grid.directive:uiGridOneBindSrc
2268 * @memberof ui.grid.directive:uiGridOneBind
2271 * @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>.
2272 * @description One time binding for the src dom tag.
2275 {tag: 'Src', method: 'attr'},
2278 * @name ui.grid.directive:uiGridOneBindText
2281 * @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>.
2282 * @description One time binding for the text dom tag.
2284 {tag: 'Text', method: 'text'},
2287 * @name ui.grid.directive:uiGridOneBindHref
2290 * @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>.
2291 * @description One time binding for the href dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2293 {tag: 'Href', method: 'attr'},
2296 * @name ui.grid.directive:uiGridOneBindClass
2299 * @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>.
2300 * @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.
2301 * this is to prevent the watcher from being removed before the scope is initialized.
2302 * @param {Array} uiGridOneBindClass An array of classes to bind to this element.
2303 * @description One time binding for the class dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2305 {tag: 'Class', method: 'addClass'},
2308 * @name ui.grid.directive:uiGridOneBindHtml
2311 * @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>.
2312 * @description One time binding for the html method on a dom element. For more information see {@link ui.grid.directive:uiGridOneBind}.
2314 {tag: 'Html', method: 'html'},
2317 * @name ui.grid.directive:uiGridOneBindAlt
2320 * @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>.
2321 * @description One time binding for the alt dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2323 {tag: 'Alt', method: 'attr'},
2326 * @name ui.grid.directive:uiGridOneBindStyle
2329 * @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>.
2330 * @description One time binding for the style dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2332 {tag: 'Style', method: 'css'},
2335 * @name ui.grid.directive:uiGridOneBindValue
2338 * @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>.
2339 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2341 {tag: 'Value', method: 'attr'},
2344 * @name ui.grid.directive:uiGridOneBindId
2347 * @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>.
2348 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2350 {tag: 'Id', method: 'attr'},
2353 * @name ui.grid.directive:uiGridOneBindIdGrid
2356 * @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>.
2357 * @description One time binding for the id dom tag.
2358 * <h1>Important Note!</h1>
2359 * If the id tag passed as a parameter does <b>not</b> contain the grid id as a substring
2360 * then the directive will search the scope and the parent controller (if it is a uiGridController) for the grid.id value.
2361 * 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.
2362 * This is done in order to ensure uniqueness of id tags across the grid.
2363 * This is to prevent two grids in the same document having duplicate id tags.
2365 {tag: 'Id', directiveName:'IdGrid', method: 'attr', appendGridId: true},
2368 * @name ui.grid.directive:uiGridOneBindTitle
2371 * @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>.
2372 * @description One time binding for the title dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2374 {tag: 'Title', method: 'attr'},
2377 * @name ui.grid.directive:uiGridOneBindAriaLabel
2380 * @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>.
2381 * @description One time binding for the aria-label dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2384 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text"></div>
2388 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text" aria-label="Add this text"></div>
2391 {tag: 'Label', method: 'attr', aria:true},
2394 * @name ui.grid.directive:uiGridOneBindAriaLabelledby
2397 * @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>.
2398 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2401 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId"></div>
2405 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId" aria-labelledby="gridID32"></div>
2408 {tag: 'Labelledby', method: 'attr', aria:true},
2411 * @name ui.grid.directive:uiGridOneBindAriaLabelledbyGrid
2414 * @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>.
2415 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2416 * 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
2417 * grid id to each one.
2420 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId"></div>
2422 * Will become ([grid.id] will be replaced by the actual grid id):
2424 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId" aria-labelledby-Grid="[grid.id]-gridID32"></div>
2427 {tag: 'Labelledby', directiveName:'LabelledbyGrid', appendGridId:true, method: 'attr', aria:true},
2430 * @name ui.grid.directive:uiGridOneBindAriaDescribedby
2433 * @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>.
2434 * @description One time binding for the aria-describedby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2437 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId"></div>
2441 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId" aria-describedby="gridID32"></div>
2444 {tag: 'Describedby', method: 'attr', aria:true},
2447 * @name ui.grid.directive:uiGridOneBindAriaDescribedbyGrid
2450 * @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>.
2451 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2452 * 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
2453 * grid id to each one.
2456 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId"></div>
2458 * Will become ([grid.id] will be replaced by the actual grid id):
2460 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId" aria-describedby="[grid.id]-gridID32"></div>
2463 {tag: 'Describedby', directiveName:'DescribedbyGrid', appendGridId:true, method: 'attr', aria:true}],
2466 var baseDirectiveName = 'uiGridOneBind';
2467 //If it is an aria tag then append the aria label seperately
2468 //This is done because the aria tags are formatted aria-* and the directive name can't have a '-' character in it.
2469 //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.
2470 var directiveName = (v.aria ? baseDirectiveName + 'Aria' : baseDirectiveName) + (v.directiveName ? v.directiveName : v.tag);
2471 oneBinders.directive(directiveName, ['gridUtil', function(gridUtil){
2474 require: ['?uiGrid','?^uiGrid'],
2475 link: function(scope, iElement, iAttrs, controllers){
2476 /* Appends the grid id to the beginnig of the value. */
2477 var appendGridId = function(val){
2478 var grid; //Get an instance of the grid if its available
2479 //If its available in the scope then we don't need to try to find it elsewhere
2483 //Another possible location to try to find the grid
2484 else if (scope.col && scope.col.grid){
2485 grid = scope.col.grid;
2487 //Last ditch effort: Search through the provided controllers.
2488 else if (!controllers.some( //Go through the controllers till one has the element we need
2489 function(controller){
2490 if (controller && controller.grid) {
2491 grid = controller.grid;
2492 return true; //We've found the grid
2495 //We tried our best to find it for you
2496 gridUtil.logError("["+directiveName+"] A valid grid could not be found to bind id. Are you using this directive " +
2497 "within the correct scope? Trying to generate id: [gridID]-" + val);
2498 throw new Error("No valid grid could be found");
2502 var idRegex = new RegExp(grid.id.toString());
2503 //If the grid id hasn't been appended already in the template declaration
2504 if (!idRegex.test(val)){
2505 val = grid.id.toString() + '-' + val;
2511 // The watch returns a function to remove itself.
2512 var rmWatcher = scope.$watch(iAttrs[directiveName], function(newV){
2514 //If we are trying to add an id element then we also apply the grid id if it isn't already there
2515 if (v.appendGridId) {
2516 var newIdString = null;
2517 //Append the id to all of the new ids.
2518 angular.forEach( newV.split(' '), function(s){
2519 newIdString = (newIdString ? (newIdString + ' ') : '') + appendGridId(s);
2524 // Append this newValue to the dom element.
2526 case 'attr': //The attr method takes two paraams the tag and the value
2528 //If it is an aria element then append the aria prefix
2529 iElement[v.method]('aria-' + v.tag.toLowerCase(),newV);
2531 iElement[v.method](v.tag.toLowerCase(),newV);
2535 //Pulled from https://github.com/Pasvaz/bindonce/blob/master/bindonce.js
2536 if (angular.isObject(newV) && !angular.isArray(newV)) {
2538 var nonNullFound = false; //We don't want to remove the binding unless the key is actually defined
2539 angular.forEach(newV, function (value, index) {
2540 if (value !== null && typeof(value) !== "undefined"){
2541 nonNullFound = true; //A non null value for a key was found so the object must have been initialized
2542 if (value) {results.push(index);}
2545 //A non null value for a key wasn't found so assume that the scope values haven't been fully initialized
2547 return; // If not initialized then the watcher should not be removed yet.
2553 iElement.addClass(angular.isArray(newV) ? newV.join(' ') : newV);
2559 iElement[v.method](newV);
2563 //Removes the watcher on itself after the bind
2566 // True ensures that equality is determined using angular.equals instead of ===
2567 }, true); //End rm watchers
2568 } //End compile function
2569 }; //End directive return
2570 } // End directive function
2572 }); // End angular foreach
2578 var module = angular.module('ui.grid');
2580 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'uiGridConstants', 'gridUtil', 'ScrollEvent',
2581 function($timeout, $document, uiGridConstants, gridUtil, ScrollEvent) {
2585 templateUrl: 'ui-grid/uiGridRenderContainer',
2586 require: ['^uiGrid', 'uiGridRenderContainer'],
2589 rowContainerName: '=',
2590 colContainerName: '=',
2591 bindScrollHorizontal: '=',
2592 bindScrollVertical: '=',
2593 enableVerticalScrollbar: '=',
2594 enableHorizontalScrollbar: '='
2596 controller: 'uiGridRenderContainer as RenderContainer',
2597 compile: function () {
2599 pre: function prelink($scope, $elm, $attrs, controllers) {
2601 var uiGridCtrl = controllers[0];
2602 var containerCtrl = controllers[1];
2603 var grid = $scope.grid = uiGridCtrl.grid;
2605 // Verify that the render container for this element exists
2606 if (!$scope.rowContainerName) {
2607 throw "No row render container name specified";
2609 if (!$scope.colContainerName) {
2610 throw "No column render container name specified";
2613 if (!grid.renderContainers[$scope.rowContainerName]) {
2614 throw "Row render container '" + $scope.rowContainerName + "' is not registered.";
2616 if (!grid.renderContainers[$scope.colContainerName]) {
2617 throw "Column render container '" + $scope.colContainerName + "' is not registered.";
2620 var rowContainer = $scope.rowContainer = grid.renderContainers[$scope.rowContainerName];
2621 var colContainer = $scope.colContainer = grid.renderContainers[$scope.colContainerName];
2623 containerCtrl.containerId = $scope.containerId;
2624 containerCtrl.rowContainer = rowContainer;
2625 containerCtrl.colContainer = colContainer;
2627 post: function postlink($scope, $elm, $attrs, controllers) {
2629 var uiGridCtrl = controllers[0];
2630 var containerCtrl = controllers[1];
2632 var grid = uiGridCtrl.grid;
2633 var rowContainer = containerCtrl.rowContainer;
2634 var colContainer = containerCtrl.colContainer;
2635 var scrollTop = null;
2636 var scrollLeft = null;
2639 var renderContainer = grid.renderContainers[$scope.containerId];
2641 // Put the container name on this element as a class
2642 $elm.addClass('ui-grid-render-container-' + $scope.containerId);
2644 // Scroll the render container viewport when the mousewheel is used
2645 gridUtil.on.mousewheel($elm, function (event) {
2646 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.RenderContainerMouseWheel);
2647 if (event.deltaY !== 0) {
2648 var scrollYAmount = event.deltaY * -1 * event.deltaFactor;
2650 scrollTop = containerCtrl.viewport[0].scrollTop;
2652 // Get the scroll percentage
2653 scrollEvent.verticalScrollLength = rowContainer.getVerticalScrollLength();
2654 var scrollYPercentage = (scrollTop + scrollYAmount) / scrollEvent.verticalScrollLength;
2656 // If we should be scrolled 100%, make sure the scrollTop matches the maximum scroll length
2657 // Viewports that have "overflow: hidden" don't let the mousewheel scroll all the way to the bottom without this check
2658 if (scrollYPercentage >= 1 && scrollTop < scrollEvent.verticalScrollLength) {
2659 containerCtrl.viewport[0].scrollTop = scrollEvent.verticalScrollLength;
2662 // Keep scrollPercentage within the range 0-1.
2663 if (scrollYPercentage < 0) { scrollYPercentage = 0; }
2664 else if (scrollYPercentage > 1) { scrollYPercentage = 1; }
2666 scrollEvent.y = { percentage: scrollYPercentage, pixels: scrollYAmount };
2668 if (event.deltaX !== 0) {
2669 var scrollXAmount = event.deltaX * event.deltaFactor;
2671 // Get the scroll percentage
2672 scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport, grid);
2673 scrollEvent.horizontalScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
2674 var scrollXPercentage = (scrollLeft + scrollXAmount) / scrollEvent.horizontalScrollLength;
2676 // Keep scrollPercentage within the range 0-1.
2677 if (scrollXPercentage < 0) { scrollXPercentage = 0; }
2678 else if (scrollXPercentage > 1) { scrollXPercentage = 1; }
2680 scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount };
2683 // Let the parent container scroll if the grid is already at the top/bottom
2684 if ((event.deltaY !== 0 && (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop))) ||
2685 (event.deltaX !== 0 && (scrollEvent.atLeft(scrollLeft) || scrollEvent.atRight(scrollLeft)))) {
2686 //parent controller scrolls
2689 event.preventDefault();
2690 event.stopPropagation();
2691 scrollEvent.fireThrottledScrollingEvent('', scrollEvent);
2696 $elm.bind('$destroy', function() {
2697 $elm.unbind('keydown');
2699 ['touchstart', 'touchmove', 'touchend','keydown', 'wheel', 'mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'].forEach(function (eventName) {
2700 $elm.unbind(eventName);
2704 // TODO(c0bra): Handle resizing the inner canvas based on the number of elements
2708 var canvasWidth = colContainer.canvasWidth;
2709 var viewportWidth = colContainer.getViewportWidth();
2711 var canvasHeight = rowContainer.getCanvasHeight();
2713 //add additional height for scrollbar on left and right container
2714 //if ($scope.containerId !== 'body') {
2715 // canvasHeight -= grid.scrollbarHeight;
2718 var viewportHeight = rowContainer.getViewportHeight();
2719 //shorten the height to make room for a scrollbar placeholder
2720 if (colContainer.needsHScrollbarPlaceholder()) {
2721 viewportHeight -= grid.scrollbarHeight;
2724 var headerViewportWidth,
2725 footerViewportWidth;
2726 headerViewportWidth = footerViewportWidth = colContainer.getHeaderViewportWidth();
2728 // Set canvas dimensions
2729 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }';
2731 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2733 if (renderContainer.explicitHeaderCanvasHeight) {
2734 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }';
2737 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: inherit; }';
2740 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }';
2741 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }';
2743 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2744 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-viewport { width: ' + footerViewportWidth + 'px; }';
2749 uiGridCtrl.grid.registerStyleComputation({
2760 module.controller('uiGridRenderContainer', ['$scope', 'gridUtil', function ($scope, gridUtil) {
2769 angular.module('ui.grid').directive('uiGridRow', ['gridUtil', function(gridUtil) {
2773 // templateUrl: 'ui-grid/ui-grid-row',
2774 require: ['^uiGrid', '^uiGridRenderContainer'],
2777 //rowRenderIndex is added to scope to give the true visual index of the row to any directives that need it
2780 compile: function() {
2782 pre: function($scope, $elm, $attrs, controllers) {
2783 var uiGridCtrl = controllers[0];
2784 var containerCtrl = controllers[1];
2786 var grid = uiGridCtrl.grid;
2788 $scope.grid = uiGridCtrl.grid;
2789 $scope.colContainer = containerCtrl.colContainer;
2791 // Function for attaching the template to this scope
2792 var clonedElement, cloneScope;
2793 function compileTemplate() {
2794 $scope.row.getRowTemplateFn.then(function (compiledElementFn) {
2795 // var compiledElementFn = $scope.row.compiledElementFn;
2797 // Create a new scope for the contents of this row, so we can destroy it later if need be
2798 var newScope = $scope.$new();
2800 compiledElementFn(newScope, function (newElm, scope) {
2801 // If we already have a cloned element, we need to remove it and destroy its scope
2802 if (clonedElement) {
2803 clonedElement.remove();
2804 cloneScope.$destroy();
2807 // Empty the row and append the new element
2808 $elm.empty().append(newElm);
2810 // Save the new cloned element and scope
2811 clonedElement = newElm;
2812 cloneScope = newScope;
2817 // Initially attach the compiled template to this scope
2820 // If the row's compiled element function changes, we need to replace this element's contents with the new compiled template
2821 $scope.$watch('row.getRowTemplateFn', function (newFunc, oldFunc) {
2822 if (newFunc !== oldFunc) {
2827 post: function($scope, $elm, $attrs, controllers) {
2841 * @name ui.grid.directive:uiGridStyle
2846 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
2849 <doc:example module="app">
2852 var app = angular.module('app', ['ui.grid']);
2854 app.controller('MainCtrl', ['$scope', function ($scope) {
2855 $scope.myStyle = '.blah { border: 1px solid }';
2859 <div ng-controller="MainCtrl">
2860 <style ui-grid-style>{{ myStyle }}</style>
2861 <span class="blah">I am in a box.</span>
2865 it('should apply the right class to the element', function () {
2866 element(by.css('.blah')).getCssValue('border-top-width')
2868 expect(c).toContain('1px');
2876 angular.module('ui.grid').directive('uiGridStyle', ['gridUtil', '$interpolate', function(gridUtil, $interpolate) {
2880 // require: '?^uiGrid',
2881 link: function($scope, $elm, $attrs, uiGridCtrl) {
2882 // gridUtil.logDebug('ui-grid-style link');
2883 // if (uiGridCtrl === undefined) {
2884 // gridUtil.logWarn('[ui-grid-style link] uiGridCtrl is undefined!');
2887 var interpolateFn = $interpolate($elm.text(), true);
2889 if (interpolateFn) {
2890 $scope.$watch(interpolateFn, function(value) {
2895 // uiGridCtrl.recalcRowStyles = function() {
2896 // var offset = (scope.options.offsetTop || 0) - (scope.options.excessRows * scope.options.rowHeight);
2897 // var rowHeight = scope.options.rowHeight;
2900 // var rowStyleCount = uiGridCtrl.minRowsToRender() + (scope.options.excessRows * 2);
2901 // for (var i = 1; i <= rowStyleCount; i++) {
2902 // ret = ret + ' .grid' + scope.gridId + ' .ui-grid-row:nth-child(' + i + ') { top: ' + offset + 'px; }';
2903 // offset = offset + rowHeight;
2906 // scope.rowStyles = ret;
2909 // uiGridCtrl.styleComputions.push(uiGridCtrl.recalcRowStyles);
2920 angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', '$log',
2921 function(gridUtil, ScrollEvent, uiGridConstants, $log) {
2925 controllerAs: 'Viewport',
2926 templateUrl: 'ui-grid/uiGridViewport',
2927 require: ['^uiGrid', '^uiGridRenderContainer'],
2928 link: function($scope, $elm, $attrs, controllers) {
2929 // gridUtil.logDebug('viewport post-link');
2931 var uiGridCtrl = controllers[0];
2932 var containerCtrl = controllers[1];
2934 $scope.containerCtrl = containerCtrl;
2936 var rowContainer = containerCtrl.rowContainer;
2937 var colContainer = containerCtrl.colContainer;
2939 var grid = uiGridCtrl.grid;
2941 $scope.grid = uiGridCtrl.grid;
2943 // Put the containers in scope so we can get rows and columns from them
2944 $scope.rowContainer = containerCtrl.rowContainer;
2945 $scope.colContainer = containerCtrl.colContainer;
2947 // Register this viewport with its container
2948 containerCtrl.viewport = $elm;
2951 $elm.on('scroll', scrollHandler);
2953 var ignoreScroll = false;
2955 function scrollHandler(evt) {
2956 //Leaving in this commented code in case it can someday be used
2957 //It does improve performance, but because the horizontal scroll is normalized,
2958 // using this code will lead to the column header getting slightly out of line with columns
2960 //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) {
2961 // //don't ask for scrollTop if we just set it
2962 // ignoreScroll = false;
2965 //ignoreScroll = true;
2967 var newScrollTop = $elm[0].scrollTop;
2968 var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid);
2970 var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop);
2971 var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft);
2973 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll);
2974 scrollEvent.newScrollLeft = newScrollLeft;
2975 scrollEvent.newScrollTop = newScrollTop;
2976 if ( horizScrollPercentage > -1 ){
2977 scrollEvent.x = { percentage: horizScrollPercentage };
2980 if ( vertScrollPercentage > -1 ){
2981 scrollEvent.y = { percentage: vertScrollPercentage };
2984 grid.scrollContainers($scope.$parent.containerId, scrollEvent);
2987 if ($scope.$parent.bindScrollVertical) {
2988 grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll);
2991 if ($scope.$parent.bindScrollHorizontal) {
2992 grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll);
2993 grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader);
2994 grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter);
2997 function syncVerticalScroll(scrollEvent){
2998 containerCtrl.prevScrollArgs = scrollEvent;
2999 var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport);
3000 $elm[0].scrollTop = newScrollTop;
3004 function syncHorizontalScroll(scrollEvent){
3005 containerCtrl.prevScrollArgs = scrollEvent;
3006 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3007 $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3010 function syncHorizontalHeader(scrollEvent){
3011 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3012 if (containerCtrl.headerViewport) {
3013 containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3017 function syncHorizontalFooter(scrollEvent){
3018 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3019 if (containerCtrl.footerViewport) {
3020 containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3026 controller: ['$scope', function ($scope) {
3027 this.rowStyle = function (index) {
3028 var rowContainer = $scope.rowContainer;
3029 var colContainer = $scope.colContainer;
3033 if (index === 0 && rowContainer.currentTopRow !== 0) {
3034 // The row offset-top is just the height of the rows above the current top-most row, which are no longer rendered
3035 var hiddenRowWidth = (rowContainer.currentTopRow) * rowContainer.grid.options.rowHeight;
3037 // return { 'margin-top': hiddenRowWidth + 'px' };
3038 styles['margin-top'] = hiddenRowWidth + 'px';
3041 if (colContainer.currentFirstColumn !== 0) {
3042 if (colContainer.grid.isRTL()) {
3043 styles['margin-right'] = colContainer.columnOffset + 'px';
3046 styles['margin-left'] = colContainer.columnOffset + 'px';
3061 angular.module('ui.grid')
3062 .directive('uiGridVisible', function uiGridVisibleAction() {
3063 return function ($scope, $elm, $attr) {
3064 $scope.$watch($attr.uiGridVisible, function (visible) {
3065 // $elm.css('visibility', visible ? 'visible' : 'hidden');
3066 $elm[visible ? 'removeClass' : 'addClass']('ui-grid-invisible');
3075 angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants',
3076 '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile',
3077 function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants,
3078 $templateCache, gridClassFactory, $timeout, $parse, $compile) {
3079 // gridUtil.logDebug('ui-grid controller');
3083 self.grid = gridClassFactory.createGrid($scope.uiGrid);
3085 //assign $scope.$parent if appScope not already assigned
3086 self.grid.appScope = self.grid.appScope || $scope.$parent;
3088 $elm.addClass('grid' + self.grid.id);
3089 self.grid.rtl = gridUtil.getStyles($elm[0])['direction'] === 'rtl';
3092 // angular.extend(self.grid.options, );
3094 //all properties of grid are available on scope
3095 $scope.grid = self.grid;
3097 if ($attrs.uiGridColumns) {
3098 $attrs.$observe('uiGridColumns', function(value) {
3099 self.grid.options.columnDefs = value;
3100 self.grid.buildColumns()
3102 self.grid.preCompileCellTemplates();
3104 self.grid.refreshCanvas(true);
3110 // if fastWatch is set we watch only the length and the reference, not every individual object
3111 var deregFunctions = [];
3112 if (self.grid.options.fastWatch) {
3113 self.uiGrid = $scope.uiGrid;
3114 if (angular.isString($scope.uiGrid.data)) {
3115 deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) );
3116 deregFunctions.push( $scope.$parent.$watch(function() {
3117 if ( self.grid.appScope[$scope.uiGrid.data] ){
3118 return self.grid.appScope[$scope.uiGrid.data].length;
3122 }, dataWatchFunction) );
3124 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3125 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, dataWatchFunction) );
3127 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3128 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, columnDefsWatchFunction) );
3130 if (angular.isString($scope.uiGrid.data)) {
3131 deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) );
3133 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3135 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3139 function columnDefsWatchFunction(n, o) {
3141 self.grid.options.columnDefs = n;
3142 self.grid.buildColumns({ orderByColumnDefs: true })
3145 self.grid.preCompileCellTemplates();
3147 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.COLUMN);
3152 function dataWatchFunction(newData) {
3153 // gridUtil.logDebug('dataWatch fired');
3156 if ( self.grid.options.fastWatch ){
3157 if (angular.isString($scope.uiGrid.data)) {
3158 newData = self.grid.appScope[$scope.uiGrid.data];
3160 newData = $scope.uiGrid.data;
3165 // columns length is greater than the number of row header columns, which don't count because they're created automatically
3166 var hasColumns = self.grid.columns.length > (self.grid.rowHeaderColumns ? self.grid.rowHeaderColumns.length : 0);
3169 // If we have no columns
3171 // ... and we don't have a ui-grid-columns attribute, which would define columns for us
3172 !$attrs.uiGridColumns &&
3173 // ... and we have no pre-defined columns
3174 self.grid.options.columnDefs.length === 0 &&
3175 // ... but we DO have data
3178 // ... then build the column definitions from the data that we have
3179 self.grid.buildColumnDefsFromData(newData);
3182 // If we haven't built columns before and either have some columns defined or some data defined
3183 if (!hasColumns && (self.grid.options.columnDefs.length > 0 || newData.length > 0)) {
3184 // Build the column set, then pre-compile the column cell templates
3185 promises.push(self.grid.buildColumns()
3187 self.grid.preCompileCellTemplates();
3191 $q.all(promises).then(function() {
3192 self.grid.modifyRows(newData)
3194 // if (self.viewport) {
3195 self.grid.redrawInPlace(true);
3198 $scope.$evalAsync(function() {
3199 self.grid.refreshCanvas(true);
3200 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW);
3207 var styleWatchDereg = $scope.$watch(function () { return self.grid.styleComputations; }, function() {
3208 self.grid.refreshCanvas(true);
3211 $scope.$on('$destroy', function() {
3212 deregFunctions.forEach( function( deregFn ){ deregFn(); });
3216 self.fireEvent = function(eventName, args) {
3217 // Add the grid to the event arguments if it's not there
3218 if (typeof(args) === 'undefined' || args === undefined) {
3222 if (typeof(args.grid) === 'undefined' || args.grid === undefined) {
3223 args.grid = self.grid;
3226 $scope.$broadcast(eventName, args);
3229 self.innerCompile = function innerCompile(elm) {
3230 $compile(elm)($scope);
3237 * @name ui.grid.directive:uiGrid
3240 * @param {Object} uiGrid Options for the grid to use
3242 * @description Create a very basic grid.
3245 <example module="app">
3246 <file name="app.js">
3247 var app = angular.module('app', ['ui.grid']);
3249 app.controller('MainCtrl', ['$scope', function ($scope) {
3251 { name: 'Bob', title: 'CEO' },
3252 { name: 'Frank', title: 'Lowly Developer' }
3256 <file name="index.html">
3257 <div ng-controller="MainCtrl">
3258 <div ui-grid="{ data: data }"></div>
3263 angular.module('ui.grid').directive('uiGrid', uiGridDirective);
3265 uiGridDirective.$inject = ['$compile', '$templateCache', '$timeout', '$window', 'gridUtil', 'uiGridConstants'];
3266 function uiGridDirective($compile, $templateCache, $timeout, $window, gridUtil, uiGridConstants) {
3268 templateUrl: 'ui-grid/ui-grid',
3274 controller: 'uiGridController',
3275 compile: function () {
3277 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3278 var grid = uiGridCtrl.grid;
3279 // Initialize scrollbars (TODO: move to controller??)
3280 uiGridCtrl.scrollbars = [];
3281 grid.element = $elm;
3284 // See if the grid has a rendered width, if not, wait a bit and try again
3285 var sizeCheckInterval = 100; // ms
3286 var maxSizeChecks = 20; // 2 seconds total
3289 // Setup (event listeners) the grid
3292 // And initialize it
3295 // Mark rendering complete so API events can happen
3296 grid.renderingComplete();
3298 // If the grid doesn't have size currently, wait for a bit to see if it gets size
3303 function checkSize() {
3304 // If the grid has no width and we haven't checked more than <maxSizeChecks> times, check again in <sizeCheckInterval> milliseconds
3305 if ($elm[0].offsetWidth <= 0 && sizeChecks < maxSizeChecks) {
3306 setTimeout(checkSize, sizeCheckInterval);
3314 // Setup event listeners and watchers
3316 // Bind to window resize events
3317 angular.element($window).on('resize', gridResize);
3319 // Unbind from window resize events when the grid is destroyed
3320 $elm.on('$destroy', function () {
3321 angular.element($window).off('resize', gridResize);
3324 // If we add a left container after render, we need to watch and react
3325 $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) {
3326 if (newValue === oldValue) {
3329 grid.refreshCanvas(true);
3332 // If we add a right container after render, we need to watch and react
3333 $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) {
3334 if (newValue === oldValue) {
3337 grid.refreshCanvas(true);
3341 // Initialize the directive
3343 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3345 // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from
3346 grid.canvasWidth = uiGridCtrl.grid.gridWidth;
3348 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3350 // 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
3351 if (grid.gridHeight < grid.options.rowHeight && grid.options.enableMinHeightCheck) {
3355 // Run initial canvas refresh
3356 grid.refreshCanvas(true);
3359 // Set the grid's height ourselves in the case that its height would be unusably small
3360 function autoAdjustHeight() {
3361 // Figure out the new height
3362 var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight;
3363 var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0;
3364 var footerHeight = grid.calcFooterHeight();
3366 var scrollbarHeight = 0;
3367 if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3368 scrollbarHeight = gridUtil.getScrollbarWidth();
3371 var maxNumberOfFilters = 0;
3372 // Calculates the maximum number of filters in the columns
3373 angular.forEach(grid.options.columnDefs, function(col) {
3374 if (col.hasOwnProperty('filter')) {
3375 if (maxNumberOfFilters < 1) {
3376 maxNumberOfFilters = 1;
3379 else if (col.hasOwnProperty('filters')) {
3380 if (maxNumberOfFilters < col.filters.length) {
3381 maxNumberOfFilters = col.filters.length;
3386 if (grid.options.enableFiltering) {
3387 var allColumnsHaveFilteringTurnedOff = grid.options.columnDefs.every(function(col) {
3388 return col.enableFiltering === false;
3391 if (!allColumnsHaveFilteringTurnedOff) {
3392 maxNumberOfFilters++;
3396 var filterHeight = maxNumberOfFilters * headerHeight;
3398 var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight;
3400 $elm.css('height', newHeight + 'px');
3402 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3405 // Resize the grid on window resize events
3406 function gridResize($event) {
3407 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3408 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3410 grid.refreshCanvas(true);
3423 // TODO: rename this file to ui-grid-pinned-container.js
3425 angular.module('ui.grid').directive('uiGridPinnedContainer', ['gridUtil', function (gridUtil) {
3429 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>',
3431 side: '=uiGridPinnedContainer'
3434 compile: function compile() {
3436 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3437 // gridUtil.logDebug('ui-grid-pinned-container ' + $scope.side + ' link');
3439 var grid = uiGridCtrl.grid;
3443 $elm.addClass('ui-grid-pinned-container-' + $scope.side);
3445 // Monkey-patch the viewport width function
3446 if ($scope.side === 'left' || $scope.side === 'right') {
3447 grid.renderContainers[$scope.side].getViewportWidth = monkeyPatchedGetViewportWidth;
3450 function monkeyPatchedGetViewportWidth() {
3451 /*jshint validthis: true */
3454 var viewportWidth = 0;
3455 self.visibleColumnCache.forEach(function (column) {
3456 viewportWidth += column.drawnWidth;
3459 var adjustment = self.getViewportAdjustment();
3461 viewportWidth = viewportWidth + adjustment.width;
3463 return viewportWidth;
3466 function updateContainerWidth() {
3467 if ($scope.side === 'left' || $scope.side === 'right') {
3468 var cols = grid.renderContainers[$scope.side].visibleColumnCache;
3470 for (var i = 0; i < cols.length; i++) {
3472 width += col.drawnWidth || col.width || 0;
3479 function updateContainerDimensions() {
3482 // Column containers
3483 if ($scope.side === 'left' || $scope.side === 'right') {
3484 myWidth = updateContainerWidth();
3486 // gridUtil.logDebug('myWidth', myWidth);
3488 // TODO(c0bra): Subtract sum of col widths from grid viewport width and update it
3489 $elm.attr('style', null);
3491 // var myHeight = grid.renderContainers.body.getViewportHeight(); // + grid.horizontalScrollbarHeight;
3493 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; } ';
3499 grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
3500 myWidth = updateContainerWidth();
3502 // Subtract our own width
3503 adjustment.width -= myWidth;
3504 adjustment.side = $scope.side;
3509 // Register style computation to adjust for columns in `side`'s render container
3510 grid.registerStyleComputation({
3512 func: updateContainerDimensions
3523 angular.module('ui.grid')
3524 .factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout','ScrollEvent',
3525 function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout, ScrollEvent) {
3529 * @name ui.grid.core.api:PublicApi
3530 * @description Public Api for the core grid features
3536 * @name ui.grid.class:Grid
3537 * @description Grid is the main viewModel. Any properties or methods needed to maintain state are defined in
3538 * this prototype. One instance of Grid is created per Grid directive instance.
3539 * @param {object} options Object map of options to pass into the grid. An 'id' property is expected.
3541 var Grid = function Grid(options) {
3543 // Get the id out of the options, then remove it
3544 if (options !== undefined && typeof(options.id) !== 'undefined' && options.id) {
3545 if (!/^[_a-zA-Z0-9-]+$/.test(options.id)) {
3546 throw new Error("Grid id '" + options.id + '" is invalid. It must follow CSS selector syntax rules.');
3550 throw new Error('No ID provided. An ID must be given when creating a grid.');
3553 self.id = options.id;
3556 // Get default options
3557 self.options = GridOptions.initialize( options );
3562 * @propertyOf ui.grid.class:Grid
3563 * @description reference to the application scope (the parent scope of the ui-grid element). Assigned in ui-grid controller
3565 * use gridOptions.appScopeProvider to override the default assignment of $scope.$parent with any reference
3567 self.appScope = self.options.appScopeProvider;
3569 self.headerHeight = self.options.headerRowHeight;
3574 * @name footerHeight
3575 * @propertyOf ui.grid.class:Grid
3576 * @description returns the total footer height gridFooter + columnFooter
3578 self.footerHeight = self.calcFooterHeight();
3583 * @name columnFooterHeight
3584 * @propertyOf ui.grid.class:Grid
3585 * @description returns the total column footer height
3587 self.columnFooterHeight = self.calcColumnFooterHeight();
3590 self.gridHeight = 0;
3592 self.columnBuilders = [];
3593 self.rowBuilders = [];
3594 self.rowsProcessors = [];
3595 self.columnsProcessors = [];
3596 self.styleComputations = [];
3597 self.viewportAdjusters = [];
3598 self.rowHeaderColumns = [];
3599 self.dataChangeCallbacks = {};
3600 self.verticalScrollSyncCallBackFns = {};
3601 self.horizontalScrollSyncCallBackFns = {};
3603 // self.visibleRowCache = [];
3605 // Set of 'render' containers for self grid, which can render sets of rows
3606 self.renderContainers = {};
3609 self.renderContainers.body = new GridRenderContainer('body', self);
3611 self.cellValueGetterCache = {};
3613 // Cached function to use with custom row templates
3614 self.getRowTemplateFn = null;
3617 //representation of the rows on the grid.
3618 //these are wrapped references to the actual data rows (options.data)
3621 //represents the columns on the grid
3626 * @name isScrollingVertically
3627 * @propertyOf ui.grid.class:Grid
3628 * @description set to true when Grid is scrolling vertically. Set to false via debounced method
3630 self.isScrollingVertically = false;
3634 * @name isScrollingHorizontally
3635 * @propertyOf ui.grid.class:Grid
3636 * @description set to true when Grid is scrolling horizontally. Set to false via debounced method
3638 self.isScrollingHorizontally = false;
3642 * @name scrollDirection
3643 * @propertyOf ui.grid.class:Grid
3644 * @description set one of the uiGridConstants.scrollDirection values (UP, DOWN, LEFT, RIGHT, NONE), which tells
3645 * us which direction we are scrolling. Set to NONE via debounced method
3647 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3649 //if true, grid will not respond to any scroll events
3650 self.disableScrolling = false;
3653 function vertical (scrollEvent) {
3654 self.isScrollingVertically = false;
3655 self.api.core.raise.scrollEnd(scrollEvent);
3656 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3659 var debouncedVertical = gridUtil.debounce(vertical, self.options.scrollDebounce);
3660 var debouncedVerticalMinDelay = gridUtil.debounce(vertical, 0);
3662 function horizontal (scrollEvent) {
3663 self.isScrollingHorizontally = false;
3664 self.api.core.raise.scrollEnd(scrollEvent);
3665 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3668 var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce);
3669 var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0);
3674 * @name flagScrollingVertically
3675 * @methodOf ui.grid.class:Grid
3676 * @description sets isScrollingVertically to true and sets it to false in a debounced function
3678 self.flagScrollingVertically = function(scrollEvent) {
3679 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3680 self.api.core.raise.scrollBegin(scrollEvent);
3682 self.isScrollingVertically = true;
3683 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3684 debouncedVerticalMinDelay(scrollEvent);
3687 debouncedVertical(scrollEvent);
3693 * @name flagScrollingHorizontally
3694 * @methodOf ui.grid.class:Grid
3695 * @description sets isScrollingHorizontally to true and sets it to false in a debounced function
3697 self.flagScrollingHorizontally = function(scrollEvent) {
3698 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3699 self.api.core.raise.scrollBegin(scrollEvent);
3701 self.isScrollingHorizontally = true;
3702 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3703 debouncedHorizontalMinDelay(scrollEvent);
3706 debouncedHorizontal(scrollEvent);
3710 self.scrollbarHeight = 0;
3711 self.scrollbarWidth = 0;
3712 if (self.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3713 self.scrollbarHeight = gridUtil.getScrollbarWidth();
3716 if (self.options.enableVerticalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3717 self.scrollbarWidth = gridUtil.getScrollbarWidth();
3722 self.api = new GridApi(self);
3727 * @methodOf ui.grid.core.api:PublicApi
3728 * @description Refresh the rendered grid on screen.
3729 * The refresh method re-runs both the columnProcessors and the
3730 * rowProcessors, as well as calling refreshCanvas to update all
3731 * the grid sizing. In general you should prefer to use queueGridRefresh
3732 * instead, which is basically a debounced version of refresh.
3734 * If you only want to resize the grid, not regenerate all the rows
3735 * and columns, you should consider directly calling refreshCanvas instead.
3738 self.api.registerMethod( 'core', 'refresh', this.refresh );
3742 * @name queueGridRefresh
3743 * @methodOf ui.grid.core.api:PublicApi
3744 * @description Request a refresh of the rendered grid on screen, if multiple
3745 * calls to queueGridRefresh are made within a digest cycle only one will execute.
3746 * The refresh method re-runs both the columnProcessors and the
3747 * rowProcessors, as well as calling refreshCanvas to update all
3748 * the grid sizing. In general you should prefer to use queueGridRefresh
3749 * instead, which is basically a debounced version of refresh.
3752 self.api.registerMethod( 'core', 'queueGridRefresh', this.queueGridRefresh );
3757 * @methodOf ui.grid.core.api:PublicApi
3758 * @description Runs only the rowProcessors, columns remain as they were.
3759 * It then calls redrawInPlace and refreshCanvas, which adjust the grid sizing.
3760 * @returns {promise} promise that is resolved when render completes?
3763 self.api.registerMethod( 'core', 'refreshRows', this.refreshRows );
3767 * @name queueRefresh
3768 * @methodOf ui.grid.core.api:PublicApi
3769 * @description Requests execution of refreshCanvas, if multiple requests are made
3770 * during a digest cycle only one will run. RefreshCanvas updates the grid sizing.
3771 * @returns {promise} promise that is resolved when render completes?
3774 self.api.registerMethod( 'core', 'queueRefresh', this.queueRefresh );
3778 * @name handleWindowResize
3779 * @methodOf ui.grid.core.api:PublicApi
3780 * @description Trigger a grid resize, normally this would be picked
3781 * up by a watch on window size, but in some circumstances it is necessary
3782 * to call this manually
3783 * @returns {promise} promise that is resolved when render completes?
3786 self.api.registerMethod( 'core', 'handleWindowResize', this.handleWindowResize );
3791 * @name addRowHeaderColumn
3792 * @methodOf ui.grid.core.api:PublicApi
3793 * @description adds a row header column to the grid
3794 * @param {object} column def
3797 self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn );
3801 * @name scrollToIfNecessary
3802 * @methodOf ui.grid.core.api:PublicApi
3803 * @description Scrolls the grid to make a certain row and column combo visible,
3804 * in the case that it is not completely visible on the screen already.
3805 * @param {GridRow} gridRow row to make visible
3806 * @param {GridCol} gridCol column to make visible
3807 * @returns {promise} a promise that is resolved when scrolling is complete
3810 self.api.registerMethod( 'core', 'scrollToIfNecessary', function(gridRow, gridCol) { return self.scrollToIfNecessary(gridRow, gridCol);} );
3815 * @methodOf ui.grid.core.api:PublicApi
3816 * @description Scroll the grid such that the specified
3817 * row and column is in view
3818 * @param {object} rowEntity gridOptions.data[] array instance to make visible
3819 * @param {object} colDef to make visible
3820 * @returns {promise} a promise that is resolved after any scrolling is finished
3822 self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} );
3826 * @name registerRowsProcessor
3827 * @methodOf ui.grid.core.api:PublicApi
3829 * Register a "rows processor" function. When the rows are updated,
3830 * the grid calls each registered "rows processor", which has a chance
3831 * to alter the set of rows (sorting, etc) as long as the count is not
3834 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
3835 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3836 * return the updated rows list, which is passed to the next processor in the chain
3837 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3838 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
3840 * At present allRowsVisible is running at 50, sort manipulations running at 60-65, filter is running at 100,
3841 * sort is at 200, grouping and treeview at 400-410, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
3843 self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor );
3847 * @name registerColumnsProcessor
3848 * @methodOf ui.grid.core.api:PublicApi
3850 * Register a "columns processor" function. When the columns are updated,
3851 * the grid calls each registered "columns processor", which has a chance
3852 * to alter the set of columns as long as the count is not
3855 * @param {function(renderedColumnsToProcess, rows )} processorFunction columns processor function, which
3856 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3857 * return the updated columns list, which is passed to the next processor in the chain
3858 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3859 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
3861 * 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)
3863 self.api.registerMethod( 'core', 'registerColumnsProcessor', this.registerColumnsProcessor );
3869 * @name sortHandleNulls
3870 * @methodOf ui.grid.core.api:PublicApi
3871 * @description A null handling method that can be used when building custom sort
3875 * mySortFn = function(a, b) {
3876 * var nulls = $scope.gridApi.core.sortHandleNulls(a, b);
3877 * if ( nulls !== null ){
3880 * // your code for sorting here
3883 * @param {object} a sort value a
3884 * @param {object} b sort value b
3885 * @returns {number} null if there were no nulls/undefineds, otherwise returns
3886 * a sort value that should be passed back from the sort function
3889 self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls );
3895 * @methodOf ui.grid.core.api:PublicApi
3896 * @description The sort criteria on one or more columns has
3897 * changed. Provides as parameters the grid and the output of
3898 * getColumnSorting, which is an array of gridColumns
3899 * that have sorting on them, sorted in priority order.
3901 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3902 * @param {Function} callBack Will be called when the event is emited. The function passes back an array of columns with
3903 * sorts on them, in priority order.
3907 * gridApi.core.on.sortChanged( $scope, function(sortColumns){
3912 self.api.registerEvent( 'core', 'sortChanged' );
3916 * @name columnVisibilityChanged
3917 * @methodOf ui.grid.core.api:PublicApi
3918 * @description The visibility of a column has changed,
3919 * the column itself is passed out as a parameter of the event.
3921 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3922 * @param {Function} callBack Will be called when the event is emited. The function passes back the GridCol that has changed.
3926 * gridApi.core.on.columnVisibilityChanged( $scope, function (column) {
3931 self.api.registerEvent( 'core', 'columnVisibilityChanged' );
3935 * @name notifyDataChange
3936 * @methodOf ui.grid.core.api:PublicApi
3937 * @description Notify the grid that a data or config change has occurred,
3938 * where that change isn't something the grid was otherwise noticing. This
3939 * might be particularly relevant where you've changed values within the data
3940 * and you'd like cell classes to be re-evaluated, or changed config within
3941 * the columnDef and you'd like headerCellClasses to be re-evaluated.
3942 * @param {string} type one of the
3943 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN), which tells
3944 * us which refreshes to fire.
3947 self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange );
3951 * @name clearAllFilters
3952 * @methodOf ui.grid.core.api:PublicApi
3953 * @description Clears all filters and optionally refreshes the visible rows.
3954 * @param {object} refreshRows Defaults to true.
3955 * @param {object} clearConditions Defaults to false.
3956 * @param {object} clearFlags Defaults to false.
3957 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
3959 self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters);
3961 self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]);
3962 self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]);
3963 self.registerDataChangeCallback( self.updateFooterHeightCallback, [uiGridConstants.dataChange.OPTIONS]);
3965 self.registerStyleComputation({
3967 func: self.getFooterStyles
3971 Grid.prototype.calcFooterHeight = function () {
3972 if (!this.hasFooter()) {
3977 if (this.options.showGridFooter) {
3978 height += this.options.gridFooterHeight;
3981 height += this.calcColumnFooterHeight();
3986 Grid.prototype.calcColumnFooterHeight = function () {
3989 if (this.options.showColumnFooter) {
3990 height += this.options.columnFooterHeight;
3996 Grid.prototype.getFooterStyles = function () {
3997 var style = '.grid' + this.id + ' .ui-grid-footer-aggregates-row { height: ' + this.options.columnFooterHeight + 'px; }';
3998 style += ' .grid' + this.id + ' .ui-grid-footer-info { height: ' + this.options.gridFooterHeight + 'px; }';
4002 Grid.prototype.hasFooter = function () {
4003 return this.options.showGridFooter || this.options.showColumnFooter;
4009 * @methodOf ui.grid.class:Grid
4010 * @description Returns true if grid is RightToLeft
4012 Grid.prototype.isRTL = function () {
4019 * @name registerColumnBuilder
4020 * @methodOf ui.grid.class:Grid
4021 * @description When the build creates columns from column definitions, the columnbuilders will be called to add
4022 * additional properties to the column.
4023 * @param {function(colDef, col, gridOptions)} columnBuilder function to be called
4025 Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) {
4026 this.columnBuilders.push(columnBuilder);
4031 * @name buildColumnDefsFromData
4032 * @methodOf ui.grid.class:Grid
4033 * @description Populates columnDefs from the provided data
4034 * @param {function(colDef, col, gridOptions)} rowBuilder function to be called
4036 Grid.prototype.buildColumnDefsFromData = function (dataRows){
4037 this.options.columnDefs = gridUtil.getColumnsFromData(dataRows, this.options.excludeProperties);
4042 * @name registerRowBuilder
4043 * @methodOf ui.grid.class:Grid
4044 * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add
4045 * additional properties to the row.
4046 * @param {function(row, gridOptions)} rowBuilder function to be called
4048 Grid.prototype.registerRowBuilder = function registerRowBuilder(rowBuilder) {
4049 this.rowBuilders.push(rowBuilder);
4055 * @name registerDataChangeCallback
4056 * @methodOf ui.grid.class:Grid
4057 * @description When a data change occurs, the data change callbacks of the specified type
4058 * will be called. The rules are:
4060 * - when the data watch fires, that is considered a ROW change (the data watch only notices
4061 * added or removed rows)
4062 * - when the api is called to inform us of a change, the declared type of that change is used
4063 * - when a cell edit completes, the EDIT callbacks are triggered
4064 * - when the columnDef watch fires, the COLUMN callbacks are triggered
4065 * - when the options watch fires, the OPTIONS callbacks are triggered
4067 * For a given event:
4068 * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks
4069 * - ROW calls ROW and ALL callbacks
4070 * - EDIT calls EDIT and ALL callbacks
4071 * - COLUMN calls COLUMN and ALL callbacks
4072 * - OPTIONS calls OPTIONS and ALL callbacks
4074 * @param {function(grid)} callback function to be called
4075 * @param {array} types the types of data change you want to be informed of. Values from
4076 * the uiGridConstants.dataChange values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to
4078 * @returns {function} deregister function - a function that can be called to deregister this callback
4080 Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) {
4081 var uid = gridUtil.nextUid();
4083 types = [uiGridConstants.dataChange.ALL];
4085 if ( !Array.isArray(types)){
4086 gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types );
4088 this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this };
4091 var deregisterFunction = function() {
4092 delete self.dataChangeCallbacks[uid];
4094 return deregisterFunction;
4099 * @name callDataChangeCallbacks
4100 * @methodOf ui.grid.class:Grid
4101 * @description Calls the callbacks based on the type of data change that
4102 * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the
4103 * event type is matching, or if the type is ALL.
4104 * @param {number} type the type of event that occurred - one of the
4105 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN, OPTIONS)
4107 Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) {
4108 angular.forEach( this.dataChangeCallbacks, function( callback, uid ){
4109 if ( callback.types.indexOf( uiGridConstants.dataChange.ALL ) !== -1 ||
4110 callback.types.indexOf( type ) !== -1 ||
4111 type === uiGridConstants.dataChange.ALL ) {
4112 if (callback._this) {
4113 callback.callback.apply(callback._this,this);
4116 callback.callback( this );
4124 * @name notifyDataChange
4125 * @methodOf ui.grid.class:Grid
4126 * @description Notifies us that a data change has occurred, used in the public
4127 * api for users to tell us when they've changed data or some other event that
4128 * our watches cannot pick up
4129 * @param {string} type the type of event that occurred - one of the
4130 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN)
4132 Grid.prototype.notifyDataChange = function notifyDataChange(type) {
4133 var constants = uiGridConstants.dataChange;
4134 if ( type === constants.ALL ||
4135 type === constants.COLUMN ||
4136 type === constants.EDIT ||
4137 type === constants.ROW ||
4138 type === constants.OPTIONS ){
4139 this.callDataChangeCallbacks( type );
4141 gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type);
4148 * @name columnRefreshCallback
4149 * @methodOf ui.grid.class:Grid
4150 * @description refreshes the grid when a column refresh
4151 * is notified, which triggers handling of the visible flag.
4152 * This is called on uiGridConstants.dataChange.COLUMN, and is
4153 * registered as a dataChangeCallback in grid.js
4154 * @param {string} name column name
4156 Grid.prototype.columnRefreshCallback = function columnRefreshCallback( grid ){
4157 grid.buildColumns();
4158 grid.queueGridRefresh();
4164 * @name processRowsCallback
4165 * @methodOf ui.grid.class:Grid
4166 * @description calls the row processors, specifically
4167 * intended to reset the sorting when an edit is called,
4168 * registered as a dataChangeCallback on uiGridConstants.dataChange.EDIT
4169 * @param {string} name column name
4171 Grid.prototype.processRowsCallback = function processRowsCallback( grid ){
4172 grid.queueGridRefresh();
4178 * @name updateFooterHeightCallback
4179 * @methodOf ui.grid.class:Grid
4180 * @description recalculates the footer height,
4181 * registered as a dataChangeCallback on uiGridConstants.dataChange.OPTIONS
4182 * @param {string} name column name
4184 Grid.prototype.updateFooterHeightCallback = function updateFooterHeightCallback( grid ){
4185 grid.footerHeight = grid.calcFooterHeight();
4186 grid.columnFooterHeight = grid.calcColumnFooterHeight();
4193 * @methodOf ui.grid.class:Grid
4194 * @description returns a grid column for the column name
4195 * @param {string} name column name
4197 Grid.prototype.getColumn = function getColumn(name) {
4198 var columns = this.columns.filter(function (column) {
4199 return column.colDef.name === name;
4201 return columns.length > 0 ? columns[0] : null;
4207 * @methodOf ui.grid.class:Grid
4208 * @description returns a grid colDef for the column name
4209 * @param {string} name column.field
4211 Grid.prototype.getColDef = function getColDef(name) {
4212 var colDefs = this.options.columnDefs.filter(function (colDef) {
4213 return colDef.name === name;
4215 return colDefs.length > 0 ? colDefs[0] : null;
4221 * @methodOf ui.grid.class:Grid
4222 * @description uses the first row of data to assign colDef.type for any types not defined.
4227 * @propertyOf ui.grid.class:GridOptions.columnDef
4228 * @description the type of the column, used in sorting. If not provided then the
4229 * grid will guess the type. Add this only if the grid guessing is not to your
4230 * satisfaction. One of:
4237 * Note that if you choose date, your dates should be in a javascript date type
4240 Grid.prototype.assignTypes = function(){
4242 self.options.columnDefs.forEach(function (colDef, index) {
4244 //Assign colDef type if not specified
4246 var col = new GridColumn(colDef, index, self);
4247 var firstRow = self.rows.length > 0 ? self.rows[0] : null;
4249 colDef.type = gridUtil.guessType(self.getCellValue(firstRow, col));
4252 colDef.type = 'string';
4261 * @name isRowHeaderColumn
4262 * @methodOf ui.grid.class:Grid
4263 * @description returns true if the column is a row Header
4264 * @param {object} column column
4266 Grid.prototype.isRowHeaderColumn = function isRowHeaderColumn(column) {
4267 return this.rowHeaderColumns.indexOf(column) !== -1;
4272 * @name addRowHeaderColumn
4273 * @methodOf ui.grid.class:Grid
4274 * @description adds a row header column to the grid
4275 * @param {object} column def
4277 Grid.prototype.addRowHeaderColumn = function addRowHeaderColumn(colDef) {
4279 var rowHeaderCol = new GridColumn(colDef, gridUtil.nextUid(), self);
4280 rowHeaderCol.isRowHeader = true;
4282 self.createRightContainer();
4283 rowHeaderCol.renderContainer = 'right';
4286 self.createLeftContainer();
4287 rowHeaderCol.renderContainer = 'left';
4290 // relies on the default column builder being first in array, as it is instantiated
4291 // as part of grid creation
4292 self.columnBuilders[0](colDef,rowHeaderCol,self.options)
4294 rowHeaderCol.enableFiltering = false;
4295 rowHeaderCol.enableSorting = false;
4296 rowHeaderCol.enableHiding = false;
4297 self.rowHeaderColumns.push(rowHeaderCol);
4300 self.preCompileCellTemplates();
4301 self.queueGridRefresh();
4308 * @name getOnlyDataColumns
4309 * @methodOf ui.grid.class:Grid
4310 * @description returns all columns except for rowHeader columns
4312 Grid.prototype.getOnlyDataColumns = function getOnlyDataColumns() {
4315 self.columns.forEach(function (col) {
4316 if (self.rowHeaderColumns.indexOf(col) === -1) {
4325 * @name buildColumns
4326 * @methodOf ui.grid.class:Grid
4327 * @description creates GridColumn objects from the columnDefinition. Calls each registered
4328 * columnBuilder to further process the column
4329 * @param {object} options An object contains options to use when building columns
4331 * * **orderByColumnDefs**: defaults to **false**. When true, `buildColumns` will reorder existing columns according to the order within the column definitions.
4333 * @returns {Promise} a promise to load any needed column resources
4335 Grid.prototype.buildColumns = function buildColumns(opts) {
4337 orderByColumnDefs: false
4340 angular.extend(options, opts);
4342 // gridUtil.logDebug('buildColumns');
4344 var builderPromises = [];
4345 var headerOffset = self.rowHeaderColumns.length;
4348 // Remove any columns for which a columnDef cannot be found
4349 // Deliberately don't use forEach, as it doesn't like splice being called in the middle
4350 // Also don't cache columns.length, as it will change during this operation
4351 for (i = 0; i < self.columns.length; i++){
4352 if (!self.getColDef(self.columns[i].name)) {
4353 self.columns.splice(i, 1);
4358 //add row header columns to the grid columns array _after_ columns without columnDefs have been removed
4359 self.rowHeaderColumns.forEach(function (rowHeaderColumn) {
4360 self.columns.unshift(rowHeaderColumn);
4364 // look at each column def, and update column properties to match. If the column def
4365 // doesn't have a column, then splice in a new gridCol
4366 self.options.columnDefs.forEach(function (colDef, index) {
4367 self.preprocessColDef(colDef);
4368 var col = self.getColumn(colDef.name);
4371 col = new GridColumn(colDef, gridUtil.nextUid(), self);
4372 self.columns.splice(index + headerOffset, 0, col);
4375 // tell updateColumnDef that the column was pre-existing
4376 col.updateColumnDef(colDef, false);
4379 self.columnBuilders.forEach(function (builder) {
4380 builderPromises.push(builder.call(self, colDef, col, self.options));
4384 /*** Reorder columns if necessary ***/
4385 if (!!options.orderByColumnDefs) {
4386 // Create a shallow copy of the columns as a cache
4387 var columnCache = self.columns.slice(0);
4389 // We need to allow for the "row headers" when mapping from the column defs array to the columns array
4390 // 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]
4392 // 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
4393 // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result
4394 var len = Math.min(self.options.columnDefs.length, self.columns.length);
4395 for (i = 0; i < len; i++) {
4396 // If the column at this index has a different name than the column at the same index in the column defs...
4397 if (self.columns[i + headerOffset].name !== self.options.columnDefs[i].name) {
4398 // Replace the one in the cache with the appropriate column
4399 columnCache[i + headerOffset] = self.getColumn(self.options.columnDefs[i].name);
4402 // Otherwise just copy over the one from the initial columns
4403 columnCache[i + headerOffset] = self.columns[i + headerOffset];
4407 // Empty out the columns array, non-destructively
4408 self.columns.length = 0;
4410 // And splice in the updated, ordered columns from the cache
4411 Array.prototype.splice.apply(self.columns, [0, 0].concat(columnCache));
4414 return $q.all(builderPromises).then(function(){
4415 if (self.rows.length > 0){
4423 * @name preCompileCellTemplates
4424 * @methodOf ui.grid.class:Grid
4425 * @description precompiles all cell templates
4427 Grid.prototype.preCompileCellTemplates = function() {
4430 var preCompileTemplate = function( col ) {
4431 var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col));
4432 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
4434 var compiledElementFn = $compile(html);
4435 col.compiledElementFn = compiledElementFn;
4437 if (col.compiledElementFnDefer) {
4438 col.compiledElementFnDefer.resolve(col.compiledElementFn);
4442 this.columns.forEach(function (col) {
4443 if ( col.cellTemplate ){
4444 preCompileTemplate( col );
4445 } else if ( col.cellTemplatePromise ){
4446 col.cellTemplatePromise.then( function() {
4447 preCompileTemplate( col );
4455 * @name getGridQualifiedColField
4456 * @methodOf ui.grid.class:Grid
4457 * @description Returns the $parse-able accessor for a column within its $scope
4458 * @param {GridColumn} col col object
4460 Grid.prototype.getQualifiedColField = function (col) {
4461 return 'row.entity.' + gridUtil.preEval(col.field);
4466 * @name createLeftContainer
4467 * @methodOf ui.grid.class:Grid
4468 * @description creates the left render container if it doesn't already exist
4470 Grid.prototype.createLeftContainer = function() {
4471 if (!this.hasLeftContainer()) {
4472 this.renderContainers.left = new GridRenderContainer('left', this, { disableColumnOffset: true });
4478 * @name createRightContainer
4479 * @methodOf ui.grid.class:Grid
4480 * @description creates the right render container if it doesn't already exist
4482 Grid.prototype.createRightContainer = function() {
4483 if (!this.hasRightContainer()) {
4484 this.renderContainers.right = new GridRenderContainer('right', this, { disableColumnOffset: true });
4490 * @name hasLeftContainer
4491 * @methodOf ui.grid.class:Grid
4492 * @description returns true if leftContainer exists
4494 Grid.prototype.hasLeftContainer = function() {
4495 return this.renderContainers.left !== undefined;
4500 * @name hasRightContainer
4501 * @methodOf ui.grid.class:Grid
4502 * @description returns true if rightContainer exists
4504 Grid.prototype.hasRightContainer = function() {
4505 return this.renderContainers.right !== undefined;
4510 * undocumented function
4511 * @name preprocessColDef
4512 * @methodOf ui.grid.class:Grid
4513 * @description defaults the name property from field to maintain backwards compatibility with 2.x
4514 * validates that name or field is present
4516 Grid.prototype.preprocessColDef = function preprocessColDef(colDef) {
4519 if (!colDef.field && !colDef.name) {
4520 throw new Error('colDef.name or colDef.field property is required');
4523 //maintain backwards compatibility with 2.x
4524 //field was required in 2.x. now name is required
4525 if (colDef.name === undefined && colDef.field !== undefined) {
4526 // See if the column name already exists:
4527 var newName = colDef.field,
4529 while (self.getColumn(newName)) {
4530 newName = colDef.field + counter.toString();
4533 colDef.name = newName;
4537 // 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
4538 Grid.prototype.newInN = function newInN(o, n, oAccessor, nAccessor) {
4542 for (var i = 0; i < n.length; i++) {
4543 var nV = nAccessor ? n[i][nAccessor] : n[i];
4546 for (var j = 0; j < o.length; j++) {
4547 var oV = oAccessor ? o[j][oAccessor] : o[j];
4548 if (self.options.rowEquality(nV, oV)) {
4564 * @methodOf ui.grid.class:Grid
4565 * @description returns the GridRow that contains the rowEntity
4566 * @param {object} rowEntity the gridOptions.data array element instance
4567 * @param {array} rows [optional] the rows to look in - if not provided then
4568 * looks in grid.rows
4570 Grid.prototype.getRow = function getRow(rowEntity, lookInRows) {
4573 lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows;
4575 var rows = lookInRows.filter(function (row) {
4576 return self.options.rowEquality(row.entity, rowEntity);
4578 return rows.length > 0 ? rows[0] : null;
4585 * @methodOf ui.grid.class:Grid
4586 * @description creates or removes GridRow objects from the newRawData array. Calls each registered
4587 * rowBuilder to further process the row
4588 * @param {array} newRawData Modified set of data
4590 * This method aims to achieve three things:
4591 * 1. the resulting rows array is in the same order as the newRawData, we'll call
4592 * rowsProcessors immediately after to sort the data anyway
4593 * 2. if we have row hashing available, we try to use the rowHash to find the row
4594 * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected
4596 * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates
4597 * the newRows and newHash
4600 * newRawData.forEach newEntity
4601 * if (hashing enabled)
4602 * check oldHash for newEntity
4604 * look for old row directly in oldRows
4605 * if !oldRowFound // must be a new row
4607 * append to the newRows and add to newHash
4608 * run the processors
4611 * Rows are identified using the hashKey if configured. If not configured, then rows
4612 * are identified using the gridOptions.rowEquality function
4614 * This method is useful when trying to select rows immediately after loading data without
4615 * using a $timeout/$interval, e.g.:
4617 * $scope.gridOptions.data = someData;
4618 * $scope.gridApi.grid.modifyRows($scope.gridOptions.data);
4619 * $scope.gridApi.selection.selectRow($scope.gridOptions.data[0]);
4621 * OR to persist row selection after data update (e.g. rows selected, new data loaded, want
4622 * originally selected rows to be re-selected))
4624 Grid.prototype.modifyRows = function modifyRows(newRawData) {
4626 var oldRows = self.rows.slice(0);
4627 var oldRowHash = self.rowHashMap || self.createRowHashMap();
4628 self.rowHashMap = self.createRowHashMap();
4629 self.rows.length = 0;
4631 newRawData.forEach( function( newEntity, i ) {
4633 if ( self.options.enableRowHashing ){
4634 // if hashing is enabled, then this row will be in the hash if we already know about it
4635 newRow = oldRowHash.get( newEntity );
4637 // otherwise, manually search the oldRows to see if we can find this row
4638 newRow = self.getRow(newEntity, oldRows);
4641 // if we didn't find the row, it must be new, so create it
4643 newRow = self.processRowBuilders(new GridRow(newEntity, i, self));
4646 self.rows.push( newRow );
4647 self.rowHashMap.put( newEntity, newRow );
4652 var p1 = $q.when(self.processRowsProcessors(self.rows))
4653 .then(function (renderableRows) {
4654 return self.setVisibleRows(renderableRows);
4657 var p2 = $q.when(self.processColumnsProcessors(self.columns))
4658 .then(function (renderableColumns) {
4659 return self.setVisibleColumns(renderableColumns);
4662 return $q.all([p1, p2]);
4667 * Private Undocumented Method
4669 * @methodOf ui.grid.class:Grid
4670 * @description adds the newRawData array of rows to the grid and calls all registered
4671 * rowBuilders. this keyword will reference the grid
4673 Grid.prototype.addRows = function addRows(newRawData) {
4676 var existingRowCount = self.rows.length;
4677 for (var i = 0; i < newRawData.length; i++) {
4678 var newRow = self.processRowBuilders(new GridRow(newRawData[i], i + existingRowCount, self));
4680 if (self.options.enableRowHashing) {
4681 var found = self.rowHashMap.get(newRow.entity);
4687 self.rows.push(newRow);
4693 * @name processRowBuilders
4694 * @methodOf ui.grid.class:Grid
4695 * @description processes all RowBuilders for the gridRow
4696 * @param {GridRow} gridRow reference to gridRow
4697 * @returns {GridRow} the gridRow with all additional behavior added
4699 Grid.prototype.processRowBuilders = function processRowBuilders(gridRow) {
4702 self.rowBuilders.forEach(function (builder) {
4703 builder.call(self, gridRow, self.options);
4711 * @name registerStyleComputation
4712 * @methodOf ui.grid.class:Grid
4713 * @description registered a styleComputation function
4715 * If the function returns a value it will be appended into the grid's `<style>` block
4716 * @param {function($scope)} styleComputation function
4718 Grid.prototype.registerStyleComputation = function registerStyleComputation(styleComputationInfo) {
4719 this.styleComputations.push(styleComputationInfo);
4723 // NOTE (c0bra): We already have rowBuilders. I think these do exactly the same thing...
4724 // Grid.prototype.registerRowFilter = function(filter) {
4725 // // TODO(c0bra): validate filter?
4727 // this.rowFilters.push(filter);
4730 // Grid.prototype.removeRowFilter = function(filter) {
4731 // var idx = this.rowFilters.indexOf(filter);
4733 // if (typeof(idx) !== 'undefined' && idx !== undefined) {
4734 // this.rowFilters.slice(idx, 1);
4738 // Grid.prototype.processRowFilters = function(rows) {
4740 // self.rowFilters.forEach(function (filter) {
4741 // filter.call(self, rows);
4748 * @name registerRowsProcessor
4749 * @methodOf ui.grid.class:Grid
4752 * Register a "rows processor" function. When the rows are updated,
4753 * the grid calls each registered "rows processor", which has a chance
4754 * to alter the set of rows (sorting, etc) as long as the count is not
4757 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
4758 * is run in the context of the grid (i.e. this for the function will be the grid), and must
4759 * return the updated rows list, which is passed to the next processor in the chain
4760 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4761 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
4763 * 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)
4766 Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor, priority) {
4767 if (!angular.isFunction(processor)) {
4768 throw 'Attempt to register non-function rows processor: ' + processor;
4771 this.rowsProcessors.push({processor: processor, priority: priority});
4772 this.rowsProcessors.sort(function sortByPriority( a, b ){
4773 return a.priority - b.priority;
4779 * @name removeRowsProcessor
4780 * @methodOf ui.grid.class:Grid
4781 * @param {function(renderableRows)} rows processor function
4782 * @description Remove a registered rows processor
4784 Grid.prototype.removeRowsProcessor = function removeRowsProcessor(processor) {
4786 this.rowsProcessors.forEach(function(rowsProcessor, index){
4787 if ( rowsProcessor.processor === processor ){
4793 this.rowsProcessors.splice(idx, 1);
4798 * Private Undocumented Method
4799 * @name processRowsProcessors
4800 * @methodOf ui.grid.class:Grid
4801 * @param {Array[GridRow]} The array of "renderable" rows
4802 * @param {Array[GridColumn]} The array of columns
4803 * @description Run all the registered rows processors on the array of renderable rows
4805 Grid.prototype.processRowsProcessors = function processRowsProcessors(renderableRows) {
4808 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4809 var myRenderableRows = renderableRows.slice(0);
4811 // Return myRenderableRows with no processing if we have no rows processors
4812 if (self.rowsProcessors.length === 0) {
4813 return $q.when(myRenderableRows);
4816 // Counter for iterating through rows processors
4819 // Promise for when we're done with all the processors
4820 var finished = $q.defer();
4822 // This function will call the processor in self.rowsProcessors at index 'i', and then
4823 // when done will call the next processor in the list, using the output from the processor
4824 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4826 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4828 function startProcessor(i, renderedRowsToProcess) {
4829 // Get the processor at 'i'
4830 var processor = self.rowsProcessors[i].processor;
4832 // Call the processor, passing in the rows to process and the current columns
4833 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4834 return $q.when( processor.call(self, renderedRowsToProcess, self.columns) )
4835 .then(function handleProcessedRows(processedRows) {
4837 if (!processedRows) {
4838 throw "Processor at index " + i + " did not return a set of renderable rows";
4841 if (!angular.isArray(processedRows)) {
4842 throw "Processor at index " + i + " did not return an array";
4845 // Processor is done, increment the counter
4848 // If we're not done with the processors, call the next one
4849 if (i <= self.rowsProcessors.length - 1) {
4850 return startProcessor(i, processedRows);
4852 // We're done! Resolve the 'finished' promise
4854 finished.resolve(processedRows);
4859 // Start on the first processor
4860 startProcessor(0, myRenderableRows);
4862 return finished.promise;
4865 Grid.prototype.setVisibleRows = function setVisibleRows(rows) {
4868 // Reset all the render container row caches
4869 for (var i in self.renderContainers) {
4870 var container = self.renderContainers[i];
4872 container.canvasHeightShouldUpdate = true;
4874 if ( typeof(container.visibleRowCache) === 'undefined' ){
4875 container.visibleRowCache = [];
4877 container.visibleRowCache.length = 0;
4881 // rows.forEach(function (row) {
4882 for (var ri = 0; ri < rows.length; ri++) {
4885 var targetContainer = (typeof(row.renderContainer) !== 'undefined' && row.renderContainer) ? row.renderContainer : 'body';
4887 // If the row is visible
4889 self.renderContainers[targetContainer].visibleRowCache.push(row);
4892 self.api.core.raise.rowsRendered(this.api);
4897 * @name registerColumnsProcessor
4898 * @methodOf ui.grid.class:Grid
4899 * @param {function(renderedColumnsToProcess, rows)} columnProcessor column processor function, which
4900 * is run in the context of the grid (i.e. this for the function will be the grid), and
4901 * which must return an updated renderedColumnsToProcess which can be passed to the next processor
4903 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4904 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
4906 * 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)
4909 Register a "columns processor" function. When the columns are updated,
4910 the grid calls each registered "columns processor", which has a chance
4911 to alter the set of columns, as long as the count is not modified.
4913 Grid.prototype.registerColumnsProcessor = function registerColumnsProcessor(processor, priority) {
4914 if (!angular.isFunction(processor)) {
4915 throw 'Attempt to register non-function rows processor: ' + processor;
4918 this.columnsProcessors.push({processor: processor, priority: priority});
4919 this.columnsProcessors.sort(function sortByPriority( a, b ){
4920 return a.priority - b.priority;
4924 Grid.prototype.removeColumnsProcessor = function removeColumnsProcessor(processor) {
4925 var idx = this.columnsProcessors.indexOf(processor);
4927 if (typeof(idx) !== 'undefined' && idx !== undefined) {
4928 this.columnsProcessors.splice(idx, 1);
4932 Grid.prototype.processColumnsProcessors = function processColumnsProcessors(renderableColumns) {
4935 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4936 var myRenderableColumns = renderableColumns.slice(0);
4938 // Return myRenderableRows with no processing if we have no rows processors
4939 if (self.columnsProcessors.length === 0) {
4940 return $q.when(myRenderableColumns);
4943 // Counter for iterating through rows processors
4946 // Promise for when we're done with all the processors
4947 var finished = $q.defer();
4949 // This function will call the processor in self.rowsProcessors at index 'i', and then
4950 // when done will call the next processor in the list, using the output from the processor
4951 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4953 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4955 function startProcessor(i, renderedColumnsToProcess) {
4956 // Get the processor at 'i'
4957 var processor = self.columnsProcessors[i].processor;
4959 // Call the processor, passing in the rows to process and the current columns
4960 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4961 return $q.when( processor.call(self, renderedColumnsToProcess, self.rows) )
4962 .then(function handleProcessedRows(processedColumns) {
4964 if (!processedColumns) {
4965 throw "Processor at index " + i + " did not return a set of renderable rows";
4968 if (!angular.isArray(processedColumns)) {
4969 throw "Processor at index " + i + " did not return an array";
4972 // Processor is done, increment the counter
4975 // If we're not done with the processors, call the next one
4976 if (i <= self.columnsProcessors.length - 1) {
4977 return startProcessor(i, myRenderableColumns);
4979 // We're done! Resolve the 'finished' promise
4981 finished.resolve(myRenderableColumns);
4986 // Start on the first processor
4987 startProcessor(0, myRenderableColumns);
4989 return finished.promise;
4992 Grid.prototype.setVisibleColumns = function setVisibleColumns(columns) {
4993 // gridUtil.logDebug('setVisibleColumns');
4997 // Reset all the render container row caches
4998 for (var i in self.renderContainers) {
4999 var container = self.renderContainers[i];
5001 container.visibleColumnCache.length = 0;
5004 for (var ci = 0; ci < columns.length; ci++) {
5005 var column = columns[ci];
5007 // If the column is visible
5008 if (column.visible) {
5009 // If the column has a container specified
5010 if (typeof(column.renderContainer) !== 'undefined' && column.renderContainer) {
5011 self.renderContainers[column.renderContainer].visibleColumnCache.push(column);
5013 // If not, put it into the body container
5015 self.renderContainers.body.visibleColumnCache.push(column);
5023 * @name handleWindowResize
5024 * @methodOf ui.grid.class:Grid
5025 * @description Triggered when the browser window resizes; automatically resizes the grid
5027 Grid.prototype.handleWindowResize = function handleWindowResize($event) {
5030 self.gridWidth = gridUtil.elementWidth(self.element);
5031 self.gridHeight = gridUtil.elementHeight(self.element);
5033 self.queueRefresh();
5038 * @name queueRefresh
5039 * @methodOf ui.grid.class:Grid
5040 * @description queues a grid refreshCanvas, a way of debouncing all the refreshes we might otherwise issue
5042 Grid.prototype.queueRefresh = function queueRefresh() {
5045 if (self.refreshCanceller) {
5046 $timeout.cancel(self.refreshCanceller);
5049 self.refreshCanceller = $timeout(function () {
5050 self.refreshCanvas(true);
5053 self.refreshCanceller.then(function () {
5054 self.refreshCanceller = null;
5057 return self.refreshCanceller;
5063 * @name queueGridRefresh
5064 * @methodOf ui.grid.class:Grid
5065 * @description queues a grid refresh, a way of debouncing all the refreshes we might otherwise issue
5067 Grid.prototype.queueGridRefresh = function queueGridRefresh() {
5070 if (self.gridRefreshCanceller) {
5071 $timeout.cancel(self.gridRefreshCanceller);
5074 self.gridRefreshCanceller = $timeout(function () {
5078 self.gridRefreshCanceller.then(function () {
5079 self.gridRefreshCanceller = null;
5082 return self.gridRefreshCanceller;
5088 * @name updateCanvasHeight
5089 * @methodOf ui.grid.class:Grid
5090 * @description flags all render containers to update their canvas height
5092 Grid.prototype.updateCanvasHeight = function updateCanvasHeight() {
5095 for (var containerId in self.renderContainers) {
5096 if (self.renderContainers.hasOwnProperty(containerId)) {
5097 var container = self.renderContainers[containerId];
5098 container.canvasHeightShouldUpdate = true;
5106 * @methodOf ui.grid.class:Grid
5107 * @description calls each styleComputation function
5109 // TODO: this used to take $scope, but couldn't see that it was used
5110 Grid.prototype.buildStyles = function buildStyles() {
5111 // gridUtil.logDebug('buildStyles');
5115 self.customStyles = '';
5117 self.styleComputations
5118 .sort(function(a, b) {
5119 if (a.priority === null) { return 1; }
5120 if (b.priority === null) { return -1; }
5121 if (a.priority === null && b.priority === null) { return 0; }
5122 return a.priority - b.priority;
5124 .forEach(function (compInfo) {
5125 // this used to provide $scope as a second parameter, but I couldn't find any
5126 // style builders that used it, so removed it as part of moving to grid from controller
5127 var ret = compInfo.func.call(self);
5129 if (angular.isString(ret)) {
5130 self.customStyles += '\n' + ret;
5136 Grid.prototype.minColumnsToRender = function minColumnsToRender() {
5138 var viewport = this.getViewportWidth();
5142 self.columns.forEach(function(col, i) {
5143 if (totalWidth < viewport) {
5144 totalWidth += col.drawnWidth;
5149 for (var j = i; j >= i - min; j--) {
5150 currWidth += self.columns[j].drawnWidth;
5152 if (currWidth < viewport) {
5161 Grid.prototype.getBodyHeight = function getBodyHeight() {
5162 // Start with the viewportHeight
5163 var bodyHeight = this.getViewportHeight();
5165 // Add the horizontal scrollbar height if there is one
5166 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5167 // bodyHeight = bodyHeight + this.horizontalScrollbarHeight;
5173 // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border)
5174 // TODO(c0bra): account for footer height
5175 Grid.prototype.getViewportHeight = function getViewportHeight() {
5178 var viewPortHeight = this.gridHeight - this.headerHeight - this.footerHeight;
5180 // Account for native horizontal scrollbar, if present
5181 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5182 // viewPortHeight = viewPortHeight - this.horizontalScrollbarHeight;
5185 var adjustment = self.getViewportAdjustment();
5187 viewPortHeight = viewPortHeight + adjustment.height;
5189 //gridUtil.logDebug('viewPortHeight', viewPortHeight);
5191 return viewPortHeight;
5194 Grid.prototype.getViewportWidth = function getViewportWidth() {
5197 var viewPortWidth = this.gridWidth;
5199 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5200 // viewPortWidth = viewPortWidth - this.verticalScrollbarWidth;
5203 var adjustment = self.getViewportAdjustment();
5205 viewPortWidth = viewPortWidth + adjustment.width;
5207 //gridUtil.logDebug('getviewPortWidth', viewPortWidth);
5209 return viewPortWidth;
5212 Grid.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
5213 var viewPortWidth = this.getViewportWidth();
5215 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5216 // viewPortWidth = viewPortWidth + this.verticalScrollbarWidth;
5219 return viewPortWidth;
5222 Grid.prototype.addVerticalScrollSync = function (containerId, callBackFn) {
5223 this.verticalScrollSyncCallBackFns[containerId] = callBackFn;
5226 Grid.prototype.addHorizontalScrollSync = function (containerId, callBackFn) {
5227 this.horizontalScrollSyncCallBackFns[containerId] = callBackFn;
5231 * Scroll needed containers by calling their ScrollSyncs
5232 * @param sourceContainerId the containerId that has already set it's top/left.
5233 * can be empty string which means all containers need to set top/left
5234 * @param scrollEvent
5236 Grid.prototype.scrollContainers = function (sourceContainerId, scrollEvent) {
5238 if (scrollEvent.y) {
5239 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5240 var verts = ['body','left', 'right'];
5242 this.flagScrollingVertically(scrollEvent);
5244 if (sourceContainerId === 'body') {
5245 verts = ['left', 'right'];
5247 else if (sourceContainerId === 'left') {
5248 verts = ['body', 'right'];
5250 else if (sourceContainerId === 'right') {
5251 verts = ['body', 'left'];
5254 for (var i = 0; i < verts.length; i++) {
5256 if (this.verticalScrollSyncCallBackFns[id]) {
5257 this.verticalScrollSyncCallBackFns[id](scrollEvent);
5263 if (scrollEvent.x) {
5264 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5265 var horizs = ['body','bodyheader', 'bodyfooter'];
5267 this.flagScrollingHorizontally(scrollEvent);
5268 if (sourceContainerId === 'body') {
5269 horizs = ['bodyheader', 'bodyfooter'];
5272 for (var j = 0; j < horizs.length; j++) {
5273 var idh = horizs[j];
5274 if (this.horizontalScrollSyncCallBackFns[idh]) {
5275 this.horizontalScrollSyncCallBackFns[idh](scrollEvent);
5283 Grid.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
5284 this.viewportAdjusters.push(func);
5287 Grid.prototype.removeViewportAdjuster = function registerViewportAdjuster(func) {
5288 var idx = this.viewportAdjusters.indexOf(func);
5290 if (typeof(idx) !== 'undefined' && idx !== undefined) {
5291 this.viewportAdjusters.splice(idx, 1);
5295 Grid.prototype.getViewportAdjustment = function getViewportAdjustment() {
5298 var adjustment = { height: 0, width: 0 };
5300 self.viewportAdjusters.forEach(function (func) {
5301 adjustment = func.call(this, adjustment);
5307 Grid.prototype.getVisibleRowCount = function getVisibleRowCount() {
5310 // this.rows.forEach(function (row) {
5311 // if (row.visible) {
5316 // return this.visibleRowCache.length;
5317 return this.renderContainers.body.visibleRowCache.length;
5320 Grid.prototype.getVisibleRows = function getVisibleRows() {
5321 return this.renderContainers.body.visibleRowCache;
5324 Grid.prototype.getVisibleColumnCount = function getVisibleColumnCount() {
5327 // this.rows.forEach(function (row) {
5328 // if (row.visible) {
5333 // return this.visibleRowCache.length;
5334 return this.renderContainers.body.visibleColumnCache.length;
5338 Grid.prototype.searchRows = function searchRows(renderableRows) {
5339 return rowSearcher.search(this, renderableRows, this.columns);
5342 Grid.prototype.sortByColumn = function sortByColumn(renderableRows) {
5343 return rowSorter.sort(this, renderableRows, this.columns);
5348 * @name getCellValue
5349 * @methodOf ui.grid.class:Grid
5350 * @description Gets the value of a cell for a particular row and column
5351 * @param {GridRow} row Row to access
5352 * @param {GridColumn} col Column to access
5354 Grid.prototype.getCellValue = function getCellValue(row, col){
5355 if ( typeof(row.entity[ '$$' + col.uid ]) !== 'undefined' ) {
5356 return row.entity[ '$$' + col.uid].rendered;
5357 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined' ){
5358 return row.entity[col.field];
5360 if (!col.cellValueGetterCache) {
5361 col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col));
5364 return col.cellValueGetterCache(row);
5370 * @name getCellDisplayValue
5371 * @methodOf ui.grid.class:Grid
5372 * @description Gets the displayed value of a cell after applying any the `cellFilter`
5373 * @param {GridRow} row Row to access
5374 * @param {GridColumn} col Column to access
5376 Grid.prototype.getCellDisplayValue = function getCellDisplayValue(row, col) {
5377 if ( !col.cellDisplayGetterCache ) {
5378 var custom_filter = col.cellFilter ? " | " + col.cellFilter : "";
5380 if (typeof(row.entity['$$' + col.uid]) !== 'undefined') {
5381 col.cellDisplayGetterCache = $parse(row.entity['$$' + col.uid].rendered + custom_filter);
5382 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined') {
5383 col.cellDisplayGetterCache = $parse(row.entity[col.field] + custom_filter);
5385 col.cellDisplayGetterCache = $parse(row.getEntityQualifiedColField(col) + custom_filter);
5389 return col.cellDisplayGetterCache(row);
5393 Grid.prototype.getNextColumnSortPriority = function getNextColumnSortPriority() {
5397 self.columns.forEach(function (col) {
5398 if (col.sort && col.sort.priority && col.sort.priority > p) {
5399 p = col.sort.priority;
5408 * @name resetColumnSorting
5409 * @methodOf ui.grid.class:Grid
5410 * @description Return the columns that the grid is currently being sorted by
5411 * @param {GridColumn} [excludedColumn] Optional GridColumn to exclude from having its sorting reset
5413 Grid.prototype.resetColumnSorting = function resetColumnSorting(excludeCol) {
5416 self.columns.forEach(function (col) {
5417 if (col !== excludeCol && !col.suppressRemoveSort) {
5425 * @name getColumnSorting
5426 * @methodOf ui.grid.class:Grid
5427 * @description Return the columns that the grid is currently being sorted by
5428 * @returns {Array[GridColumn]} An array of GridColumn objects
5430 Grid.prototype.getColumnSorting = function getColumnSorting() {
5433 var sortedCols = [], myCols;
5435 // Iterate through all the columns, sorted by priority
5436 // Make local copy of column list, because sorting is in-place and we do not want to
5437 // change the original sequence of columns
5438 myCols = self.columns.slice(0);
5439 myCols.sort(rowSorter.prioritySort).forEach(function (col) {
5440 if (col.sort && typeof(col.sort.direction) !== 'undefined' && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
5441 sortedCols.push(col);
5451 * @methodOf ui.grid.class:Grid
5452 * @description Set the sorting on a given column, optionally resetting any existing sorting on the Grid.
5453 * Emits the sortChanged event whenever the sort criteria are changed.
5454 * @param {GridColumn} column Column to set the sorting on
5455 * @param {uiGridConstants.ASC|uiGridConstants.DESC} [direction] Direction to sort by, either descending or ascending.
5456 * If not provided, the column will iterate through the sort directions
5457 * specified in the {@link ui.grid.class:GridOptions.columnDef#sortDirectionCycle sortDirectionCycle} attribute.
5458 * @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
5459 * by this column only
5460 * @returns {Promise} A resolved promise that supplies the column.
5463 Grid.prototype.sortColumn = function sortColumn(column, directionOrAdd, add) {
5467 if (typeof(column) === 'undefined' || !column) {
5468 throw new Error('No column parameter provided');
5471 // Second argument can either be a direction or whether to add this column to the existing sort.
5472 // If it's a boolean, it's an add, otherwise, it's a direction
5473 if (typeof(directionOrAdd) === 'boolean') {
5474 add = directionOrAdd;
5477 direction = directionOrAdd;
5481 self.resetColumnSorting(column);
5482 column.sort.priority = 0;
5483 // Get the actual priority since there may be columns which have suppressRemoveSort set
5484 column.sort.priority = self.getNextColumnSortPriority();
5486 else if (!column.sort.priority){
5487 column.sort.priority = self.getNextColumnSortPriority();
5491 // Find the current position in the cycle (or -1).
5492 var i = column.sortDirectionCycle.indexOf(column.sort.direction ? column.sort.direction : null);
5493 // Proceed to the next position in the cycle (or start at the beginning).
5494 i = (i+1) % column.sortDirectionCycle.length;
5495 // If suppressRemoveSort is set, and the next position in the cycle would
5496 // remove the sort, skip it.
5497 if (column.colDef && column.suppressRemoveSort && !column.sortDirectionCycle[i]) {
5498 i = (i+1) % column.sortDirectionCycle.length;
5501 if (column.sortDirectionCycle[i]) {
5502 column.sort.direction = column.sortDirectionCycle[i];
5508 column.sort.direction = direction;
5511 self.api.core.raise.sortChanged( self, self.getColumnSorting() );
5513 return $q.when(column);
5517 * communicate to outside world that we are done with initial rendering
5519 Grid.prototype.renderingComplete = function(){
5520 if (angular.isFunction(this.options.onRegisterApi)) {
5521 this.options.onRegisterApi(this.api);
5523 this.api.core.raise.renderingComplete( this.api );
5526 Grid.prototype.createRowHashMap = function createRowHashMap() {
5529 var hashMap = new RowHashMap();
5530 hashMap.grid = self;
5539 * @methodOf ui.grid.class:Grid
5540 * @description Refresh the rendered grid on screen.
5541 * @param {boolean} [rowsAltered] Optional flag for refreshing when the number of rows has changed.
5543 Grid.prototype.refresh = function refresh(rowsAltered) {
5546 var p1 = self.processRowsProcessors(self.rows).then(function (renderableRows) {
5547 self.setVisibleRows(renderableRows);
5550 var p2 = self.processColumnsProcessors(self.columns).then(function (renderableColumns) {
5551 self.setVisibleColumns(renderableColumns);
5554 return $q.all([p1, p2]).then(function () {
5555 self.redrawInPlace(rowsAltered);
5557 self.refreshCanvas(true);
5564 * @methodOf ui.grid.class:Grid
5565 * @description Refresh the rendered rows on screen? Note: not functional at present
5566 * @returns {promise} promise that is resolved when render completes?
5569 Grid.prototype.refreshRows = function refreshRows() {
5572 return self.processRowsProcessors(self.rows)
5573 .then(function (renderableRows) {
5574 self.setVisibleRows(renderableRows);
5576 self.redrawInPlace();
5578 self.refreshCanvas( true );
5584 * @name refreshCanvas
5585 * @methodOf ui.grid.class:Grid
5586 * @description Builds all styles and recalculates much of the grid sizing
5587 * @param {object} buildStyles optional parameter. Use TBD
5588 * @returns {promise} promise that is resolved when the canvas
5589 * has been refreshed
5592 Grid.prototype.refreshCanvas = function(buildStyles) {
5601 // Get all the header heights
5602 var containerHeadersToRecalc = [];
5603 for (var containerId in self.renderContainers) {
5604 if (self.renderContainers.hasOwnProperty(containerId)) {
5605 var container = self.renderContainers[containerId];
5607 // Skip containers that have no canvasWidth set yet
5608 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5612 if (container.header || container.headerCanvas) {
5613 container.explicitHeaderHeight = container.explicitHeaderHeight || null;
5614 container.explicitHeaderCanvasHeight = container.explicitHeaderCanvasHeight || null;
5616 containerHeadersToRecalc.push(container);
5623 * Here we loop through the headers, measuring each element as well as any header "canvas" it has within it.
5625 * If any header is less than the largest header height, it will be resized to that so that we don't have headers
5626 * with different heights, which looks like a rendering problem
5628 * We'll do the same thing with the header canvases, and give the header CELLS an explicit height if their canvas
5629 * is smaller than the largest canvas height. That was header cells without extra controls like filtering don't
5630 * appear shorter than other cells.
5633 if (containerHeadersToRecalc.length > 0) {
5634 // Build the styles without the explicit header heights
5639 // Putting in a timeout as it's not calculating after the grid element is rendered and filled out
5640 $timeout(function() {
5641 // var oldHeaderHeight = self.grid.headerHeight;
5642 // self.grid.headerHeight = gridUtil.outerElementHeight(self.header);
5644 var rebuildStyles = false;
5646 // Get all the header heights
5647 var maxHeaderHeight = 0;
5648 var maxHeaderCanvasHeight = 0;
5650 var getHeight = function(oldVal, newVal){
5651 if ( oldVal !== newVal){
5652 rebuildStyles = true;
5656 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5657 container = containerHeadersToRecalc[i];
5659 // Skip containers that have no canvasWidth set yet
5660 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5664 if (container.header) {
5665 var headerHeight = container.headerHeight = getHeight(container.headerHeight, parseInt(gridUtil.outerElementHeight(container.header), 10));
5667 // 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
5668 var topBorder = gridUtil.getBorderSize(container.header, 'top');
5669 var bottomBorder = gridUtil.getBorderSize(container.header, 'bottom');
5670 var innerHeaderHeight = parseInt(headerHeight - topBorder - bottomBorder, 10);
5672 innerHeaderHeight = innerHeaderHeight < 0 ? 0 : innerHeaderHeight;
5674 container.innerHeaderHeight = innerHeaderHeight;
5676 // If the header doesn't have an explicit height set, save the largest header height for use later
5677 // 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
5678 if (!container.explicitHeaderHeight && innerHeaderHeight > maxHeaderHeight) {
5679 maxHeaderHeight = innerHeaderHeight;
5683 if (container.headerCanvas) {
5684 var headerCanvasHeight = container.headerCanvasHeight = getHeight(container.headerCanvasHeight, parseInt(gridUtil.outerElementHeight(container.headerCanvas), 10));
5687 // If the header doesn't have an explicit canvas height, save the largest header canvas height for use later
5688 // 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
5689 if (!container.explicitHeaderCanvasHeight && headerCanvasHeight > maxHeaderCanvasHeight) {
5690 maxHeaderCanvasHeight = headerCanvasHeight;
5695 // Go through all the headers
5696 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5697 container = containerHeadersToRecalc[i];
5700 1. We have a max header height
5701 2. This container has a header height defined
5702 3. And either this container has an explicit header height set, OR its header height is less than the max
5706 Give this container's header an explicit height so it will line up with the tallest header
5709 maxHeaderHeight > 0 && typeof(container.headerHeight) !== 'undefined' && container.headerHeight !== null &&
5710 (container.explicitHeaderHeight || container.headerHeight < maxHeaderHeight)
5712 container.explicitHeaderHeight = getHeight(container.explicitHeaderHeight, maxHeaderHeight);
5715 // Do the same as above except for the header canvas
5717 maxHeaderCanvasHeight > 0 && typeof(container.headerCanvasHeight) !== 'undefined' && container.headerCanvasHeight !== null &&
5718 (container.explicitHeaderCanvasHeight || container.headerCanvasHeight < maxHeaderCanvasHeight)
5720 container.explicitHeaderCanvasHeight = getHeight(container.explicitHeaderCanvasHeight, maxHeaderCanvasHeight);
5724 // Rebuild styles if the header height has changed
5725 // The header height is used in body/viewport calculations and those are then used in other styles so we need it to be available
5726 if (buildStyles && rebuildStyles) {
5734 // Timeout still needs to be here to trigger digest after styles have been rebuilt
5735 $timeout(function() {
5746 * @name redrawCanvas
5747 * @methodOf ui.grid.class:Grid
5748 * @description Redraw the rows and columns based on our current scroll position
5749 * @param {boolean} [rowsAdded] Optional to indicate rows are added and the scroll percentage must be recalculated
5752 Grid.prototype.redrawInPlace = function redrawInPlace(rowsAdded) {
5753 // gridUtil.logDebug('redrawInPlace');
5757 for (var i in self.renderContainers) {
5758 var container = self.renderContainers[i];
5760 // gridUtil.logDebug('redrawing container', i);
5763 container.adjustRows(container.prevScrollTop, null);
5764 container.adjustColumns(container.prevScrollLeft, null);
5767 container.adjustRows(null, container.prevScrolltopPercentage);
5768 container.adjustColumns(null, container.prevScrollleftPercentage);
5775 * @name hasLeftContainerColumns
5776 * @methodOf ui.grid.class:Grid
5777 * @description returns true if leftContainer has columns
5779 Grid.prototype.hasLeftContainerColumns = function () {
5780 return this.hasLeftContainer() && this.renderContainers.left.renderedColumns.length > 0;
5785 * @name hasRightContainerColumns
5786 * @methodOf ui.grid.class:Grid
5787 * @description returns true if rightContainer has columns
5789 Grid.prototype.hasRightContainerColumns = function () {
5790 return this.hasRightContainer() && this.renderContainers.right.renderedColumns.length > 0;
5795 * @methodOf ui.grid.class:Grid
5796 * @name scrollToIfNecessary
5797 * @description Scrolls the grid to make a certain row and column combo visible,
5798 * in the case that it is not completely visible on the screen already.
5799 * @param {GridRow} gridRow row to make visible
5800 * @param {GridCol} gridCol column to make visible
5801 * @returns {promise} a promise that is resolved when scrolling is complete
5803 Grid.prototype.scrollToIfNecessary = function (gridRow, gridCol) {
5806 var scrollEvent = new ScrollEvent(self, 'uiGrid.scrollToIfNecessary');
5808 // Alias the visible row and column caches
5809 var visRowCache = self.renderContainers.body.visibleRowCache;
5810 var visColCache = self.renderContainers.body.visibleColumnCache;
5812 /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/
5814 // 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
5815 var topBound = self.renderContainers.body.prevScrollTop + self.headerHeight;
5817 // Don't the let top boundary be less than 0
5818 topBound = (topBound < 0) ? 0 : topBound;
5820 // The left boundary is the current X scroll position
5821 var leftBound = self.renderContainers.body.prevScrollLeft;
5823 // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height.
5824 // Basically this is the viewport height added on to the scroll position
5825 var bottomBound = self.renderContainers.body.prevScrollTop + self.gridHeight - self.renderContainers.body.headerHeight - self.footerHeight - self.scrollbarWidth;
5827 // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows
5828 //if (self.horizontalScrollbarHeight) {
5829 // bottomBound = bottomBound - self.horizontalScrollbarHeight;
5832 // The right position is the current X scroll position minus the grid width
5833 var rightBound = self.renderContainers.body.prevScrollLeft + Math.ceil(self.renderContainers.body.getViewportWidth());
5835 // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells
5836 //if (self.verticalScrollbarWidth) {
5837 // rightBound = rightBound - self.verticalScrollbarWidth;
5840 // We were given a row to scroll to
5841 if (gridRow !== null) {
5842 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5843 var seekRowIndex = visRowCache.indexOf(gridRow);
5845 // Total vertical scroll length of the grid
5846 var scrollLength = (self.renderContainers.body.getCanvasHeight() - self.renderContainers.body.getViewportHeight());
5848 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5849 //if (self.horizontalScrollbarHeight && self.horizontalScrollbarHeight > 0) {
5850 // scrollLength = scrollLength + self.horizontalScrollbarHeight;
5853 // This is the minimum amount of pixels we need to scroll vertical in order to see this row.
5854 var pixelsToSeeRow = ((seekRowIndex + 1) * self.options.rowHeight);
5856 // Don't let the pixels required to see the row be less than zero
5857 pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow;
5859 var scrollPixels, percentage;
5861 // 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...
5862 if (pixelsToSeeRow < topBound) {
5863 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5864 // to get the full position we need
5865 scrollPixels = self.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow);
5867 // Turn the scroll position into a percentage and make it an argument for a scroll event
5868 percentage = scrollPixels / scrollLength;
5869 scrollEvent.y = { percentage: percentage };
5871 // 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...
5872 else if (pixelsToSeeRow > bottomBound) {
5873 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5874 // to get the full position we need
5875 scrollPixels = pixelsToSeeRow - bottomBound + self.renderContainers.body.prevScrollTop;
5877 // Turn the scroll position into a percentage and make it an argument for a scroll event
5878 percentage = scrollPixels / scrollLength;
5879 scrollEvent.y = { percentage: percentage };
5883 // We were given a column to scroll to
5884 if (gridCol !== null) {
5885 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5886 var seekColumnIndex = visColCache.indexOf(gridCol);
5888 // Total vertical scroll length of the grid
5889 var horizScrollLength = (self.renderContainers.body.getCanvasWidth() - self.renderContainers.body.getViewportWidth());
5891 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5892 // if (self.verticalScrollbarWidth && self.verticalScrollbarWidth > 0) {
5893 // horizScrollLength = horizScrollLength + self.verticalScrollbarWidth;
5896 // This is the minimum amount of pixels we need to scroll vertical in order to see this column
5897 var columnLeftEdge = 0;
5898 for (var i = 0; i < seekColumnIndex; i++) {
5899 var col = visColCache[i];
5900 columnLeftEdge += col.drawnWidth;
5902 columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge;
5904 var columnRightEdge = columnLeftEdge + gridCol.drawnWidth;
5906 // Don't let the pixels required to see the column be less than zero
5907 columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge;
5909 var horizScrollPixels, horizPercentage;
5911 // 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...
5912 if (columnLeftEdge < leftBound) {
5913 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5914 // to get the full position we need
5915 horizScrollPixels = self.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge);
5917 // Turn the scroll position into a percentage and make it an argument for a scroll event
5918 horizPercentage = horizScrollPixels / horizScrollLength;
5919 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5920 scrollEvent.x = { percentage: horizPercentage };
5922 // 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...
5923 else if (columnRightEdge > rightBound) {
5924 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5925 // to get the full position we need
5926 horizScrollPixels = columnRightEdge - rightBound + self.renderContainers.body.prevScrollLeft;
5928 // Turn the scroll position into a percentage and make it an argument for a scroll event
5929 horizPercentage = horizScrollPixels / horizScrollLength;
5930 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5931 scrollEvent.x = { percentage: horizPercentage };
5935 var deferred = $q.defer();
5937 // If we need to scroll on either the x or y axes, fire a scroll event
5938 if (scrollEvent.y || scrollEvent.x) {
5939 scrollEvent.withDelay = false;
5940 self.scrollContainers('',scrollEvent);
5941 var dereg = self.api.core.on.scrollEnd(null,function() {
5942 deferred.resolve(scrollEvent);
5950 return deferred.promise;
5955 * @methodOf ui.grid.class:Grid
5957 * @description Scroll the grid such that the specified
5958 * row and column is in view
5959 * @param {object} rowEntity gridOptions.data[] array instance to make visible
5960 * @param {object} colDef to make visible
5961 * @returns {promise} a promise that is resolved after any scrolling is finished
5963 Grid.prototype.scrollTo = function (rowEntity, colDef) {
5964 var gridRow = null, gridCol = null;
5966 if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) {
5967 gridRow = this.getRow(rowEntity);
5970 if (colDef !== null && typeof(colDef) !== 'undefined' ) {
5971 gridCol = this.getColumn(colDef.name ? colDef.name : colDef.field);
5973 return this.scrollToIfNecessary(gridRow, gridCol);
5978 * @name clearAllFilters
5979 * @methodOf ui.grid.class:Grid
5980 * @description Clears all filters and optionally refreshes the visible rows.
5981 * @param {object} refreshRows Defaults to true.
5982 * @param {object} clearConditions Defaults to false.
5983 * @param {object} clearFlags Defaults to false.
5984 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
5986 Grid.prototype.clearAllFilters = function clearAllFilters(refreshRows, clearConditions, clearFlags) {
5987 // Default `refreshRows` to true because it will be the most commonly desired behaviour.
5988 if (refreshRows === undefined) {
5991 if (clearConditions === undefined) {
5992 clearConditions = false;
5994 if (clearFlags === undefined) {
5998 this.columns.forEach(function(column) {
5999 column.filters.forEach(function(filter) {
6000 filter.term = undefined;
6002 if (clearConditions) {
6003 filter.condition = undefined;
6007 filter.flags = undefined;
6013 return this.refreshRows();
6018 // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?)
6019 function RowHashMap() {}
6021 RowHashMap.prototype = {
6023 * Store key value pair
6024 * @param key key to store can be any type
6025 * @param value value to store can be any type
6027 put: function(key, value) {
6028 this[this.grid.options.rowIdentity(key)] = value;
6033 * @returns {Object} the value for the key
6035 get: function(key) {
6036 return this[this.grid.options.rowIdentity(key)];
6040 * Remove the key/value pair
6043 remove: function(key) {
6044 var value = this[key = this.grid.options.rowIdentity(key)];
6060 angular.module('ui.grid')
6061 .factory('GridApi', ['$q', '$rootScope', 'gridUtil', 'uiGridConstants', 'GridRow', 'uiGridGridMenuService',
6062 function ($q, $rootScope, gridUtil, uiGridConstants, GridRow, uiGridGridMenuService) {
6065 * @name ui.grid.class:GridApi
6066 * @description GridApi provides the ability to register public methods events inside the grid and allow
6067 * for other components to use the api via featureName.raise.methodName and featureName.on.eventName(function(args){}.
6069 * To listen to events, you must add a callback to gridOptions.onRegisterApi
6071 * $scope.gridOptions.onRegisterApi = function(gridApi){
6072 * gridApi.cellNav.on.navigate($scope,function(newRowCol, oldRowCol){
6073 * $log.log('navigation event');
6077 * @param {object} grid grid that owns api
6079 var GridApi = function GridApi(grid) {
6081 this.listeners = [];
6085 * @name renderingComplete
6086 * @methodOf ui.grid.core.api:PublicApi
6087 * @description Rendering is complete, called at the same
6088 * time as `onRegisterApi`, but provides a way to obtain
6089 * that same event within features without stopping end
6090 * users from getting at the onRegisterApi method.
6092 * Included in gridApi so that it's always there - otherwise
6093 * there is still a timing problem with when a feature can
6096 * @param {GridApi} gridApi the grid api, as normally
6097 * returned in the onRegisterApi method
6101 * gridApi.core.on.renderingComplete( grid );
6104 this.registerEvent( 'core', 'renderingComplete' );
6108 * @name filterChanged
6109 * @eventOf ui.grid.core.api:PublicApi
6110 * @description is raised after the filter is changed. The nature
6111 * of the watch expression doesn't allow notification of what changed,
6112 * so the receiver of this event will need to re-extract the filter
6113 * conditions from the columns.
6116 this.registerEvent( 'core', 'filterChanged' );
6120 * @name setRowInvisible
6121 * @methodOf ui.grid.core.api:PublicApi
6122 * @description Sets an override on the row to make it always invisible,
6123 * which will override any filtering or other visibility calculations.
6124 * If the row is currently visible then sets it to invisible and calls
6125 * both grid refresh and emits the rowsVisibleChanged event
6126 * @param {object} rowEntity gridOptions.data[] array instance
6128 this.registerMethod( 'core', 'setRowInvisible', GridRow.prototype.setRowInvisible );
6132 * @name clearRowInvisible
6133 * @methodOf ui.grid.core.api:PublicApi
6134 * @description Clears any override on visibility for the row so that it returns to
6135 * using normal filtering and other visibility calculations.
6136 * If the row is currently invisible then sets it to visible and calls
6137 * both grid refresh and emits the rowsVisibleChanged event
6138 * TODO: if a filter is active then we can't just set it to visible?
6139 * @param {object} rowEntity gridOptions.data[] array instance
6141 this.registerMethod( 'core', 'clearRowInvisible', GridRow.prototype.clearRowInvisible );
6145 * @name getVisibleRows
6146 * @methodOf ui.grid.core.api:PublicApi
6147 * @description Returns all visible rows
6148 * @param {Grid} grid the grid you want to get visible rows from
6149 * @returns {array} an array of gridRow
6151 this.registerMethod( 'core', 'getVisibleRows', this.grid.getVisibleRows );
6155 * @name rowsVisibleChanged
6156 * @eventOf ui.grid.core.api:PublicApi
6157 * @description is raised after the rows that are visible
6158 * change. The filtering is zero-based, so it isn't possible
6159 * to say which rows changed (unlike in the selection feature).
6160 * We can plausibly know which row was changed when setRowInvisible
6161 * is called, but in that situation the user already knows which row
6162 * they changed. When a filter runs we don't know what changed,
6163 * and that is the one that would have been useful.
6166 this.registerEvent( 'core', 'rowsVisibleChanged' );
6170 * @name rowsRendered
6171 * @eventOf ui.grid.core.api:PublicApi
6172 * @description is raised after the cache of visible rows is changed.
6174 this.registerEvent( 'core', 'rowsRendered' );
6180 * @eventOf ui.grid.core.api:PublicApi
6181 * @description is raised when scroll begins. Is throttled, so won't be raised too frequently
6183 this.registerEvent( 'core', 'scrollBegin' );
6188 * @eventOf ui.grid.core.api:PublicApi
6189 * @description is raised when scroll has finished. Is throttled, so won't be raised too frequently
6191 this.registerEvent( 'core', 'scrollEnd' );
6195 * @name canvasHeightChanged
6196 * @eventOf ui.grid.core.api:PublicApi
6197 * @description is raised when the canvas height has changed
6199 * arguments: oldHeight, newHeight
6201 this.registerEvent( 'core', 'canvasHeightChanged');
6206 * @name ui.grid.class:suppressEvents
6207 * @methodOf ui.grid.class:GridApi
6208 * @description Used to execute a function while disabling the specified event listeners.
6209 * Disables the listenerFunctions, executes the callbackFn, and then enables
6210 * the listenerFunctions again
6211 * @param {object} listenerFuncs listenerFunc or array of listenerFuncs to suppress. These must be the same
6212 * functions that were used in the .on.eventName method
6213 * @param {object} callBackFn function to execute
6216 * var navigate = function (newRowCol, oldRowCol){
6217 * //do something on navigate
6220 * gridApi.cellNav.on.navigate(scope,navigate);
6223 * //call the scrollTo event and suppress our navigate listener
6224 * //scrollTo will still raise the event for other listeners
6225 * gridApi.suppressEvents(navigate, function(){
6226 * gridApi.cellNav.scrollTo(aRow, aCol);
6231 GridApi.prototype.suppressEvents = function (listenerFuncs, callBackFn) {
6233 var listeners = angular.isArray(listenerFuncs) ? listenerFuncs : [listenerFuncs];
6235 //find all registered listeners
6236 var foundListeners = self.listeners.filter(function(listener) {
6237 return listeners.some(function(l) {
6238 return listener.handler === l;
6242 //deregister all the listeners
6243 foundListeners.forEach(function(l){
6249 //reregister all the listeners
6250 foundListeners.forEach(function(l){
6251 l.dereg = registerEventWithAngular(l.eventId, l.handler, self.grid, l._this);
6258 * @name registerEvent
6259 * @methodOf ui.grid.class:GridApi
6260 * @description Registers a new event for the given feature. The event will get a
6261 * .raise and .on prepended to it
6263 * .raise.eventName() - takes no arguments
6266 * .on.eventName(scope, callBackFn, _this)
6268 * scope - a scope reference to add a deregister call to the scopes .$on('destroy'). Scope is optional and can be a null value,
6269 * but in this case you must deregister it yourself via the returned deregister function
6271 * callBackFn - The function to call
6273 * _this - optional this context variable for callbackFn. If omitted, grid.api will be used for the context
6275 * .on.eventName returns a dereg funtion that will remove the listener. It's not necessary to use it as the listener
6276 * will be removed when the scope is destroyed.
6277 * @param {string} featureName name of the feature that raises the event
6278 * @param {string} eventName name of the event
6280 GridApi.prototype.registerEvent = function (featureName, eventName) {
6282 if (!self[featureName]) {
6283 self[featureName] = {};
6286 var feature = self[featureName];
6292 var eventId = self.grid.id + featureName + eventName;
6294 // gridUtil.logDebug('Creating raise event method ' + featureName + '.raise.' + eventName);
6295 feature.raise[eventName] = function () {
6296 $rootScope.$emit.apply($rootScope, [eventId].concat(Array.prototype.slice.call(arguments)));
6299 // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName);
6300 feature.on[eventName] = function (scope, handler, _this) {
6301 if ( scope !== null && typeof(scope.$on) === 'undefined' ){
6302 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');
6305 var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this);
6307 //track our listener so we can turn off and on
6308 var listener = {handler: handler, dereg: deregAngularOn, eventId: eventId, scope: scope, _this:_this};
6309 self.listeners.push(listener);
6311 var removeListener = function(){
6313 var index = self.listeners.indexOf(listener);
6314 self.listeners.splice(index,1);
6317 //destroy tracking when scope is destroyed
6319 scope.$on('$destroy', function() {
6325 return removeListener;
6329 function registerEventWithAngular(eventId, handler, grid, _this) {
6330 return $rootScope.$on(eventId, function (event) {
6331 var args = Array.prototype.slice.call(arguments);
6332 args.splice(0, 1); //remove evt argument
6333 handler.apply(_this ? _this : grid.api, args);
6339 * @name registerEventsFromObject
6340 * @methodOf ui.grid.class:GridApi
6341 * @description Registers features and events from a simple objectMap.
6342 * eventObjectMap must be in this format (multiple features allowed)
6346 * eventNameOne:function(args){},
6347 * eventNameTwo:function(args){}
6351 * @param {object} eventObjectMap map of feature/event names
6353 GridApi.prototype.registerEventsFromObject = function (eventObjectMap) {
6356 angular.forEach(eventObjectMap, function (featProp, featPropName) {
6357 var feature = {name: featPropName, events: []};
6358 angular.forEach(featProp, function (prop, propName) {
6359 feature.events.push(propName);
6361 features.push(feature);
6364 features.forEach(function (feature) {
6365 feature.events.forEach(function (event) {
6366 self.registerEvent(feature.name, event);
6374 * @name registerMethod
6375 * @methodOf ui.grid.class:GridApi
6376 * @description Registers a new event for the given feature
6377 * @param {string} featureName name of the feature
6378 * @param {string} methodName name of the method
6379 * @param {object} callBackFn function to execute
6380 * @param {object} _this binds callBackFn 'this' to _this. Defaults to gridApi.grid
6382 GridApi.prototype.registerMethod = function (featureName, methodName, callBackFn, _this) {
6383 if (!this[featureName]) {
6384 this[featureName] = {};
6387 var feature = this[featureName];
6389 feature[methodName] = gridUtil.createBoundedWrapper(_this || this.grid, callBackFn);
6394 * @name registerMethodsFromObject
6395 * @methodOf ui.grid.class:GridApi
6396 * @description Registers features and methods from a simple objectMap.
6397 * eventObjectMap must be in this format (multiple features allowed)
6401 * methodNameOne:function(args){},
6402 * methodNameTwo:function(args){}
6404 * @param {object} eventObjectMap map of feature/event names
6405 * @param {object} _this binds this to _this for all functions. Defaults to gridApi.grid
6407 GridApi.prototype.registerMethodsFromObject = function (methodMap, _this) {
6410 angular.forEach(methodMap, function (featProp, featPropName) {
6411 var feature = {name: featPropName, methods: []};
6412 angular.forEach(featProp, function (prop, propName) {
6413 feature.methods.push({name: propName, fn: prop});
6415 features.push(feature);
6418 features.forEach(function (feature) {
6419 feature.methods.forEach(function (method) {
6420 self.registerMethod(feature.name, method.name, method.fn, _this);
6434 angular.module('ui.grid')
6435 .factory('GridColumn', ['gridUtil', 'uiGridConstants', 'i18nService', function(gridUtil, uiGridConstants, i18nService) {
6438 * ******************************************************************************************
6439 * PaulL1: Ugly hack here in documentation. These properties are clearly properties of GridColumn,
6440 * and need to be noted as such for those extending and building ui-grid itself.
6441 * However, from an end-developer perspective, they interact with all these through columnDefs,
6442 * and they really need to be documented there. I feel like they're relatively static, and
6443 * I can't find an elegant way for ngDoc to reference to both....so I've duplicated each
6444 * comment block. Ugh.
6451 * @propertyOf ui.grid.class:GridColumn
6452 * @description (mandatory) each column should have a name, although for backward
6453 * compatibility with 2.x name can be omitted if field is present
6460 * @propertyOf ui.grid.class:GridOptions.columnDef
6461 * @description (mandatory) each column should have a name, although for backward
6462 * compatibility with 2.x name can be omitted if field is present
6469 * @propertyOf ui.grid.class:GridColumn
6470 * @description Column name that will be shown in the header. If displayName is not
6471 * provided then one is generated using the name.
6478 * @propertyOf ui.grid.class:GridOptions.columnDef
6479 * @description Column name that will be shown in the header. If displayName is not
6480 * provided then one is generated using the name.
6487 * @propertyOf ui.grid.class:GridColumn
6488 * @description field must be provided if you wish to bind to a
6489 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6490 * array element. Can be a complex expression: <code>employee.address.city</code>, or can be a function: <code>employee.getFullAddress()</code>.
6491 * See the angular docs on binding expressions.
6498 * @propertyOf ui.grid.class:GridOptions.columnDef
6499 * @description field must be provided if you wish to bind to a
6500 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6501 * 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. *
6507 * @propertyOf ui.grid.class:GridColumn
6508 * @description Filter on this column.
6510 * <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>
6516 * @name ui.grid.class:GridColumn
6517 * @description Represents the viewModel for each column. Any state or methods needed for a Grid Column
6518 * are defined on this prototype
6519 * @param {ColumnDef} colDef the column def to associate with this column
6520 * @param {number} uid the unique and immutable uid we'd like to allocate to this column
6521 * @param {Grid} grid the grid we'd like to create this column in
6523 function GridColumn(colDef, uid, grid) {
6529 self.updateColumnDef(colDef, true);
6534 * @methodOf ui.grid.class:GridColumn
6535 * @description Hides the column by setting colDef.visible = false
6537 GridColumn.prototype.hideColumn = function() {
6538 this.colDef.visible = false;
6541 self.aggregationValue = undefined;
6543 // The footer cell registers to listen for the rowsRendered event, and calls this. Needed to be
6544 // in something with a scope so that the dereg would get called
6545 self.updateAggregationValue = function() {
6547 // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name);
6551 * @name aggregationType
6552 * @propertyOf ui.grid.class:GridOptions.columnDef
6553 * @description The aggregation that you'd like to show in the columnFooter for this
6554 * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`,
6555 * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`,
6556 * `uiGridConstants.aggregationTypes.max`.
6558 * You can also provide a function as the aggregation type, in this case your function needs to accept the full
6559 * set of visible rows, and return a value that should be shown
6561 if (!self.aggregationType) {
6562 self.aggregationValue = undefined;
6567 var visibleRows = self.grid.getVisibleRows();
6569 var cellValues = function(){
6571 visibleRows.forEach(function (row) {
6572 var cellValue = self.grid.getCellValue(row, self);
6573 var cellNumber = Number(cellValue);
6574 if (!isNaN(cellNumber)) {
6575 values.push(cellNumber);
6581 if (angular.isFunction(self.aggregationType)) {
6582 self.aggregationValue = self.aggregationType(visibleRows, self);
6584 else if (self.aggregationType === uiGridConstants.aggregationTypes.count) {
6585 self.aggregationValue = self.grid.getVisibleRowCount();
6587 else if (self.aggregationType === uiGridConstants.aggregationTypes.sum) {
6588 cellValues().forEach(function (value) {
6591 self.aggregationValue = result;
6593 else if (self.aggregationType === uiGridConstants.aggregationTypes.avg) {
6594 cellValues().forEach(function (value) {
6597 result = result / cellValues().length;
6598 self.aggregationValue = result;
6600 else if (self.aggregationType === uiGridConstants.aggregationTypes.min) {
6601 self.aggregationValue = Math.min.apply(null, cellValues());
6603 else if (self.aggregationType === uiGridConstants.aggregationTypes.max) {
6604 self.aggregationValue = Math.max.apply(null, cellValues());
6607 self.aggregationValue = '\u00A0';
6611 // var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true, context: self.name });
6615 * @name getAggregationValue
6616 * @methodOf ui.grid.class:GridColumn
6617 * @description gets the aggregation value based on the aggregation type for this column.
6618 * Debounced using scrollDebounce option setting
6620 this.getAggregationValue = function() {
6621 // if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) {
6622 // throttledUpdateAggregationValue();
6625 return self.aggregationValue;
6632 * @methodOf ui.grid.class:GridColumn
6633 * @name setPropertyOrDefault
6634 * @description Sets a property on the column using the passed in columnDef, and
6635 * setting the defaultValue if the value cannot be found on the colDef
6636 * @param {ColumnDef} colDef the column def to look in for the property value
6637 * @param {string} propName the property name we'd like to set
6638 * @param {object} defaultValue the value to use if the colDef doesn't provide the setting
6640 GridColumn.prototype.setPropertyOrDefault = function (colDef, propName, defaultValue) {
6643 // Use the column definition filter if we were passed it
6644 if (typeof(colDef[propName]) !== 'undefined' && colDef[propName]) {
6645 self[propName] = colDef[propName];
6647 // Otherwise use our own if it's set
6648 else if (typeof(self[propName]) !== 'undefined') {
6649 self[propName] = self[propName];
6651 // Default to empty object for the filter
6653 self[propName] = defaultValue ? defaultValue : {};
6662 * @propertyOf ui.grid.class:GridOptions.columnDef
6663 * @description sets the column width. Can be either
6664 * a number or a percentage, or an * for auto.
6666 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', width: 100},
6667 * { field: 'field2', width: '20%'},
6668 * { field: 'field3', width: '*' }]; </pre>
6675 * @propertyOf ui.grid.class:GridOptions.columnDef
6676 * @description sets the minimum column width. Should be a number.
6678 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', minWidth: 100}]; </pre>
6685 * @propertyOf ui.grid.class:GridOptions.columnDef
6686 * @description sets the maximum column width. Should be a number.
6688 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', maxWidth: 100}]; </pre>
6695 * @propertyOf ui.grid.class:GridOptions.columnDef
6696 * @description sets whether or not the column is visible
6697 * </br>Default is true
6699 * <pre> $scope.gridOptions.columnDefs = [
6700 * { field: 'field1', visible: true},
6701 * { field: 'field2', visible: false }
6709 * @propertyOf ui.grid.class:GridOptions.columnDef
6710 * @description An object of sort information, attributes are:
6712 * - direction: values are uiGridConstants.ASC or uiGridConstants.DESC
6713 * - ignoreSort: if set to true this sort is ignored (used by tree to manipulate the sort functionality)
6714 * - priority: says what order to sort the columns in (lower priority gets sorted first).
6717 * $scope.gridOptions.columnDefs = [{
6720 * direction: uiGridConstants.ASC,
6731 * @name sortingAlgorithm
6732 * @propertyOf ui.grid.class:GridOptions.columnDef
6733 * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters
6734 * like any normal sorting function with additional 'rowA', 'rowB', and 'direction' parameters
6735 * that are the row objects and the current direction of the sort respectively.
6742 * @propertyOf ui.grid.class:GridOptions.columnDef
6743 * @description Specify multiple filter fields.
6745 * <pre>$scope.gridOptions.columnDefs = [
6747 * field: 'field1', filters: [
6750 * condition: uiGridConstants.filter.STARTS_WITH,
6751 * placeholder: 'starts with...',
6752 * ariaLabel: 'Filter for field1',
6753 * flags: { caseSensitive: false },
6754 * type: uiGridConstants.filter.SELECT,
6755 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6758 * condition: uiGridConstants.filter.ENDS_WITH,
6759 * placeholder: 'ends with...'
6771 * @propertyOf ui.grid.class:GridColumn
6772 * @description Filters for this column. Includes 'term' property bound to filter input elements.
6776 * term: 'foo', // ngModel for <input>
6777 * condition: uiGridConstants.filter.STARTS_WITH,
6778 * placeholder: 'starts with...',
6779 * ariaLabel: 'Filter for foo',
6780 * flags: { caseSensitive: false },
6781 * type: uiGridConstants.filter.SELECT,
6782 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6786 * condition: uiGridConstants.filter.ENDS_WITH,
6787 * placeholder: 'ends with...'
6797 * @propertyOf ui.grid.class:GridOptions.columnDef
6798 * @description used to add menu items to a column. Refer to the tutorial on this
6799 * functionality. A number of settings are supported:
6801 * - title: controls the title that is displayed in the menu
6802 * - icon: the icon shown alongside that title
6803 * - action: the method to call when the menu is clicked
6804 * - shown: a function to evaluate to determine whether or not to show the item
6805 * - active: a function to evaluate to determine whether or not the item is currently selected
6806 * - context: context to pass to the action function, available in this.context in your handler
6807 * - leaveOpen: if set to true, the menu should stay open after the action, defaults to false
6809 * <pre> $scope.gridOptions.columnDefs = [
6810 * { field: 'field1', menuItems: [
6812 * title: 'Outer Scope Alert',
6813 * icon: 'ui-grid-icon-info-circled',
6814 * action: function($event) {
6815 * this.context.blargh(); // $scope.blargh() would work too, this is just an example
6817 * shown: function() { return true; },
6818 * active: function() { return true; },
6823 * action: function() {
6824 * alert('Grid ID: ' + this.grid.id);
6833 * @methodOf ui.grid.class:GridColumn
6834 * @name updateColumnDef
6835 * @description Moves settings from the columnDef down onto the column,
6836 * and sets properties as appropriate
6837 * @param {ColumnDef} colDef the column def to look in for the property value
6838 * @param {boolean} isNew whether the column is being newly created, if not
6839 * we're updating an existing column, and some items such as the sort shouldn't
6842 GridColumn.prototype.updateColumnDef = function(colDef, isNew) {
6845 self.colDef = colDef;
6847 if (colDef.name === undefined) {
6848 throw new Error('colDef.name is required for column at index ' + self.grid.options.columnDefs.indexOf(colDef));
6851 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6853 if (!angular.isNumber(self.width) || !self.hasCustomWidth || colDef.allowCustomWidthOverride) {
6854 var colDefWidth = colDef.width;
6855 var parseErrorMsg = "Cannot parse column width '" + colDefWidth + "' for column named '" + colDef.name + "'";
6856 self.hasCustomWidth = false;
6858 if (!angular.isString(colDefWidth) && !angular.isNumber(colDefWidth)) {
6860 } else if (angular.isString(colDefWidth)) {
6861 // See if it ends with a percent
6862 if (gridUtil.endsWith(colDefWidth, '%')) {
6863 // If so we should be able to parse the non-percent-sign part to a number
6864 var percentStr = colDefWidth.replace(/%/g, '');
6865 var percent = parseInt(percentStr, 10);
6866 if (isNaN(percent)) {
6867 throw new Error(parseErrorMsg);
6869 self.width = colDefWidth;
6871 // And see if it's a number string
6872 else if (colDefWidth.match(/^(\d+)$/)) {
6873 self.width = parseInt(colDefWidth.match(/^(\d+)$/)[1], 10);
6875 // Otherwise it should be a string of asterisks
6876 else if (colDefWidth.match(/^\*+$/)) {
6877 self.width = colDefWidth;
6879 // No idea, throw an Error
6881 throw new Error(parseErrorMsg);
6884 // Is a number, use it as the width
6886 self.width = colDefWidth;
6890 ['minWidth', 'maxWidth'].forEach(function (name) {
6891 var minOrMaxWidth = colDef[name];
6892 var parseErrorMsg = "Cannot parse column " + name + " '" + minOrMaxWidth + "' for column named '" + colDef.name + "'";
6894 if (!angular.isString(minOrMaxWidth) && !angular.isNumber(minOrMaxWidth)) {
6895 //Sets default minWidth and maxWidth values
6896 self[name] = ((name === 'minWidth') ? 30 : 9000);
6897 } else if (angular.isString(minOrMaxWidth)) {
6898 if (minOrMaxWidth.match(/^(\d+)$/)) {
6899 self[name] = parseInt(minOrMaxWidth.match(/^(\d+)$/)[1], 10);
6901 throw new Error(parseErrorMsg);
6904 self[name] = minOrMaxWidth;
6908 //use field if it is defined; name if it is not
6909 self.field = (colDef.field === undefined) ? colDef.name : colDef.field;
6911 if ( typeof( self.field ) !== 'string' ){
6912 gridUtil.logError( 'Field is not a string, this is likely to break the code, Field is: ' + self.field );
6915 self.name = colDef.name;
6917 // Use colDef.displayName as long as it's not undefined, otherwise default to the field name
6918 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6920 //self.originalIndex = index;
6922 self.aggregationType = angular.isDefined(colDef.aggregationType) ? colDef.aggregationType : null;
6923 self.footerCellTemplate = angular.isDefined(colDef.footerCellTemplate) ? colDef.footerCellTemplate : null;
6928 * @propertyOf ui.grid.class:GridOptions.columnDef
6929 * @description Whether or not to show a tooltip when a user hovers over the cell.
6930 * If set to false, no tooltip. If true, the cell value is shown in the tooltip (useful
6931 * if you have long values in your cells), if a function then that function is called
6932 * passing in the row and the col `cellTooltip( row, col )`, and the return value is shown in the tooltip,
6933 * if it is a static string then displays that static string.
6938 if ( typeof(colDef.cellTooltip) === 'undefined' || colDef.cellTooltip === false ) {
6939 self.cellTooltip = false;
6940 } else if ( colDef.cellTooltip === true ){
6941 self.cellTooltip = function(row, col) {
6942 return self.grid.getCellValue( row, col );
6944 } else if (typeof(colDef.cellTooltip) === 'function' ){
6945 self.cellTooltip = colDef.cellTooltip;
6947 self.cellTooltip = function ( row, col ){
6948 return col.colDef.cellTooltip;
6954 * @name headerTooltip
6955 * @propertyOf ui.grid.class:GridOptions.columnDef
6956 * @description Whether or not to show a tooltip when a user hovers over the header cell.
6957 * If set to false, no tooltip. If true, the displayName is shown in the tooltip (useful
6958 * if you have long values in your headers), if a function then that function is called
6959 * passing in the row and the col `headerTooltip( col )`, and the return value is shown in the tooltip,
6960 * if a static string then shows that static string.
6965 if ( typeof(colDef.headerTooltip) === 'undefined' || colDef.headerTooltip === false ) {
6966 self.headerTooltip = false;
6967 } else if ( colDef.headerTooltip === true ){
6968 self.headerTooltip = function(col) {
6969 return col.displayName;
6971 } else if (typeof(colDef.headerTooltip) === 'function' ){
6972 self.headerTooltip = colDef.headerTooltip;
6974 self.headerTooltip = function ( col ) {
6975 return col.colDef.headerTooltip;
6982 * @name footerCellClass
6983 * @propertyOf ui.grid.class:GridOptions.columnDef
6984 * @description footerCellClass can be a string specifying the class to append to a cell
6985 * or it can be a function(row,rowRenderIndex, col, colRenderIndex) that returns a class name
6988 self.footerCellClass = colDef.footerCellClass;
6993 * @propertyOf ui.grid.class:GridOptions.columnDef
6994 * @description cellClass can be a string specifying the class to append to a cell
6995 * or it can be a function(row,rowRenderIndex, col, colRenderIndex) that returns a class name
6998 self.cellClass = colDef.cellClass;
7002 * @name headerCellClass
7003 * @propertyOf ui.grid.class:GridOptions.columnDef
7004 * @description headerCellClass can be a string specifying the class to append to a cell
7005 * or it can be a function(row,rowRenderIndex, col, colRenderIndex) that returns a class name
7008 self.headerCellClass = colDef.headerCellClass;
7013 * @propertyOf ui.grid.class:GridOptions.columnDef
7014 * @description cellFilter is a filter to apply to the content of each cell
7017 * gridOptions.columnDefs[0].cellFilter = 'date'
7020 self.cellFilter = colDef.cellFilter ? colDef.cellFilter : "";
7024 * @name sortCellFiltered
7025 * @propertyOf ui.grid.class:GridOptions.columnDef
7026 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7027 * sorting the data. Note that when using this option uiGrid will assume that the displayed value is
7028 * a string, and use the {@link ui.grid.class:RowSorter#sortAlpha sortAlpha} `sortFn`. It is possible
7029 * 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}
7030 * for the column which hanldes the returned type. You may specify one of the `sortingAlgorithms`
7031 * found in the {@link ui.grid.RowSorter rowSorter} service.
7033 self.sortCellFiltered = colDef.sortCellFiltered ? true : false;
7037 * @name filterCellFiltered
7038 * @propertyOf ui.grid.class:GridOptions.columnDef
7039 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7040 * applying "search" `filters`.
7042 self.filterCellFiltered = colDef.filterCellFiltered ? true : false;
7046 * @name headerCellFilter
7047 * @propertyOf ui.grid.class:GridOptions.columnDef
7048 * @description headerCellFilter is a filter to apply to the content of the column header
7051 * gridOptions.columnDefs[0].headerCellFilter = 'translate'
7054 self.headerCellFilter = colDef.headerCellFilter ? colDef.headerCellFilter : "";
7058 * @name footerCellFilter
7059 * @propertyOf ui.grid.class:GridOptions.columnDef
7060 * @description footerCellFilter is a filter to apply to the content of the column footer
7063 * gridOptions.columnDefs[0].footerCellFilter = 'date'
7066 self.footerCellFilter = colDef.footerCellFilter ? colDef.footerCellFilter : "";
7068 self.visible = gridUtil.isNullOrUndefined(colDef.visible) || colDef.visible;
7070 self.headerClass = colDef.headerClass;
7071 //self.cursor = self.sortable ? 'pointer' : 'default';
7073 // Turn on sorting by default
7074 self.enableSorting = typeof(colDef.enableSorting) !== 'undefined' ? colDef.enableSorting : true;
7075 self.sortingAlgorithm = colDef.sortingAlgorithm;
7079 * @name sortDirectionCycle
7080 * @propertyOf ui.grid.class:GridOptions.columnDef
7081 * @description (optional) An array of sort directions, specifying the order that they
7082 * should cycle through as the user repeatedly clicks on the column heading.
7083 * The default is `[null, uiGridConstants.ASC, uiGridConstants.DESC]`. Null
7084 * refers to the unsorted state. This does not affect the initial sort
7085 * direction; use the {@link ui.grid.class:GridOptions.columnDef#sort sort}
7086 * property for that. If
7087 * {@link ui.grid.class:GridOptions.columnDef#suppressRemoveSort suppressRemoveSort}
7088 * is also set, the unsorted state will be skipped even if it is listed here.
7089 * Each direction may not appear in the list more than once (e.g. `[ASC,
7090 * DESC, DESC]` is not allowed), and the list may not be empty.
7092 self.sortDirectionCycle = typeof(colDef.sortDirectionCycle) !== 'undefined' ?
7093 colDef.sortDirectionCycle :
7094 [null, uiGridConstants.ASC, uiGridConstants.DESC];
7098 * @name suppressRemoveSort
7099 * @propertyOf ui.grid.class:GridOptions.columnDef
7100 * @description (optional) False by default. When enabled, this setting hides the removeSort option
7101 * in the menu, and prevents users from manually removing the sort
7103 if ( typeof(self.suppressRemoveSort) === 'undefined'){
7104 self.suppressRemoveSort = typeof(colDef.suppressRemoveSort) !== 'undefined' ? colDef.suppressRemoveSort : false;
7109 * @name enableFiltering
7110 * @propertyOf ui.grid.class:GridOptions.columnDef
7111 * @description turn off filtering for an individual column, where
7112 * you've turned on filtering for the overall grid
7115 * gridOptions.columnDefs[0].enableFiltering = false;
7118 // Turn on filtering by default (it's disabled by default at the Grid level)
7119 self.enableFiltering = typeof(colDef.enableFiltering) !== 'undefined' ? colDef.enableFiltering : true;
7121 // self.menuItems = colDef.menuItems;
7122 self.setPropertyOrDefault(colDef, 'menuItems', []);
7124 // Use the column definition sort if we were passed it, but only if this is a newly added column
7126 self.setPropertyOrDefault(colDef, 'sort');
7129 // Set up default filters array for when one is not provided.
7130 // In other words, this (in column def):
7132 // filter: { term: 'something', flags: {}, condition: [CONDITION] }
7134 // is just shorthand for this:
7136 // filters: [{ term: 'something', flags: {}, condition: [CONDITION] }]
7138 var defaultFilters = [];
7139 if (colDef.filter) {
7140 defaultFilters.push(colDef.filter);
7142 else if ( colDef.filters ){
7143 defaultFilters = colDef.filters;
7145 // Add an empty filter definition object, which will
7146 // translate to a guessed condition and no pre-populated
7147 // value for the filter <input>.
7148 defaultFilters.push({});
7154 * @propertyOf ui.grid.class:GridOptions.columnDef
7155 * @description Specify a single filter field on this column.
7157 * A filter consists of a condition, a term, and a placeholder:
7159 * - condition defines how rows are chosen as matching the filter term. This can be set to
7160 * one of the constants in uiGridConstants.filter, or you can supply a custom filter function
7161 * that gets passed the following arguments: [searchTerm, cellValue, row, column].
7162 * - term: If set, the filter field will be pre-populated
7164 * - placeholder: String that will be set to the `<input>.placeholder` attribute.
7165 * - ariaLabel: String that will be set to the `<input>.ariaLabel` attribute. This is what is read as a label to screen reader users.
7166 * - noTerm: set this to true if you have defined a custom function in condition, and
7167 * your custom function doesn't require a term (so it can run even when the term is null)
7168 * - flags: only flag currently available is `caseSensitive`, set to false if you don't want
7169 * case sensitive matching
7170 * - type: defaults to uiGridConstants.filter.INPUT, which gives a text box. If set to uiGridConstants.filter.SELECT
7171 * then a select box will be shown with options selectOptions
7172 * - selectOptions: options in the format `[ { value: 1, label: 'male' }]`. No i18n filter is provided, you need
7173 * to perform the i18n on the values before you provide them
7174 * - disableCancelFilterButton: defaults to false. If set to true then the 'x' button that cancels/clears the filter
7175 * will not be shown.
7177 * <pre>$scope.gridOptions.columnDefs = [
7182 * condition: uiGridConstants.filter.STARTS_WITH,
7183 * placeholder: 'starts with...',
7184 * ariaLabel: 'Starts with filter for field1',
7185 * flags: { caseSensitive: false },
7186 * type: uiGridConstants.filter.SELECT,
7187 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ],
7188 * disableCancelFilterButton: true
7203 condition: uiGridConstants.filter.CONTAINS,
7204 placeholder: 'my placeholder',
7205 ariaLabel: 'Starts with filter for field1',
7214 // Only set filter if this is a newly added column, if we're updating an existing
7215 // column then we don't want to put the default filter back if the user may have already
7217 // However, we do want to keep the settings if they change, just not the term
7219 self.setPropertyOrDefault(colDef, 'filter');
7220 self.setPropertyOrDefault(colDef, 'filters', defaultFilters);
7221 } else if ( self.filters.length === defaultFilters.length ) {
7222 self.filters.forEach( function( filter, index ){
7223 if (typeof(defaultFilters[index].placeholder) !== 'undefined') {
7224 filter.placeholder = defaultFilters[index].placeholder;
7226 if (typeof(defaultFilters[index].ariaLabel) !== 'undefined') {
7227 filter.ariaLabel = defaultFilters[index].ariaLabel;
7229 if (typeof(defaultFilters[index].flags) !== 'undefined') {
7230 filter.flags = defaultFilters[index].flags;
7232 if (typeof(defaultFilters[index].type) !== 'undefined') {
7233 filter.type = defaultFilters[index].type;
7235 if (typeof(defaultFilters[index].selectOptions) !== 'undefined') {
7236 filter.selectOptions = defaultFilters[index].selectOptions;
7241 // Remove this column from the grid sorting, include inside build columns so has
7242 // access to self - all seems a bit dodgy but doesn't work otherwise so have left
7244 GridColumn.prototype.unsort = function () {
7246 self.grid.api.core.raise.sortChanged( self.grid, self.grid.getColumnSorting() );
7255 * @methodOf ui.grid.class:GridColumn
7256 * @description Returns the class name for the column
7257 * @param {bool} prefixDot if true, will return .className instead of className
7259 GridColumn.prototype.getColClass = function (prefixDot) {
7260 var cls = uiGridConstants.COL_CLASS_PREFIX + this.uid;
7262 return prefixDot ? '.' + cls : cls;
7267 * @name isPinnedLeft
7268 * @methodOf ui.grid.class:GridColumn
7269 * @description Returns true if column is in the left render container
7271 GridColumn.prototype.isPinnedLeft = function () {
7272 return this.renderContainer === 'left';
7277 * @name isPinnedRight
7278 * @methodOf ui.grid.class:GridColumn
7279 * @description Returns true if column is in the right render container
7281 GridColumn.prototype.isPinnedRight = function () {
7282 return this.renderContainer === 'right';
7288 * @name getColClassDefinition
7289 * @methodOf ui.grid.class:GridColumn
7290 * @description Returns the class definition for th column
7292 GridColumn.prototype.getColClassDefinition = function () {
7293 return ' .grid' + this.grid.id + ' ' + this.getColClass(true) + ' { min-width: ' + this.drawnWidth + 'px; max-width: ' + this.drawnWidth + 'px; }';
7298 * @name getRenderContainer
7299 * @methodOf ui.grid.class:GridColumn
7300 * @description Returns the render container object that this column belongs to.
7302 * Columns will be default be in the `body` render container if they aren't allocated to one specifically.
7304 GridColumn.prototype.getRenderContainer = function getRenderContainer() {
7307 var containerId = self.renderContainer;
7309 if (containerId === null || containerId === '' || containerId === undefined) {
7310 containerId = 'body';
7313 return self.grid.renderContainers[containerId];
7319 * @methodOf ui.grid.class:GridColumn
7320 * @description Makes the column visible by setting colDef.visible = true
7322 GridColumn.prototype.showColumn = function() {
7323 this.colDef.visible = true;
7329 * @name aggregationHideLabel
7330 * @propertyOf ui.grid.class:GridOptions.columnDef
7331 * @description defaults to false, if set to true hides the label text
7332 * in the aggregation footer, so only the value is displayed.
7337 * @name getAggregationText
7338 * @methodOf ui.grid.class:GridColumn
7339 * @description Gets the aggregation label from colDef.aggregationLabel if
7340 * specified or by using i18n, including deciding whether or not to display
7341 * based on colDef.aggregationHideLabel.
7343 * @param {string} label the i18n lookup value to use for the column label
7346 GridColumn.prototype.getAggregationText = function () {
7348 if ( self.colDef.aggregationHideLabel ){
7351 else if ( self.colDef.aggregationLabel ) {
7352 return self.colDef.aggregationLabel;
7355 switch ( self.colDef.aggregationType ){
7356 case uiGridConstants.aggregationTypes.count:
7357 return i18nService.getSafeText('aggregation.count');
7358 case uiGridConstants.aggregationTypes.sum:
7359 return i18nService.getSafeText('aggregation.sum');
7360 case uiGridConstants.aggregationTypes.avg:
7361 return i18nService.getSafeText('aggregation.avg');
7362 case uiGridConstants.aggregationTypes.min:
7363 return i18nService.getSafeText('aggregation.min');
7364 case uiGridConstants.aggregationTypes.max:
7365 return i18nService.getSafeText('aggregation.max');
7372 GridColumn.prototype.getCellTemplate = function () {
7375 return self.cellTemplatePromise;
7378 GridColumn.prototype.getCompiledElementFn = function () {
7381 return self.compiledElementFnDefer.promise;
7391 angular.module('ui.grid')
7392 .factory('GridOptions', ['gridUtil','uiGridConstants', function(gridUtil,uiGridConstants) {
7396 * @name ui.grid.class:GridOptions
7397 * @description Default GridOptions class. GridOptions are defined by the application developer and overlaid
7398 * over this object. Setting gridOptions within your controller is the most common method for an application
7399 * developer to configure the behaviour of their ui-grid
7401 * @example To define your gridOptions within your controller:
7402 * <pre>$scope.gridOptions = {
7403 * data: $scope.myData,
7405 * { name: 'field1', displayName: 'pretty display name' },
7406 * { name: 'field2', visible: false }
7410 * You can then use this within your html template, when you define your grid:
7411 * <pre><div ui-grid="gridOptions"></div></pre>
7413 * To provide default options for all of the grids within your application, use an angular
7414 * decorator to modify the GridOptions factory.
7416 * app.config(function($provide){
7417 * $provide.decorator('GridOptions',function($delegate){
7419 * gridOptions = angular.copy($delegate);
7420 * gridOptions.initialize = function(options) {
7422 * initOptions = $delegate.initialize(options);
7423 * initOptions.enableColumnMenus = false;
7424 * return initOptions;
7426 * return gridOptions;
7432 initialize: function( baseOptions ){
7435 * @name onRegisterApi
7436 * @propertyOf ui.grid.class:GridOptions
7437 * @description A callback that returns the gridApi once the grid is instantiated, which is
7438 * then used to interact with the grid programatically.
7440 * Note that the gridApi.core.renderingComplete event is identical to this
7441 * callback, but has the advantage that it can be called from multiple places
7446 * $scope.gridOptions.onRegisterApi = function ( gridApi ) {
7447 * $scope.gridApi = gridApi;
7448 * $scope.gridApi.selection.selectAllRows( $scope.gridApi.grid );
7453 baseOptions.onRegisterApi = baseOptions.onRegisterApi || angular.noop();
7458 * @propertyOf ui.grid.class:GridOptions
7459 * @description (mandatory) Array of data to be rendered into the grid, providing the data source or data binding for
7462 * Most commonly the data is an array of objects, where each object has a number of attributes.
7463 * Each attribute automatically becomes a column in your grid. This array could, for example, be sourced from
7464 * an angularJS $resource query request. The array can also contain complex objects, refer the binding tutorial
7465 * for examples of that.
7467 * The most flexible usage is to set your data on $scope:
7469 * `$scope.data = data;`
7471 * And then direct the grid to resolve whatever is in $scope.data:
7473 * `$scope.gridOptions.data = 'data';`
7475 * This is the most flexible approach as it allows you to replace $scope.data whenever you feel like it without
7476 * getting pointer issues.
7478 * Alternatively you can directly set the data array:
7480 * `$scope.gridOptions.data = [ ];`
7483 * `$http.get('/data/100.json')
7484 * .success(function(data) {
7485 * $scope.myData = data;
7486 * $scope.gridOptions.data = $scope.myData;
7489 * Where you do this, you need to take care in updating the data - you can't just update `$scope.myData` to some other
7490 * array, you need to update $scope.gridOptions.data to point to that new array as well.
7493 baseOptions.data = baseOptions.data || [];
7498 * @propertyOf ui.grid.class:GridOptions
7499 * @description Array of columnDef objects. Only required property is name.
7500 * The individual options available in columnDefs are documented in the
7501 * {@link ui.grid.class:GridOptions.columnDef columnDef} section
7502 * </br>_field property can be used in place of name for backwards compatibility with 2.x_
7505 * <pre>var columnDefs = [{name:'field1'}, {name:'field2'}];</pre>
7508 baseOptions.columnDefs = baseOptions.columnDefs || [];
7512 * @name ui.grid.class:GridOptions.columnDef
7513 * @description Definition / configuration of an individual column, which would typically be
7514 * one of many column definitions within the gridOptions.columnDefs array
7516 * <pre>{name:'field1', field: 'field1', filter: { term: 'xxx' }}</pre>
7523 * @name excludeProperties
7524 * @propertyOf ui.grid.class:GridOptions
7525 * @description Array of property names in data to ignore when auto-generating column names. Provides the
7526 * inverse of columnDefs - columnDefs is a list of columns to include, excludeProperties is a list of columns
7529 * If columnDefs is defined, this will be ignored.
7531 * Defaults to ['$$hashKey']
7534 baseOptions.excludeProperties = baseOptions.excludeProperties || ['$$hashKey'];
7538 * @name enableRowHashing
7539 * @propertyOf ui.grid.class:GridOptions
7540 * @description True by default. When enabled, this setting allows uiGrid to add
7541 * `$$hashKey`-type properties (similar to Angular) to elements in the `data` array. This allows
7542 * the grid to maintain state while vastly speeding up the process of altering `data` by adding/moving/removing rows.
7544 * Note that this DOES add properties to your data that you may not want, but they are stripped out when using `angular.toJson()`. IF
7545 * 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
7546 * and are altering the data set often.
7548 baseOptions.enableRowHashing = baseOptions.enableRowHashing !== false;
7553 * @methodOf ui.grid.class:GridOptions
7554 * @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).
7556 * By default it returns the `$$hashKey` property if it exists. If it doesn't it uses gridUtil.nextUid() to generate one
7558 baseOptions.rowIdentity = baseOptions.rowIdentity || function rowIdentity(row) {
7559 return gridUtil.hashKey(row);
7564 * @name getRowIdentity
7565 * @methodOf ui.grid.class:GridOptions
7566 * @description This function returns the identity value uniquely identifying this row, if one is not present it does not set it.
7568 * By default it returns the `$$hashKey` property but can be overridden to use any property or set of properties you want.
7570 baseOptions.getRowIdentity = baseOptions.getRowIdentity || function getRowIdentity(row) {
7571 return row.$$hashKey;
7576 * @name flatEntityAccess
7577 * @propertyOf ui.grid.class:GridOptions
7578 * @description Set to true if your columns are all related directly to fields in a flat object structure - i.e.
7579 * each of your columns associate directly with a property on each of the entities in your data array.
7581 * In that situation we can avoid all the logic associated with complex binding to functions or to properties of sub-objects,
7582 * which can provide a significant speed improvement with large data sets when filtering or sorting.
7586 baseOptions.flatEntityAccess = baseOptions.flatEntityAccess === true;
7591 * @propertyOf ui.grid.class:GridOptions
7592 * @description True by default. When set to false, this setting will replace the
7593 * standard header template with '<div></div>', resulting in no header being shown.
7595 baseOptions.showHeader = typeof(baseOptions.showHeader) !== "undefined" ? baseOptions.showHeader : true;
7597 /* (NOTE): Don't show this in the docs. We only use it internally
7599 * @name headerRowHeight
7600 * @propertyOf ui.grid.class:GridOptions
7601 * @description The height of the header in pixels, defaults to 30
7604 if (!baseOptions.showHeader) {
7605 baseOptions.headerRowHeight = 0;
7608 baseOptions.headerRowHeight = typeof(baseOptions.headerRowHeight) !== "undefined" ? baseOptions.headerRowHeight : 30;
7614 * @propertyOf ui.grid.class:GridOptions
7615 * @description The height of the row in pixels, defaults to 30
7618 baseOptions.rowHeight = baseOptions.rowHeight || 30;
7622 * @name minRowsToShow
7623 * @propertyOf ui.grid.class:GridOptions
7624 * @description Minimum number of rows to show when the grid doesn't have a defined height. Defaults to "10".
7626 baseOptions.minRowsToShow = typeof(baseOptions.minRowsToShow) !== "undefined" ? baseOptions.minRowsToShow : 10;
7630 * @name showGridFooter
7631 * @propertyOf ui.grid.class:GridOptions
7632 * @description Whether or not to show the footer, defaults to false
7633 * The footer display Total Rows and Visible Rows (filtered rows)
7635 baseOptions.showGridFooter = baseOptions.showGridFooter === true;
7639 * @name showColumnFooter
7640 * @propertyOf ui.grid.class:GridOptions
7641 * @description Whether or not to show the column footer, defaults to false
7642 * The column footer displays column aggregates
7644 baseOptions.showColumnFooter = baseOptions.showColumnFooter === true;
7648 * @name columnFooterHeight
7649 * @propertyOf ui.grid.class:GridOptions
7650 * @description The height of the footer rows (column footer and grid footer) in pixels
7653 baseOptions.columnFooterHeight = typeof(baseOptions.columnFooterHeight) !== "undefined" ? baseOptions.columnFooterHeight : 30;
7654 baseOptions.gridFooterHeight = typeof(baseOptions.gridFooterHeight) !== "undefined" ? baseOptions.gridFooterHeight : 30;
7656 baseOptions.columnWidth = typeof(baseOptions.columnWidth) !== "undefined" ? baseOptions.columnWidth : 50;
7660 * @name maxVisibleColumnCount
7661 * @propertyOf ui.grid.class:GridOptions
7662 * @description Defaults to 200
7665 baseOptions.maxVisibleColumnCount = typeof(baseOptions.maxVisibleColumnCount) !== "undefined" ? baseOptions.maxVisibleColumnCount : 200;
7669 * @name virtualizationThreshold
7670 * @propertyOf ui.grid.class:GridOptions
7671 * @description Turn virtualization on when number of data elements goes over this number, defaults to 20
7673 baseOptions.virtualizationThreshold = typeof(baseOptions.virtualizationThreshold) !== "undefined" ? baseOptions.virtualizationThreshold : 20;
7677 * @name columnVirtualizationThreshold
7678 * @propertyOf ui.grid.class:GridOptions
7679 * @description Turn virtualization on when number of columns goes over this number, defaults to 10
7681 baseOptions.columnVirtualizationThreshold = typeof(baseOptions.columnVirtualizationThreshold) !== "undefined" ? baseOptions.columnVirtualizationThreshold : 10;
7686 * @propertyOf ui.grid.class:GridOptions
7687 * @description Extra rows to to render outside of the viewport, which helps with smoothness of scrolling.
7690 baseOptions.excessRows = typeof(baseOptions.excessRows) !== "undefined" ? baseOptions.excessRows : 4;
7693 * @name scrollThreshold
7694 * @propertyOf ui.grid.class:GridOptions
7695 * @description Defaults to 4
7697 baseOptions.scrollThreshold = typeof(baseOptions.scrollThreshold) !== "undefined" ? baseOptions.scrollThreshold : 4;
7701 * @name excessColumns
7702 * @propertyOf ui.grid.class:GridOptions
7703 * @description Extra columns to to render outside of the viewport, which helps with smoothness of scrolling.
7706 baseOptions.excessColumns = typeof(baseOptions.excessColumns) !== "undefined" ? baseOptions.excessColumns : 4;
7709 * @name horizontalScrollThreshold
7710 * @propertyOf ui.grid.class:GridOptions
7711 * @description Defaults to 4
7713 baseOptions.horizontalScrollThreshold = typeof(baseOptions.horizontalScrollThreshold) !== "undefined" ? baseOptions.horizontalScrollThreshold : 2;
7718 * @name aggregationCalcThrottle
7719 * @propertyOf ui.grid.class:GridOptions
7720 * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 500ms
7722 baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 500;
7726 * @name wheelScrollThrottle
7727 * @propertyOf ui.grid.class:GridOptions
7728 * @description Default time in milliseconds to throttle scroll events to, defaults to 70ms
7730 baseOptions.wheelScrollThrottle = typeof(baseOptions.wheelScrollThrottle) !== "undefined" ? baseOptions.wheelScrollThrottle : 70;
7735 * @name scrollDebounce
7736 * @propertyOf ui.grid.class:GridOptions
7737 * @description Default time in milliseconds to debounce scroll events, defaults to 300ms
7739 baseOptions.scrollDebounce = typeof(baseOptions.scrollDebounce) !== "undefined" ? baseOptions.scrollDebounce : 300;
7743 * @name enableSorting
7744 * @propertyOf ui.grid.class:GridOptions
7745 * @description True by default. When enabled, this setting adds sort
7746 * widgets to the column headers, allowing sorting of the data for the entire grid.
7747 * Sorting can then be disabled on individual columns using the columnDefs.
7749 baseOptions.enableSorting = baseOptions.enableSorting !== false;
7753 * @name enableFiltering
7754 * @propertyOf ui.grid.class:GridOptions
7755 * @description False by default. When enabled, this setting adds filter
7756 * boxes to each column header, allowing filtering within the column for the entire grid.
7757 * Filtering can then be disabled on individual columns using the columnDefs.
7759 baseOptions.enableFiltering = baseOptions.enableFiltering === true;
7763 * @name enableColumnMenus
7764 * @propertyOf ui.grid.class:GridOptions
7765 * @description True by default. When enabled, this setting displays a column
7766 * menu within each column.
7768 baseOptions.enableColumnMenus = baseOptions.enableColumnMenus !== false;
7772 * @name enableVerticalScrollbar
7773 * @propertyOf ui.grid.class:GridOptions
7774 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the vertical scrollbar for the grid.
7775 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7777 baseOptions.enableVerticalScrollbar = typeof(baseOptions.enableVerticalScrollbar) !== "undefined" ? baseOptions.enableVerticalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7781 * @name enableHorizontalScrollbar
7782 * @propertyOf ui.grid.class:GridOptions
7783 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the horizontal scrollbar for the grid.
7784 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7786 baseOptions.enableHorizontalScrollbar = typeof(baseOptions.enableHorizontalScrollbar) !== "undefined" ? baseOptions.enableHorizontalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7790 * @name enableMinHeightCheck
7791 * @propertyOf ui.grid.class:GridOptions
7792 * @description True by default. When enabled, a newly initialized grid will check to see if it is tall enough to display
7793 * at least one row of data. If the grid is not tall enough, it will resize the DOM element to display minRowsToShow number
7796 baseOptions.enableMinHeightCheck = baseOptions.enableMinHeightCheck !== false;
7800 * @name minimumColumnSize
7801 * @propertyOf ui.grid.class:GridOptions
7802 * @description Columns can't be smaller than this, defaults to 10 pixels
7804 baseOptions.minimumColumnSize = typeof(baseOptions.minimumColumnSize) !== "undefined" ? baseOptions.minimumColumnSize : 10;
7809 * @methodOf ui.grid.class:GridOptions
7810 * @description By default, rows are compared using object equality. This option can be overridden
7811 * to compare on any data item property or function
7812 * @param {object} entityA First Data Item to compare
7813 * @param {object} entityB Second Data Item to compare
7815 baseOptions.rowEquality = baseOptions.rowEquality || function(entityA, entityB) {
7816 return entityA === entityB;
7821 * @name headerTemplate
7822 * @propertyOf ui.grid.class:GridOptions
7823 * @description Null by default. When provided, this setting uses a custom header
7824 * template, rather than the default template. Can be set to either the name of a template file:
7825 * <pre> $scope.gridOptions.headerTemplate = 'header_template.html';</pre>
7827 * <pre> $scope.gridOptions.headerTemplate = '<div class="ui-grid-top-panel" style="text-align: center">I am a Custom Grid Header</div>'</pre>
7828 * or the id of a precompiled template (TBD how to use this).
7829 * </br>Refer to the custom header tutorial for more information.
7830 * If you want no header at all, you can set to an empty div:
7831 * <pre> $scope.gridOptions.headerTemplate = '<div></div>';</pre>
7833 * If you want to only have a static header, then you can set to static content. If
7834 * you want to tailor the existing column headers, then you should look at the
7835 * current 'ui-grid-header.html' template in github as your starting point.
7838 baseOptions.headerTemplate = baseOptions.headerTemplate || null;
7842 * @name footerTemplate
7843 * @propertyOf ui.grid.class:GridOptions
7844 * @description (optional) ui-grid/ui-grid-footer by default. This footer shows the per-column
7845 * aggregation totals.
7846 * 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
7847 * <pre>'<div class="ui-grid-bottom-panel" style="text-align: center">I am a Custom Grid Footer</div>'</pre>, or the id
7848 * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information.
7850 baseOptions.footerTemplate = baseOptions.footerTemplate || 'ui-grid/ui-grid-footer';
7854 * @name gridFooterTemplate
7855 * @propertyOf ui.grid.class:GridOptions
7856 * @description (optional) ui-grid/ui-grid-grid-footer by default. This template by default shows the
7857 * total items at the bottom of the grid, and the selected items if selection is enabled.
7859 baseOptions.gridFooterTemplate = baseOptions.gridFooterTemplate || 'ui-grid/ui-grid-grid-footer';
7864 * @propertyOf ui.grid.class:GridOptions
7865 * @description 'ui-grid/ui-grid-row' by default. When provided, this setting uses a
7866 * custom row template. Can be set to either the name of a template file:
7867 * <pre> $scope.gridOptions.rowTemplate = 'row_template.html';</pre>
7869 * <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>
7870 * or the id of a precompiled template (TBD how to use this) can be provided.
7871 * </br>Refer to the custom row template tutorial for more information.
7873 baseOptions.rowTemplate = baseOptions.rowTemplate || 'ui-grid/ui-grid-row';
7877 * @name appScopeProvider
7878 * @propertyOf ui.grid.class:GridOptions
7879 * @description by default, the parent scope of the ui-grid element will be assigned to grid.appScope
7880 * this property allows you to assign any reference you want to grid.appScope
7882 baseOptions.appScopeProvider = baseOptions.appScopeProvider || null;
7895 angular.module('ui.grid')
7899 * @name ui.grid.class:GridRenderContainer
7900 * @description The grid has render containers, allowing the ability to have pinned columns. If the grid
7901 * is right-to-left then there may be a right render container, if left-to-right then there may
7902 * be a left render container. There is always a body render container.
7903 * @param {string} name The name of the render container ('body', 'left', or 'right')
7904 * @param {Grid} grid the grid the render container is in
7905 * @param {object} options the render container options
7907 .factory('GridRenderContainer', ['gridUtil', 'uiGridConstants', function(gridUtil, uiGridConstants) {
7908 function GridRenderContainer(name, grid, options) {
7911 // if (gridUtil.type(grid) !== 'Grid') {
7912 // throw new Error('Grid argument is not a Grid object');
7919 // self.rowCache = [];
7920 // self.columnCache = [];
7922 self.visibleRowCache = [];
7923 self.visibleColumnCache = [];
7925 self.renderedRows = [];
7926 self.renderedColumns = [];
7928 self.prevScrollTop = 0;
7929 self.prevScrolltopPercentage = 0;
7930 self.prevRowScrollIndex = 0;
7932 self.prevScrollLeft = 0;
7933 self.prevScrollleftPercentage = 0;
7934 self.prevColumnScrollIndex = 0;
7936 self.columnStyles = "";
7938 self.viewportAdjusters = [];
7942 * @name hasHScrollbar
7943 * @propertyOf ui.grid.class:GridRenderContainer
7944 * @description flag to signal that container has a horizontal scrollbar
7946 self.hasHScrollbar = false;
7950 * @name hasVScrollbar
7951 * @propertyOf ui.grid.class:GridRenderContainer
7952 * @description flag to signal that container has a vertical scrollbar
7954 self.hasVScrollbar = false;
7958 * @name canvasHeightShouldUpdate
7959 * @propertyOf ui.grid.class:GridRenderContainer
7960 * @description flag to signal that container should recalculate the canvas size
7962 self.canvasHeightShouldUpdate = true;
7966 * @name canvasHeight
7967 * @propertyOf ui.grid.class:GridRenderContainer
7968 * @description last calculated canvas height value
7970 self.$$canvasHeight = 0;
7972 if (options && angular.isObject(options)) {
7973 angular.extend(self, options);
7976 grid.registerStyleComputation({
7979 self.updateColumnWidths();
7980 return self.columnStyles;
7986 GridRenderContainer.prototype.reset = function reset() {
7987 // this.rowCache.length = 0;
7988 // this.columnCache.length = 0;
7990 this.visibleColumnCache.length = 0;
7991 this.visibleRowCache.length = 0;
7993 this.renderedRows.length = 0;
7994 this.renderedColumns.length = 0;
7997 // TODO(c0bra): calculate size?? Should this be in a stackable directive?
8000 GridRenderContainer.prototype.containsColumn = function (col) {
8001 return this.visibleColumnCache.indexOf(col) !== -1;
8004 GridRenderContainer.prototype.minRowsToRender = function minRowsToRender() {
8007 var rowAddedHeight = 0;
8008 var viewPortHeight = self.getViewportHeight();
8009 for (var i = self.visibleRowCache.length - 1; rowAddedHeight < viewPortHeight && i >= 0; i--) {
8010 rowAddedHeight += self.visibleRowCache[i].height;
8016 GridRenderContainer.prototype.minColumnsToRender = function minColumnsToRender() {
8018 var viewportWidth = this.getViewportWidth();
8022 // self.columns.forEach(function(col, i) {
8023 for (var i = 0; i < self.visibleColumnCache.length; i++) {
8024 var col = self.visibleColumnCache[i];
8026 if (totalWidth < viewportWidth) {
8027 totalWidth += col.drawnWidth ? col.drawnWidth : 0;
8032 for (var j = i; j >= i - min; j--) {
8033 currWidth += self.visibleColumnCache[j].drawnWidth ? self.visibleColumnCache[j].drawnWidth : 0;
8035 if (currWidth < viewportWidth) {
8044 GridRenderContainer.prototype.getVisibleRowCount = function getVisibleRowCount() {
8045 return this.visibleRowCache.length;
8050 * @name registerViewportAdjuster
8051 * @methodOf ui.grid.class:GridRenderContainer
8052 * @description Registers an adjuster to the render container's available width or height. Adjusters are used
8053 * to tell the render container that there is something else consuming space, and to adjust it's size
8055 * @param {function} func the adjuster function we want to register
8058 GridRenderContainer.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
8059 this.viewportAdjusters.push(func);
8064 * @name removeViewportAdjuster
8065 * @methodOf ui.grid.class:GridRenderContainer
8066 * @description Removes an adjuster, should be used when your element is destroyed
8067 * @param {function} func the adjuster function we want to remove
8069 GridRenderContainer.prototype.removeViewportAdjuster = function removeViewportAdjuster(func) {
8070 var idx = this.viewportAdjusters.indexOf(func);
8073 this.viewportAdjusters.splice(idx, 1);
8079 * @name getViewportAdjustment
8080 * @methodOf ui.grid.class:GridRenderContainer
8081 * @description Gets the adjustment based on the viewportAdjusters.
8082 * @returns {object} a hash of { height: x, width: y }. Usually the values will be negative
8084 GridRenderContainer.prototype.getViewportAdjustment = function getViewportAdjustment() {
8087 var adjustment = { height: 0, width: 0 };
8089 self.viewportAdjusters.forEach(function (func) {
8090 adjustment = func.call(this, adjustment);
8096 GridRenderContainer.prototype.getMargin = function getMargin(side) {
8101 self.viewportAdjusters.forEach(function (func) {
8102 var adjustment = func.call(this, { height: 0, width: 0 });
8104 if (adjustment.side && adjustment.side === side) {
8105 amount += adjustment.width * -1;
8112 GridRenderContainer.prototype.getViewportHeight = function getViewportHeight() {
8115 var headerHeight = (self.headerHeight) ? self.headerHeight : self.grid.headerHeight;
8117 var viewPortHeight = self.grid.gridHeight - headerHeight - self.grid.footerHeight;
8120 var adjustment = self.getViewportAdjustment();
8122 viewPortHeight = viewPortHeight + adjustment.height;
8124 return viewPortHeight;
8127 GridRenderContainer.prototype.getViewportWidth = function getViewportWidth() {
8130 var viewportWidth = self.grid.gridWidth;
8132 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8133 // viewPortWidth = viewPortWidth - self.grid.verticalScrollbarWidth;
8136 // var viewportWidth = 0;\
8137 // self.visibleColumnCache.forEach(function (column) {
8138 // viewportWidth += column.drawnWidth;
8141 var adjustment = self.getViewportAdjustment();
8143 viewportWidth = viewportWidth + adjustment.width;
8145 return viewportWidth;
8148 GridRenderContainer.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
8151 var viewportWidth = this.getViewportWidth();
8153 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8154 // viewPortWidth = viewPortWidth + self.grid.verticalScrollbarWidth;
8157 // var adjustment = self.getViewportAdjustment();
8158 // viewPortWidth = viewPortWidth + adjustment.width;
8160 return viewportWidth;
8166 * @name getCanvasHeight
8167 * @methodOf ui.grid.class:GridRenderContainer
8168 * @description Returns the total canvas height. Only recalculates if canvasHeightShouldUpdate = false
8169 * @returns {number} total height of all the visible rows in the container
8171 GridRenderContainer.prototype.getCanvasHeight = function getCanvasHeight() {
8174 if (!self.canvasHeightShouldUpdate) {
8175 return self.$$canvasHeight;
8178 var oldCanvasHeight = self.$$canvasHeight;
8180 self.$$canvasHeight = 0;
8182 self.visibleRowCache.forEach(function(row){
8183 self.$$canvasHeight += row.height;
8187 self.canvasHeightShouldUpdate = false;
8189 self.grid.api.core.raise.canvasHeightChanged(oldCanvasHeight, self.$$canvasHeight);
8191 return self.$$canvasHeight;
8194 GridRenderContainer.prototype.getVerticalScrollLength = function getVerticalScrollLength() {
8195 return this.getCanvasHeight() - this.getViewportHeight() + this.grid.scrollbarHeight;
8198 GridRenderContainer.prototype.getCanvasWidth = function getCanvasWidth() {
8201 var ret = self.canvasWidth;
8206 GridRenderContainer.prototype.setRenderedRows = function setRenderedRows(newRows) {
8207 this.renderedRows.length = newRows.length;
8208 for (var i = 0; i < newRows.length; i++) {
8209 this.renderedRows[i] = newRows[i];
8213 GridRenderContainer.prototype.setRenderedColumns = function setRenderedColumns(newColumns) {
8217 this.renderedColumns.length = newColumns.length;
8218 for (var i = 0; i < newColumns.length; i++) {
8219 this.renderedColumns[i] = newColumns[i];
8222 this.updateColumnOffset();
8225 GridRenderContainer.prototype.updateColumnOffset = function updateColumnOffset() {
8226 // Calculate the width of the columns on the left side that are no longer rendered.
8227 // That will be the offset for the columns as we scroll horizontally.
8228 var hiddenColumnsWidth = 0;
8229 for (var i = 0; i < this.currentFirstColumn; i++) {
8230 hiddenColumnsWidth += this.visibleColumnCache[i].drawnWidth;
8233 this.columnOffset = hiddenColumnsWidth;
8236 GridRenderContainer.prototype.scrollVertical = function (newScrollTop) {
8237 var vertScrollPercentage = -1;
8239 if (newScrollTop !== this.prevScrollTop) {
8240 var yDiff = newScrollTop - this.prevScrollTop;
8242 if (yDiff > 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; }
8243 if (yDiff < 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.UP; }
8245 var vertScrollLength = this.getVerticalScrollLength();
8247 vertScrollPercentage = newScrollTop / vertScrollLength;
8249 // console.log('vert', vertScrollPercentage, newScrollTop, vertScrollLength);
8251 if (vertScrollPercentage > 1) { vertScrollPercentage = 1; }
8252 if (vertScrollPercentage < 0) { vertScrollPercentage = 0; }
8254 this.adjustScrollVertical(newScrollTop, vertScrollPercentage);
8255 return vertScrollPercentage;
8259 GridRenderContainer.prototype.scrollHorizontal = function(newScrollLeft){
8260 var horizScrollPercentage = -1;
8264 if (newScrollLeft !== this.prevScrollLeft) {
8265 var xDiff = newScrollLeft - this.prevScrollLeft;
8267 if (xDiff > 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; }
8268 if (xDiff < 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; }
8270 var horizScrollLength = (this.canvasWidth - this.getViewportWidth());
8271 if (horizScrollLength !== 0) {
8272 horizScrollPercentage = newScrollLeft / horizScrollLength;
8275 horizScrollPercentage = 0;
8278 this.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage);
8279 return horizScrollPercentage;
8283 GridRenderContainer.prototype.adjustScrollVertical = function adjustScrollVertical(scrollTop, scrollPercentage, force) {
8284 if (this.prevScrollTop === scrollTop && !force) {
8288 if (typeof(scrollTop) === 'undefined' || scrollTop === undefined || scrollTop === null) {
8289 scrollTop = (this.getCanvasHeight() - this.getViewportHeight()) * scrollPercentage;
8292 this.adjustRows(scrollTop, scrollPercentage, false);
8294 this.prevScrollTop = scrollTop;
8295 this.prevScrolltopPercentage = scrollPercentage;
8297 this.grid.queueRefresh();
8300 GridRenderContainer.prototype.adjustScrollHorizontal = function adjustScrollHorizontal(scrollLeft, scrollPercentage, force) {
8301 if (this.prevScrollLeft === scrollLeft && !force) {
8305 if (typeof(scrollLeft) === 'undefined' || scrollLeft === undefined || scrollLeft === null) {
8306 scrollLeft = (this.getCanvasWidth() - this.getViewportWidth()) * scrollPercentage;
8309 this.adjustColumns(scrollLeft, scrollPercentage);
8311 this.prevScrollLeft = scrollLeft;
8312 this.prevScrollleftPercentage = scrollPercentage;
8314 this.grid.queueRefresh();
8317 GridRenderContainer.prototype.adjustRows = function adjustRows(scrollTop, scrollPercentage, postDataLoaded) {
8320 var minRows = self.minRowsToRender();
8322 var rowCache = self.visibleRowCache;
8324 var maxRowIndex = rowCache.length - minRows;
8326 // console.log('scroll%1', scrollPercentage);
8328 // Calculate the scroll percentage according to the scrollTop location, if no percentage was provided
8329 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollTop) {
8330 scrollPercentage = scrollTop / self.getVerticalScrollLength();
8333 var rowIndex = Math.ceil(Math.min(maxRowIndex, maxRowIndex * scrollPercentage));
8335 // console.log('maxRowIndex / scroll%', maxRowIndex, scrollPercentage, rowIndex);
8337 // Define a max row index that we can't scroll past
8338 if (rowIndex > maxRowIndex) {
8339 rowIndex = maxRowIndex;
8343 if (rowCache.length > self.grid.options.virtualizationThreshold) {
8344 if (!(typeof(scrollTop) === 'undefined' || scrollTop === null)) {
8345 // Have we hit the threshold going down?
8346 if ( !self.grid.suppressParentScrollDown && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8349 //Have we hit the threshold going up?
8350 if ( !self.grid.suppressParentScrollUp && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8354 var rangeStart = {};
8357 rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows);
8358 rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows);
8360 newRange = [rangeStart, rangeEnd];
8363 var maxLen = self.visibleRowCache.length;
8364 newRange = [0, Math.max(maxLen, minRows + self.grid.options.excessRows)];
8367 self.updateViewableRowRange(newRange);
8369 self.prevRowScrollIndex = rowIndex;
8372 GridRenderContainer.prototype.adjustColumns = function adjustColumns(scrollLeft, scrollPercentage) {
8375 var minCols = self.minColumnsToRender();
8377 var columnCache = self.visibleColumnCache;
8378 var maxColumnIndex = columnCache.length - minCols;
8380 // Calculate the scroll percentage according to the scrollLeft location, if no percentage was provided
8381 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollLeft) {
8382 var horizScrollLength = (self.getCanvasWidth() - self.getViewportWidth());
8383 scrollPercentage = scrollLeft / horizScrollLength;
8386 var colIndex = Math.ceil(Math.min(maxColumnIndex, maxColumnIndex * scrollPercentage));
8388 // Define a max row index that we can't scroll past
8389 if (colIndex > maxColumnIndex) {
8390 colIndex = maxColumnIndex;
8394 if (columnCache.length > self.grid.options.columnVirtualizationThreshold && self.getCanvasWidth() > self.getViewportWidth()) {
8395 /* Commented the following lines because otherwise the moved column wasn't visible immediately on the new position
8396 * in the case of many columns with horizontal scroll, one had to scroll left or right and then return in order to see it
8397 // Have we hit the threshold going down?
8398 if (self.prevScrollLeft < scrollLeft && colIndex < self.prevColumnScrollIndex + self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8401 //Have we hit the threshold going up?
8402 if (self.prevScrollLeft > scrollLeft && colIndex > self.prevColumnScrollIndex - self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8406 var rangeStart = Math.max(0, colIndex - self.grid.options.excessColumns);
8407 var rangeEnd = Math.min(columnCache.length, colIndex + minCols + self.grid.options.excessColumns);
8409 newRange = [rangeStart, rangeEnd];
8412 var maxLen = self.visibleColumnCache.length;
8414 newRange = [0, Math.max(maxLen, minCols + self.grid.options.excessColumns)];
8417 self.updateViewableColumnRange(newRange);
8419 self.prevColumnScrollIndex = colIndex;
8422 // Method for updating the visible rows
8423 GridRenderContainer.prototype.updateViewableRowRange = function updateViewableRowRange(renderedRange) {
8424 // Slice out the range of rows from the data
8425 // var rowArr = uiGridCtrl.grid.rows.slice(renderedRange[0], renderedRange[1]);
8426 var rowArr = this.visibleRowCache.slice(renderedRange[0], renderedRange[1]);
8428 // Define the top-most rendered row
8429 this.currentTopRow = renderedRange[0];
8431 this.setRenderedRows(rowArr);
8434 // Method for updating the visible columns
8435 GridRenderContainer.prototype.updateViewableColumnRange = function updateViewableColumnRange(renderedRange) {
8436 // Slice out the range of rows from the data
8437 // var columnArr = uiGridCtrl.grid.columns.slice(renderedRange[0], renderedRange[1]);
8438 var columnArr = this.visibleColumnCache.slice(renderedRange[0], renderedRange[1]);
8440 // Define the left-most rendered columns
8441 this.currentFirstColumn = renderedRange[0];
8443 this.setRenderedColumns(columnArr);
8446 GridRenderContainer.prototype.headerCellWrapperStyle = function () {
8449 if (self.currentFirstColumn !== 0) {
8450 var offset = self.columnOffset;
8452 if (self.grid.isRTL()) {
8453 return { 'margin-right': offset + 'px' };
8456 return { 'margin-left': offset + 'px' };
8465 * @name updateColumnWidths
8466 * @propertyOf ui.grid.class:GridRenderContainer
8467 * @description Determine the appropriate column width of each column across all render containers.
8469 * Column width is easy when each column has a specified width. When columns are variable width (i.e.
8470 * have an * or % of the viewport) then we try to calculate so that things fit in. The problem is that
8471 * we have multiple render containers, and we don't want one render container to just take the whole viewport
8472 * when it doesn't need to - we want things to balance out across the render containers.
8474 * To do this, we use this method to calculate all the renderContainers, recognising that in a given render
8475 * cycle it'll get called once per render container, so it needs to return the same values each time.
8477 * The constraints on this method are therefore:
8478 * - must return the same value when called multiple times, to do this it needs to rely on properties of the
8479 * columns, but not properties that change when this is called (so it shouldn't rely on drawnWidth)
8481 * The general logic of this method is:
8482 * - calculate our total available width
8483 * - look at all the columns across all render containers, and work out which have widths and which have
8484 * constraints such as % or * or something else
8485 * - for those with *, count the total number of * we see and add it onto a running total, add this column to an * array
8486 * - for those with a %, allocate the % as a percentage of the viewport, having consideration of min and max
8487 * - for those with manual width (in pixels) we set the drawnWidth to the specified width
8488 * - we end up with an asterisks array still to process
8489 * - we look at our remaining width. If it's greater than zero, we divide it up among the asterisk columns, then process
8490 * them for min and max width constraints
8491 * - if it's zero or less, we set the asterisk columns to their minimum widths
8492 * - we use parseInt quite a bit, as we try to make all our column widths integers
8494 GridRenderContainer.prototype.updateColumnWidths = function () {
8497 var asterisksArray = [],
8502 // Get the width of the viewport
8503 var availableWidth = self.grid.getViewportWidth() - self.grid.scrollbarWidth;
8505 // get all the columns across all render containers, we have to calculate them all or one render container
8506 // could consume the whole viewport
8507 var columnCache = [];
8508 angular.forEach(self.grid.renderContainers, function( container, name){
8509 columnCache = columnCache.concat(container.visibleColumnCache);
8512 // look at each column, process any manual values or %, put the * into an array to look at later
8513 columnCache.forEach(function(column, i) {
8515 // Skip hidden columns
8516 if (!column.visible) { return; }
8518 if (angular.isNumber(column.width)) {
8519 // pixel width, set to this value
8520 width = parseInt(column.width, 10);
8521 usedWidthSum = usedWidthSum + width;
8522 column.drawnWidth = width;
8524 } else if (gridUtil.endsWith(column.width, "%")) {
8525 // percentage width, set to percentage of the viewport
8526 width = parseInt(parseInt(column.width.replace(/%/g, ''), 10) / 100 * availableWidth);
8528 if ( width > column.maxWidth ){
8529 width = column.maxWidth;
8532 if ( width < column.minWidth ){
8533 width = column.minWidth;
8536 usedWidthSum = usedWidthSum + width;
8537 column.drawnWidth = width;
8538 } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) {
8539 // is an asterisk column, the gridColumn already checked the string consists only of '****'
8540 asteriskNum = asteriskNum + column.width.length;
8541 asterisksArray.push(column);
8545 // Get the remaining width (available width subtracted by the used widths sum)
8546 var remainingWidth = availableWidth - usedWidthSum;
8548 var i, column, colWidth;
8550 if (asterisksArray.length > 0) {
8551 // the width that each asterisk value would be assigned (this can be negative)
8552 var asteriskVal = remainingWidth / asteriskNum;
8554 asterisksArray.forEach(function( column ){
8555 var width = parseInt(column.width.length * asteriskVal, 10);
8557 if ( width > column.maxWidth ){
8558 width = column.maxWidth;
8561 if ( width < column.minWidth ){
8562 width = column.minWidth;
8565 usedWidthSum = usedWidthSum + width;
8566 column.drawnWidth = width;
8570 // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our
8571 // calculated widths would have the grid narrower than the available space,
8572 // dole the remainder out one by one to make everything fit
8573 var processColumnUpwards = function(column){
8574 if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) {
8575 column.drawnWidth++;
8578 columnsToChange = true;
8582 var leftoverWidth = availableWidth - usedWidthSum;
8583 var columnsToChange = true;
8585 while (leftoverWidth > 0 && columnsToChange) {
8586 columnsToChange = false;
8587 asterisksArray.forEach(processColumnUpwards);
8590 // We can end up with too much width even though some columns aren't at their max width, in this situation
8591 // we can trim the columns a little
8592 var processColumnDownwards = function(column){
8593 if ( column.drawnWidth > column.minWidth && excessWidth > 0) {
8594 column.drawnWidth--;
8597 columnsToChange = true;
8601 var excessWidth = usedWidthSum - availableWidth;
8602 columnsToChange = true;
8604 while (excessWidth > 0 && columnsToChange) {
8605 columnsToChange = false;
8606 asterisksArray.forEach(processColumnDownwards);
8610 // all that was across all the renderContainers, now we need to work out what that calculation decided for
8611 // our renderContainer
8612 var canvasWidth = 0;
8613 self.visibleColumnCache.forEach(function(column){
8614 if ( column.visible ){
8615 canvasWidth = canvasWidth + column.drawnWidth;
8620 columnCache.forEach(function (column) {
8621 ret = ret + column.getColClassDefinition();
8624 self.canvasWidth = canvasWidth;
8626 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
8629 // Set this render container's column styles so they can be used in style computation
8630 this.columnStyles = ret;
8633 GridRenderContainer.prototype.needsHScrollbarPlaceholder = function () {
8634 return this.grid.options.enableHorizontalScrollbar && !this.hasHScrollbar && !this.grid.disableScrolling;
8637 GridRenderContainer.prototype.getViewportStyle = function () {
8641 self.hasHScrollbar = false;
8642 self.hasVScrollbar = false;
8644 if (self.grid.disableScrolling) {
8645 styles['overflow-x'] = 'hidden';
8646 styles['overflow-y'] = 'hidden';
8650 if (self.name === 'body') {
8651 self.hasHScrollbar = self.grid.options.enableHorizontalScrollbar !== uiGridConstants.scrollbars.NEVER;
8652 if (!self.grid.isRTL()) {
8653 if (!self.grid.hasRightContainerColumns()) {
8654 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8658 if (!self.grid.hasLeftContainerColumns()) {
8659 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8663 else if (self.name === 'left') {
8664 self.hasVScrollbar = self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8667 self.hasVScrollbar = !self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8670 styles['overflow-x'] = self.hasHScrollbar ? 'scroll' : 'hidden';
8671 styles['overflow-y'] = self.hasVScrollbar ? 'scroll' : 'hidden';
8679 return GridRenderContainer;
8686 angular.module('ui.grid')
8687 .factory('GridRow', ['gridUtil', function(gridUtil) {
8691 * @name ui.grid.class:GridRow
8692 * @description GridRow is the viewModel for one logical row on the grid. A grid Row is not necessarily a one-to-one
8693 * relation to gridOptions.data.
8694 * @param {object} entity the array item from GridOptions.data
8695 * @param {number} index the current position of the row in the array
8696 * @param {Grid} reference to the parent grid
8698 function GridRow(entity, index, grid) {
8703 * @propertyOf ui.grid.class:GridRow
8704 * @description A reference back to the grid
8711 * @propertyOf ui.grid.class:GridRow
8712 * @description A reference to an item in gridOptions.data[]
8714 this.entity = entity;
8719 * @propertyOf ui.grid.class:GridRow
8720 * @description UniqueId of row
8722 this.uid = gridUtil.nextUid();
8727 * @propertyOf ui.grid.class:GridRow
8728 * @description If true, the row will be rendered
8731 this.visible = true;
8734 this.$$height = grid.options.rowHeight;
8741 * @propertyOf ui.grid.class:GridRow
8742 * @description height of each individual row. changing the height will flag all
8743 * row renderContainers to recalculate their canvas height
8745 Object.defineProperty(GridRow.prototype, 'height', {
8747 return this.$$height;
8749 set: function(height) {
8750 if (height !== this.$$height) {
8751 this.grid.updateCanvasHeight();
8752 this.$$height = height;
8759 * @name getQualifiedColField
8760 * @methodOf ui.grid.class:GridRow
8761 * @description returns the qualified field name as it exists on scope
8762 * ie: row.entity.fieldA
8763 * @param {GridCol} col column instance
8764 * @returns {string} resulting name that can be evaluated on scope
8766 GridRow.prototype.getQualifiedColField = function(col) {
8767 return 'row.' + this.getEntityQualifiedColField(col);
8772 * @name getEntityQualifiedColField
8773 * @methodOf ui.grid.class:GridRow
8774 * @description returns the qualified field name minus the row path
8776 * @param {GridCol} col column instance
8777 * @returns {string} resulting name that can be evaluated against a row
8779 GridRow.prototype.getEntityQualifiedColField = function(col) {
8780 return gridUtil.preEval('entity.' + col.field);
8786 * @name setRowInvisible
8787 * @methodOf ui.grid.class:GridRow
8788 * @description Sets an override on the row that forces it to always
8789 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8791 * This method can be called from the api, passing in the gridRow we want
8792 * altered. It should really work by calling gridRow.setRowInvisible, but that's
8793 * not the way I coded it, and too late to change now. Changed to just call
8794 * the internal function row.setThisRowInvisible().
8796 * @param {GridRow} row the row we want to set to invisible
8799 GridRow.prototype.setRowInvisible = function ( row ) {
8800 if (row && row.setThisRowInvisible){
8801 row.setThisRowInvisible( 'user' );
8808 * @name clearRowInvisible
8809 * @methodOf ui.grid.class:GridRow
8810 * @description Clears an override on the row that forces it to always
8811 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8813 * This method can be called from the api, passing in the gridRow we want
8814 * altered. It should really work by calling gridRow.clearRowInvisible, but that's
8815 * not the way I coded it, and too late to change now. Changed to just call
8816 * the internal function row.clearThisRowInvisible().
8818 * @param {GridRow} row the row we want to clear the invisible flag
8821 GridRow.prototype.clearRowInvisible = function ( row ) {
8822 if (row && row.clearThisRowInvisible){
8823 row.clearThisRowInvisible( 'user' );
8830 * @name setThisRowInvisible
8831 * @methodOf ui.grid.class:GridRow
8832 * @description Sets an override on the row that forces it to always
8833 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility
8835 * @param {string} reason the reason (usually the module) for the row to be invisible.
8836 * E.g. grouping, user, filter
8837 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8839 GridRow.prototype.setThisRowInvisible = function ( reason, fromRowsProcessor ) {
8840 if ( !this.invisibleReason ){
8841 this.invisibleReason = {};
8843 this.invisibleReason[reason] = true;
8844 this.evaluateRowVisibility( fromRowsProcessor);
8850 * @name clearRowInvisible
8851 * @methodOf ui.grid.class:GridRow
8852 * @description Clears any override on the row visibility, returning it
8853 * to normal visibility calculations. Emits the rowsVisibleChanged
8856 * @param {string} reason the reason (usually the module) for the row to be invisible.
8857 * E.g. grouping, user, filter
8858 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8860 GridRow.prototype.clearThisRowInvisible = function ( reason, fromRowsProcessor ) {
8861 if (typeof(this.invisibleReason) !== 'undefined' ) {
8862 delete this.invisibleReason[reason];
8864 this.evaluateRowVisibility( fromRowsProcessor );
8870 * @name evaluateRowVisibility
8871 * @methodOf ui.grid.class:GridRow
8872 * @description Determines whether the row should be visible based on invisibleReason,
8873 * and if it changes the row visibility, then emits the rowsVisibleChanged event.
8875 * Queues a grid refresh, but doesn't call it directly to avoid hitting lots of grid refreshes.
8876 * @param {boolean} fromRowProcessor if true, then it won't raise events or queue the refresh, the
8877 * row processor does that already
8879 GridRow.prototype.evaluateRowVisibility = function ( fromRowProcessor ) {
8880 var newVisibility = true;
8881 if ( typeof(this.invisibleReason) !== 'undefined' ){
8882 angular.forEach(this.invisibleReason, function( value, key ){
8884 newVisibility = false;
8889 if ( typeof(this.visible) === 'undefined' || this.visible !== newVisibility ){
8890 this.visible = newVisibility;
8891 if ( !fromRowProcessor ){
8892 this.grid.queueGridRefresh();
8893 this.grid.api.core.raise.rowsVisibleChanged(this);
8908 * @name ui.grid.class:GridRowColumn
8909 * @param {GridRow} row The row for this pair
8910 * @param {GridColumn} column The column for this pair
8911 * @description A row and column pair that represents the intersection of these two entities.
8912 * Must be instantiated as a constructor using the `new` keyword.
8914 angular.module('ui.grid')
8915 .factory('GridRowColumn', ['$parse', '$filter',
8916 function GridRowColumnFactory($parse, $filter){
8917 var GridRowColumn = function GridRowColumn(row, col) {
8918 if ( !(this instanceof GridRowColumn)){
8919 throw "Using GridRowColumn as a function insead of as a constructor. Must be called with `new` keyword";
8925 * @propertyOf ui.grid.class:GridRowColumn
8926 * @description {@link ui.grid.class:GridRow }
8932 * @propertyOf ui.grid.class:GridRowColumn
8933 * @description {@link ui.grid.class:GridColumn }
8940 * @name getIntersectionValueRaw
8941 * @methodOf ui.grid.class:GridRowColumn
8942 * @description Gets the intersection of where the row and column meet.
8943 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8944 * If the column has a cellFilter this will NOT return the filtered value.
8946 GridRowColumn.prototype.getIntersectionValueRaw = function(){
8947 var getter = $parse(this.row.getEntityQualifiedColField(this.col));
8948 var context = this.row;
8949 return getter(context);
8953 * @name getIntersectionValueFiltered
8954 * @methodOf ui.grid.class:GridRowColumn
8955 * @description Gets the intersection of where the row and column meet.
8956 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8957 * If the column has a cellFilter this will also apply the filter to it and return the value that the filter displays.
8959 GridRowColumn.prototype.getIntersectionValueFiltered = function(){
8960 var value = this.getIntersectionValueRaw();
8961 if (this.col.cellFilter && this.col.cellFilter !== ''){
8962 var getFilterIfExists = function(filterName){
8964 return $filter(filterName);
8969 var filter = getFilterIfExists(this.col.cellFilter);
8970 if (filter) { // Check if this is filter name or a filter string
8971 value = filter(value);
8972 } else { // We have the template version of a filter so we need to parse it apart
8973 // Get the filter params out using a regex
8974 // Test out this regex here https://regex101.com/r/rC5eR5/2
8975 var re = /([^:]*):([^:]*):?([\s\S]+)?/;
8977 if ((matches = re.exec(this.col.cellFilter)) !== null) {
8978 // View your result using the matches-variable.
8979 // eg matches[0] etc.
8980 value = $filter(matches[1])(value, matches[2], matches[3]);
8986 return GridRowColumn;
8992 angular.module('ui.grid')
8993 .factory('ScrollEvent', ['gridUtil', function (gridUtil) {
8997 * @name ui.grid.class:ScrollEvent
8998 * @description Model for all scrollEvents
8999 * @param {Grid} grid that owns the scroll event
9000 * @param {GridRenderContainer} sourceRowContainer that owns the scroll event. Can be null
9001 * @param {GridRenderContainer} sourceColContainer that owns the scroll event. Can be null
9002 * @param {string} source the source of the event - from uiGridConstants.scrollEventSources or a string value of directive/service/factory.functionName
9004 function ScrollEvent(grid, sourceRowContainer, sourceColContainer, source) {
9007 throw new Error("grid argument is required");
9013 * @propertyOf ui.grid.class:ScrollEvent
9014 * @description A reference back to the grid
9023 * @propertyOf ui.grid.class:ScrollEvent
9024 * @description the source of the scroll event. limited to values from uiGridConstants.scrollEventSources
9026 self.source = source;
9032 * @propertyOf ui.grid.class:ScrollEvent
9033 * @description most scroll events from the mouse or trackpad require delay to operate properly
9034 * set to false to eliminate delay. Useful for scroll events that the grid causes, such as scrolling to make a row visible.
9036 self.withDelay = true;
9038 self.sourceRowContainer = sourceRowContainer;
9039 self.sourceColContainer = sourceColContainer;
9041 self.newScrollLeft = null;
9042 self.newScrollTop = null;
9046 self.verticalScrollLength = -9999999;
9047 self.horizontalScrollLength = -999999;
9052 * @name fireThrottledScrollingEvent
9053 * @methodOf ui.grid.class:ScrollEvent
9054 * @description fires a throttled event using grid.api.core.raise.scrollEvent
9056 self.fireThrottledScrollingEvent = gridUtil.throttle(function(sourceContainerId) {
9057 self.grid.scrollContainers(sourceContainerId, self);
9058 }, self.grid.options.wheelScrollThrottle, {trailing: true});
9065 * @name getNewScrollLeft
9066 * @methodOf ui.grid.class:ScrollEvent
9067 * @description returns newScrollLeft property if available; calculates a new value if it isn't
9069 ScrollEvent.prototype.getNewScrollLeft = function(colContainer, viewport){
9072 if (!self.newScrollLeft){
9073 var scrollWidth = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
9075 var oldScrollLeft = gridUtil.normalizeScrollLeft(viewport, self.grid);
9077 var scrollXPercentage;
9078 if (typeof(self.x.percentage) !== 'undefined' && self.x.percentage !== undefined) {
9079 scrollXPercentage = self.x.percentage;
9081 else if (typeof(self.x.pixels) !== 'undefined' && self.x.pixels !== undefined) {
9082 scrollXPercentage = self.x.percentage = (oldScrollLeft + self.x.pixels) / scrollWidth;
9085 throw new Error("No percentage or pixel value provided for scroll event X axis");
9088 return Math.max(0, scrollXPercentage * scrollWidth);
9091 return self.newScrollLeft;
9097 * @name getNewScrollTop
9098 * @methodOf ui.grid.class:ScrollEvent
9099 * @description returns newScrollTop property if available; calculates a new value if it isn't
9101 ScrollEvent.prototype.getNewScrollTop = function(rowContainer, viewport){
9105 if (!self.newScrollTop){
9106 var scrollLength = rowContainer.getVerticalScrollLength();
9108 var oldScrollTop = viewport[0].scrollTop;
9110 var scrollYPercentage;
9111 if (typeof(self.y.percentage) !== 'undefined' && self.y.percentage !== undefined) {
9112 scrollYPercentage = self.y.percentage;
9114 else if (typeof(self.y.pixels) !== 'undefined' && self.y.pixels !== undefined) {
9115 scrollYPercentage = self.y.percentage = (oldScrollTop + self.y.pixels) / scrollLength;
9118 throw new Error("No percentage or pixel value provided for scroll event Y axis");
9121 return Math.max(0, scrollYPercentage * scrollLength);
9124 return self.newScrollTop;
9127 ScrollEvent.prototype.atTop = function(scrollTop) {
9128 return (this.y && (this.y.percentage === 0 || this.verticalScrollLength < 0) && scrollTop === 0);
9131 ScrollEvent.prototype.atBottom = function(scrollTop) {
9132 return (this.y && (this.y.percentage === 1 || this.verticalScrollLength === 0) && scrollTop > 0);
9135 ScrollEvent.prototype.atLeft = function(scrollLeft) {
9136 return (this.x && (this.x.percentage === 0 || this.horizontalScrollLength < 0) && scrollLeft === 0);
9139 ScrollEvent.prototype.atRight = function(scrollLeft) {
9140 return (this.x && (this.x.percentage === 1 || this.horizontalScrollLength ===0) && scrollLeft > 0);
9144 ScrollEvent.Sources = {
9145 ViewPortScroll: 'ViewPortScroll',
9146 RenderContainerMouseWheel: 'RenderContainerMouseWheel',
9147 RenderContainerTouchMove: 'RenderContainerTouchMove',
9162 * @name ui.grid.service:gridClassFactory
9164 * @description factory to return dom specific instances of a grid
9167 angular.module('ui.grid').service('gridClassFactory', ['gridUtil', '$q', '$compile', '$templateCache', 'uiGridConstants', 'Grid', 'GridColumn', 'GridRow',
9168 function (gridUtil, $q, $compile, $templateCache, uiGridConstants, Grid, GridColumn, GridRow) {
9174 * @methodOf ui.grid.service:gridClassFactory
9175 * @description Creates a new grid instance. Each instance will have a unique id
9176 * @param {object} options An object map of options to pass into the created grid instance.
9177 * @returns {Grid} grid
9179 createGrid : function(options) {
9180 options = (typeof(options) !== 'undefined') ? options : {};
9181 options.id = gridUtil.newId();
9182 var grid = new Grid(options);
9184 // NOTE/TODO: rowTemplate should always be defined...
9185 if (grid.options.rowTemplate) {
9186 var rowTemplateFnPromise = $q.defer();
9187 grid.getRowTemplateFn = rowTemplateFnPromise.promise;
9189 gridUtil.getTemplate(grid.options.rowTemplate)
9191 function (template) {
9192 var rowTemplateFn = $compile(template);
9193 rowTemplateFnPromise.resolve(rowTemplateFn);
9196 // Todo handle response error here?
9197 throw new Error("Couldn't fetch/use row template '" + grid.options.rowTemplate + "'");
9201 grid.registerColumnBuilder(service.defaultColumnBuilder);
9203 // Row builder for custom row templates
9204 grid.registerRowBuilder(service.rowTemplateAssigner);
9206 // Reset all rows to visible initially
9207 grid.registerRowsProcessor(function allRowsVisible(rows) {
9208 rows.forEach(function (row) {
9209 row.evaluateRowVisibility( true );
9215 grid.registerColumnsProcessor(function allColumnsVisible(columns) {
9216 columns.forEach(function (column) {
9217 column.visible = true;
9223 grid.registerColumnsProcessor(function(renderableColumns) {
9224 renderableColumns.forEach(function (column) {
9225 if (column.colDef.visible === false) {
9226 column.visible = false;
9230 return renderableColumns;
9234 grid.registerRowsProcessor(grid.searchRows, 100);
9236 // Register the default row processor, it sorts rows by selected columns
9237 if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) {
9238 grid.registerRowsProcessor(grid.options.externalSort, 200);
9241 grid.registerRowsProcessor(grid.sortByColumn, 200);
9249 * @name defaultColumnBuilder
9250 * @methodOf ui.grid.service:gridClassFactory
9251 * @description Processes designTime column definitions and applies them to col for the
9252 * core grid features
9253 * @param {object} colDef reference to column definition
9254 * @param {GridColumn} col reference to gridCol
9255 * @param {object} gridOptions reference to grid options
9257 defaultColumnBuilder: function (colDef, col, gridOptions) {
9259 var templateGetPromises = [];
9261 // Abstracts the standard template processing we do for every template type.
9262 var processTemplate = function( templateType, providedType, defaultTemplate, filterType, tooltipType ) {
9263 if ( !colDef[templateType] ){
9264 col[providedType] = defaultTemplate;
9266 col[providedType] = colDef[templateType];
9269 templateGetPromises.push(gridUtil.getTemplate(col[providedType])
9271 function (template) {
9272 if ( angular.isFunction(template) ) { template = template(); }
9273 var tooltipCall = ( tooltipType === 'cellTooltip' ) ? 'col.cellTooltip(row,col)' : 'col.headerTooltip(col)';
9274 if ( tooltipType && col[tooltipType] === false ){
9275 template = template.replace(uiGridConstants.TOOLTIP, '');
9276 } else if ( tooltipType && col[tooltipType] ){
9277 template = template.replace(uiGridConstants.TOOLTIP, 'title="{{' + tooltipCall + ' CUSTOM_FILTERS }}"');
9281 col[templateType] = template.replace(uiGridConstants.CUSTOM_FILTERS, function() {
9282 return col[filterType] ? "|" + col[filterType] : "";
9285 col[templateType] = template;
9289 throw new Error("Couldn't fetch/use colDef." + templateType + " '" + colDef[templateType] + "'");
9298 * @name cellTemplate
9299 * @propertyOf ui.grid.class:GridOptions.columnDef
9300 * @description a custom template for each cell in this column. The default
9301 * is ui-grid/uiGridCell. If you are using the cellNav feature, this template
9302 * must contain a div that can receive focus.
9305 processTemplate( 'cellTemplate', 'providedCellTemplate', 'ui-grid/uiGridCell', 'cellFilter', 'cellTooltip' );
9306 col.cellTemplatePromise = templateGetPromises[0];
9310 * @name headerCellTemplate
9311 * @propertyOf ui.grid.class:GridOptions.columnDef
9312 * @description a custom template for the header for this column. The default
9313 * is ui-grid/uiGridHeaderCell
9316 processTemplate( 'headerCellTemplate', 'providedHeaderCellTemplate', 'ui-grid/uiGridHeaderCell', 'headerCellFilter', 'headerTooltip' );
9320 * @name footerCellTemplate
9321 * @propertyOf ui.grid.class:GridOptions.columnDef
9322 * @description a custom template for the footer for this column. The default
9323 * is ui-grid/uiGridFooterCell
9326 processTemplate( 'footerCellTemplate', 'providedFooterCellTemplate', 'ui-grid/uiGridFooterCell', 'footerCellFilter' );
9330 * @name filterHeaderTemplate
9331 * @propertyOf ui.grid.class:GridOptions.columnDef
9332 * @description a custom template for the filter input. The default is ui-grid/ui-grid-filter
9335 processTemplate( 'filterHeaderTemplate', 'providedFilterHeaderTemplate', 'ui-grid/ui-grid-filter' );
9337 // Create a promise for the compiled element function
9338 col.compiledElementFnDefer = $q.defer();
9340 return $q.all(templateGetPromises);
9344 rowTemplateAssigner: function rowTemplateAssigner(row) {
9347 // Row has no template assigned to it
9348 if (!row.rowTemplate) {
9349 // Use the default row template from the grid
9350 row.rowTemplate = grid.options.rowTemplate;
9352 // Use the grid's function for fetching the compiled row template function
9353 row.getRowTemplateFn = grid.getRowTemplateFn;
9355 // Row has its own template assigned
9357 // Create a promise for the compiled row template function
9358 var perRowTemplateFnPromise = $q.defer();
9359 row.getRowTemplateFn = perRowTemplateFnPromise.promise;
9361 // Get the row template
9362 gridUtil.getTemplate(row.rowTemplate)
9363 .then(function (template) {
9364 // Compile the template
9365 var rowTemplateFn = $compile(template);
9367 // Resolve the compiled template function promise
9368 perRowTemplateFnPromise.resolve(rowTemplateFn);
9371 // Todo handle response error here?
9372 throw new Error("Couldn't fetch/use row template '" + row.rowTemplate + "'");
9376 return row.getRowTemplateFn;
9380 //class definitions (moved to separate factories)
9389 var module = angular.module('ui.grid');
9391 function escapeRegExp(str) {
9392 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
9398 * @name ui.grid.service:rowSearcher
9400 * @description Service for searching/filtering rows based on column value conditions.
9402 module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil, uiGridConstants) {
9403 var defaultCondition = uiGridConstants.filter.CONTAINS;
9405 var rowSearcher = {};
9410 * @methodOf ui.grid.service:rowSearcher
9411 * @description Get the term from a filter
9412 * Trims leading and trailing whitespace
9413 * @param {object} filter object to use
9414 * @returns {object} Parsed term
9416 rowSearcher.getTerm = function getTerm(filter) {
9417 if (typeof(filter.term) === 'undefined') { return filter.term; }
9419 var term = filter.term;
9421 // Strip leading and trailing whitespace if the term is a string
9422 if (typeof(term) === 'string') {
9432 * @methodOf ui.grid.service:rowSearcher
9433 * @description Remove leading and trailing asterisk (*) from the filter's term
9434 * @param {object} filter object to use
9435 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9437 rowSearcher.stripTerm = function stripTerm(filter) {
9438 var term = rowSearcher.getTerm(filter);
9440 if (typeof(term) === 'string') {
9441 return escapeRegExp(term.replace(/(^\*|\*$)/g, ''));
9451 * @name guessCondition
9452 * @methodOf ui.grid.service:rowSearcher
9453 * @description Guess the condition for a filter based on its term
9455 * Defaults to STARTS_WITH. Uses CONTAINS for strings beginning and ending with *s (*bob*).
9456 * Uses STARTS_WITH for strings ending with * (bo*). Uses ENDS_WITH for strings starting with * (*ob).
9457 * @param {object} filter object to use
9458 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9460 rowSearcher.guessCondition = function guessCondition(filter) {
9461 if (typeof(filter.term) === 'undefined' || !filter.term) {
9462 return defaultCondition;
9465 var term = rowSearcher.getTerm(filter);
9467 if (/\*/.test(term)) {
9468 var regexpFlags = '';
9469 if (!filter.flags || !filter.flags.caseSensitive) {
9473 var reText = term.replace(/(\\)?\*/g, function ($0, $1) { return $1 ? $0 : '[\\s\\S]*?'; });
9474 return new RegExp('^' + reText + '$', regexpFlags);
9476 // Otherwise default to default condition
9478 return defaultCondition;
9485 * @name setupFilters
9486 * @methodOf ui.grid.service:rowSearcher
9487 * @description For a given columns filters (either col.filters, or [col.filter] can be passed in),
9488 * do all the parsing and pre-processing and store that data into a new filters object. The object
9489 * has the condition, the flags, the stripped term, and a parsed reg exp if there was one.
9491 * We could use a forEach in here, since it's much less performance sensitive, but since we're using
9492 * for loops everywhere else in this module...
9494 * @param {array} filters the filters from the column (col.filters or [col.filter])
9495 * @returns {array} An array of parsed/preprocessed filters
9497 rowSearcher.setupFilters = function setupFilters( filters ){
9498 var newFilters = [];
9500 var filtersLength = filters.length;
9501 for ( var i = 0; i < filtersLength; i++ ){
9502 var filter = filters[i];
9504 if ( filter.noTerm || !gridUtil.isNullOrUndefined(filter.term) ){
9507 var regexpFlags = '';
9508 if (!filter.flags || !filter.flags.caseSensitive) {
9512 if ( !gridUtil.isNullOrUndefined(filter.term) ){
9513 // it is possible to have noTerm. We don't need to copy that across, it was just a flag to avoid
9514 // getting the filter ignored if the filter was a function that didn't use a term
9515 newFilter.term = rowSearcher.stripTerm(filter);
9518 if ( filter.condition ){
9519 newFilter.condition = filter.condition;
9521 newFilter.condition = rowSearcher.guessCondition(filter);
9524 newFilter.flags = angular.extend( { caseSensitive: false, date: false }, filter.flags );
9526 if (newFilter.condition === uiGridConstants.filter.STARTS_WITH) {
9527 newFilter.startswithRE = new RegExp('^' + newFilter.term, regexpFlags);
9530 if (newFilter.condition === uiGridConstants.filter.ENDS_WITH) {
9531 newFilter.endswithRE = new RegExp(newFilter.term + '$', regexpFlags);
9534 if (newFilter.condition === uiGridConstants.filter.CONTAINS) {
9535 newFilter.containsRE = new RegExp(newFilter.term, regexpFlags);
9538 if (newFilter.condition === uiGridConstants.filter.EXACT) {
9539 newFilter.exactRE = new RegExp('^' + newFilter.term + '$', regexpFlags);
9542 newFilters.push(newFilter);
9551 * @name runColumnFilter
9552 * @methodOf ui.grid.service:rowSearcher
9553 * @description Runs a single pre-parsed filter against a cell, returning true
9554 * if the cell matches that one filter.
9556 * @param {Grid} grid the grid we're working against
9557 * @param {GridRow} row the row we're matching against
9558 * @param {GridCol} column the column that we're working against
9559 * @param {object} filter the specific, preparsed, filter that we want to test
9560 * @returns {boolean} true if we match (row stays visible)
9562 rowSearcher.runColumnFilter = function runColumnFilter(grid, row, column, filter) {
9563 // Cache typeof condition
9564 var conditionType = typeof(filter.condition);
9566 // Term to search for.
9567 var term = filter.term;
9569 // Get the column value for this row
9571 if ( column.filterCellFiltered ){
9572 value = grid.getCellDisplayValue(row, column);
9574 value = grid.getCellValue(row, column);
9578 // If the filter's condition is a RegExp, then use it
9579 if (filter.condition instanceof RegExp) {
9580 return filter.condition.test(value);
9583 // If the filter's condition is a function, run it
9584 if (conditionType === 'function') {
9585 return filter.condition(term, value, row, column);
9588 if (filter.startswithRE) {
9589 return filter.startswithRE.test(value);
9592 if (filter.endswithRE) {
9593 return filter.endswithRE.test(value);
9596 if (filter.containsRE) {
9597 return filter.containsRE.test(value);
9600 if (filter.exactRE) {
9601 return filter.exactRE.test(value);
9604 if (filter.condition === uiGridConstants.filter.NOT_EQUAL) {
9605 var regex = new RegExp('^' + term + '$');
9606 return !regex.exec(value);
9609 if (typeof(value) === 'number' && typeof(term) === 'string' ){
9610 // if the term has a decimal in it, it comes through as '9\.4', we need to take out the \
9611 // the same for negative numbers
9612 // TODO: I suspect the right answer is to look at escapeRegExp at the top of this code file, maybe it's not needed?
9613 var tempFloat = parseFloat(term.replace(/\\\./,'.').replace(/\\\-/,'-'));
9614 if (!isNaN(tempFloat)) {
9619 if (filter.flags.date === true) {
9620 value = new Date(value);
9621 // If the term has a dash in it, it comes through as '\-' -- we need to take out the '\'.
9622 term = new Date(term.replace(/\\/g, ''));
9625 if (filter.condition === uiGridConstants.filter.GREATER_THAN) {
9626 return (value > term);
9629 if (filter.condition === uiGridConstants.filter.GREATER_THAN_OR_EQUAL) {
9630 return (value >= term);
9633 if (filter.condition === uiGridConstants.filter.LESS_THAN) {
9634 return (value < term);
9637 if (filter.condition === uiGridConstants.filter.LESS_THAN_OR_EQUAL) {
9638 return (value <= term);
9647 * @name useExternalFiltering
9648 * @propertyOf ui.grid.class:GridOptions
9649 * @description False by default. When enabled, this setting suppresses the internal filtering.
9650 * All UI logic will still operate, allowing filter conditions to be set and modified.
9652 * The external filter logic can listen for the `filterChange` event, which fires whenever
9653 * a filter has been adjusted.
9657 * @name searchColumn
9658 * @methodOf ui.grid.service:rowSearcher
9659 * @description Process provided filters on provided column against a given row. If the row meets
9660 * the conditions on all the filters, return true.
9661 * @param {Grid} grid Grid to search in
9662 * @param {GridRow} row Row to search on
9663 * @param {GridCol} column Column with the filters to use
9664 * @param {array} filters array of pre-parsed/preprocessed filters to apply
9665 * @returns {boolean} Whether the column matches or not.
9667 rowSearcher.searchColumn = function searchColumn(grid, row, column, filters) {
9668 if (grid.options.useExternalFiltering) {
9672 var filtersLength = filters.length;
9673 for (var i = 0; i < filtersLength; i++) {
9674 var filter = filters[i];
9676 var ret = rowSearcher.runColumnFilter(grid, row, column, filter);
9689 * @methodOf ui.grid.service:rowSearcher
9690 * @description Run a search across the given rows and columns, marking any rows that don't
9691 * match the stored col.filters or col.filter as invisible.
9692 * @param {Grid} grid Grid instance to search inside
9693 * @param {Array[GridRow]} rows GridRows to filter
9694 * @param {Array[GridColumn]} columns GridColumns with filters to process
9696 rowSearcher.search = function search(grid, rows, columns) {
9698 * Added performance optimisations into this code base, as this logic creates deeply nested
9699 * loops and is therefore very performance sensitive. In particular, avoiding forEach as
9700 * this impacts some browser optimisers (particularly Chrome), using iterators instead
9703 // Don't do anything if we weren't passed any rows
9708 // don't filter if filtering currently disabled
9709 if (!grid.options.enableFiltering){
9713 // Build list of filters to apply
9714 var filterData = [];
9716 var colsLength = columns.length;
9718 var hasTerm = function( filters ) {
9719 var hasTerm = false;
9721 filters.forEach( function (filter) {
9722 if ( !gridUtil.isNullOrUndefined(filter.term) && filter.term !== '' || filter.noTerm ){
9730 for (var i = 0; i < colsLength; i++) {
9731 var col = columns[i];
9733 if (typeof(col.filters) !== 'undefined' && hasTerm(col.filters) ) {
9734 filterData.push( { col: col, filters: rowSearcher.setupFilters(col.filters) } );
9738 if (filterData.length > 0) {
9739 // define functions outside the loop, performance optimisation
9740 var foreachRow = function(grid, row, col, filters){
9741 if ( row.visible && !rowSearcher.searchColumn(grid, row, col, filters) ) {
9742 row.visible = false;
9746 var foreachFilterCol = function(grid, filterData){
9747 var rowsLength = rows.length;
9748 for ( var i = 0; i < rowsLength; i++){
9749 foreachRow(grid, rows[i], filterData.col, filterData.filters);
9753 // nested loop itself - foreachFilterCol, which in turn calls foreachRow
9754 var filterDataLength = filterData.length;
9755 for ( var j = 0; j < filterDataLength; j++){
9756 foreachFilterCol( grid, filterData[j] );
9759 if (grid.api.core.raise.rowsVisibleChanged) {
9760 grid.api.core.raise.rowsVisibleChanged();
9763 // drop any invisible rows
9764 // 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
9765 // rows = rows.filter(function(row){ return row.visible; });
9779 var module = angular.module('ui.grid');
9783 * @name ui.grid.class:RowSorter
9784 * @description RowSorter provides the default sorting mechanisms,
9785 * including guessing column types and applying appropriate sort
9790 module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGridConstants) {
9791 var currencyRegexStr =
9793 uiGridConstants.CURRENCY_SYMBOLS
9794 .map(function (a) { return '\\' + a; }) // Escape all the currency symbols ($ at least will jack up this regex)
9795 .join('|') + // Join all the symbols together with |s
9798 // /^[-+]?[£$¤¥]?[\d,.]+%?$/
9799 var numberStrRegex = new RegExp('^[-+]?' + currencyRegexStr + '[\\d,.]+' + currencyRegexStr + '%?$');
9802 // Cache of sorting functions. Once we create them, we don't want to keep re-doing it
9803 // this takes a piece of data from the cell and tries to determine its type and what sorting
9804 // function to use for it
9811 * @methodOf ui.grid.class:RowSorter
9813 * @description Assigns a sort function to use based on the itemType in the column
9814 * @param {string} itemType one of 'number', 'boolean', 'string', 'date', 'object'. And
9815 * error will be thrown for any other type.
9816 * @returns {function} a sort function that will sort that type
9818 rowSorter.guessSortFn = function guessSortFn(itemType) {
9821 return rowSorter.sortNumber;
9823 return rowSorter.sortNumberStr;
9825 return rowSorter.sortBool;
9827 return rowSorter.sortAlpha;
9829 return rowSorter.sortDate;
9831 return rowSorter.basicSort;
9833 throw new Error('No sorting function found for type:' + itemType);
9840 * @methodOf ui.grid.class:RowSorter
9842 * @description Sorts nulls and undefined to the bottom (top when
9843 * descending). Called by each of the internal sorters before
9844 * attempting to sort. Note that this method is available on the core api
9845 * via gridApi.core.sortHandleNulls
9846 * @param {object} a sort value a
9847 * @param {object} b sort value b
9848 * @returns {number} null if there were no nulls/undefineds, otherwise returns
9849 * a sort value that should be passed back from the sort function
9851 rowSorter.handleNulls = function handleNulls(a, b) {
9852 // We want to allow zero values and false values to be evaluated in the sort function
9853 if ((!a && a !== 0 && a !== false) || (!b && b !== 0 && b !== false)) {
9854 // We want to force nulls and such to the bottom when we sort... which effectively is "greater than"
9855 if ((!a && a !== 0 && a !== false) && (!b && b !== 0 && b !== false)) {
9858 else if (!a && a !== 0 && a !== false) {
9861 else if (!b && b !== 0 && b !== false) {
9871 * @methodOf ui.grid.class:RowSorter
9873 * @description Sorts any values that provide the < method, including strings
9874 * or numbers. Handles nulls and undefined through calling handleNulls
9875 * @param {object} a sort value a
9876 * @param {object} b sort value b
9877 * @returns {number} normal sort function, returns -ve, 0, +ve
9879 rowSorter.basicSort = function basicSort(a, b) {
9880 var nulls = rowSorter.handleNulls(a, b);
9881 if ( nulls !== null ){
9897 * @methodOf ui.grid.class:RowSorter
9899 * @description Sorts numerical values. Handles nulls and undefined through calling handleNulls
9900 * @param {object} a sort value a
9901 * @param {object} b sort value b
9902 * @returns {number} normal sort function, returns -ve, 0, +ve
9904 rowSorter.sortNumber = function sortNumber(a, b) {
9905 var nulls = rowSorter.handleNulls(a, b);
9906 if ( nulls !== null ){
9916 * @methodOf ui.grid.class:RowSorter
9917 * @name sortNumberStr
9918 * @description Sorts numerical values that are stored in a string (i.e. parses them to numbers first).
9919 * Handles nulls and undefined through calling handleNulls
9920 * @param {object} a sort value a
9921 * @param {object} b sort value b
9922 * @returns {number} normal sort function, returns -ve, 0, +ve
9924 rowSorter.sortNumberStr = function sortNumberStr(a, b) {
9925 var nulls = rowSorter.handleNulls(a, b);
9926 if ( nulls !== null ){
9929 var numA, // The parsed number form of 'a'
9930 numB, // The parsed number form of 'b'
9934 // Try to parse 'a' to a float
9935 numA = parseFloat(a.replace(/[^0-9.-]/g, ''));
9937 // If 'a' couldn't be parsed to float, flag it as bad
9942 // Try to parse 'b' to a float
9943 numB = parseFloat(b.replace(/[^0-9.-]/g, ''));
9945 // If 'b' couldn't be parsed to float, flag it as bad
9950 // We want bad ones to get pushed to the bottom... which effectively is "greater than"
9970 * @methodOf ui.grid.class:RowSorter
9972 * @description Sorts string values. Handles nulls and undefined through calling handleNulls
9973 * @param {object} a sort value a
9974 * @param {object} b sort value b
9975 * @returns {number} normal sort function, returns -ve, 0, +ve
9977 rowSorter.sortAlpha = function sortAlpha(a, b) {
9978 var nulls = rowSorter.handleNulls(a, b);
9979 if ( nulls !== null ){
9982 var strA = a.toString().toLowerCase(),
9983 strB = b.toString().toLowerCase();
9985 return strA === strB ? 0 : (strA < strB ? -1 : 1);
9992 * @methodOf ui.grid.class:RowSorter
9994 * @description Sorts date values. Handles nulls and undefined through calling handleNulls.
9995 * Handles date strings by converting to Date object if not already an instance of Date
9996 * @param {object} a sort value a
9997 * @param {object} b sort value b
9998 * @returns {number} normal sort function, returns -ve, 0, +ve
10000 rowSorter.sortDate = function sortDate(a, b) {
10001 var nulls = rowSorter.handleNulls(a, b);
10002 if ( nulls !== null ){
10005 if (!(a instanceof Date)) {
10008 if (!(b instanceof Date)){
10011 var timeA = a.getTime(),
10012 timeB = b.getTime();
10014 return timeA === timeB ? 0 : (timeA < timeB ? -1 : 1);
10021 * @methodOf ui.grid.class:RowSorter
10023 * @description Sorts boolean values, true is considered larger than false.
10024 * Handles nulls and undefined through calling handleNulls
10025 * @param {object} a sort value a
10026 * @param {object} b sort value b
10027 * @returns {number} normal sort function, returns -ve, 0, +ve
10029 rowSorter.sortBool = function sortBool(a, b) {
10030 var nulls = rowSorter.handleNulls(a, b);
10031 if ( nulls !== null ){
10050 * @methodOf ui.grid.class:RowSorter
10052 * @description Get the sort function for the column. Looks first in
10053 * rowSorter.colSortFnCache using the column name, failing that it
10054 * looks at col.sortingAlgorithm (and puts it in the cache), failing that
10055 * it guesses the sort algorithm based on the data type.
10057 * The cache currently seems a bit pointless, as none of the work we do is
10058 * processor intensive enough to need caching. Presumably in future we might
10059 * inspect the row data itself to guess the sort function, and in that case
10060 * it would make sense to have a cache, the infrastructure is in place to allow
10063 * @param {Grid} grid the grid to consider
10064 * @param {GridCol} col the column to find a function for
10065 * @param {array} rows an array of grid rows. Currently unused, but presumably in future
10066 * we might inspect the rows themselves to decide what sort of data might be there
10067 * @returns {function} the sort function chosen for the column
10069 rowSorter.getSortFn = function getSortFn(grid, col, rows) {
10072 // See if we already figured out what to use to sort the column and have it in the cache
10073 if (rowSorter.colSortFnCache[col.colDef.name]) {
10074 sortFn = rowSorter.colSortFnCache[col.colDef.name];
10076 // If the column has its OWN sorting algorithm, use that
10077 else if (col.sortingAlgorithm !== undefined) {
10078 sortFn = col.sortingAlgorithm;
10079 rowSorter.colSortFnCache[col.colDef.name] = col.sortingAlgorithm;
10081 // Always default to sortAlpha when sorting after a cellFilter
10082 else if ( col.sortCellFiltered && col.cellFilter ){
10083 sortFn = rowSorter.sortAlpha;
10084 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10086 // Try and guess what sort function to use
10088 // Guess the sort function
10089 sortFn = rowSorter.guessSortFn(col.colDef.type);
10091 // If we found a sort function, cache it
10093 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10096 // We assign the alpha sort because anything that is null/undefined will never get passed to
10097 // the actual sorting function. It will get caught in our null check and returned to be sorted
10098 // down to the bottom
10099 sortFn = rowSorter.sortAlpha;
10110 * @methodOf ui.grid.class:RowSorter
10111 * @name prioritySort
10112 * @description Used where multiple columns are present in the sort criteria,
10113 * we determine which column should take precedence in the sort by sorting
10114 * the columns based on their sort.priority
10116 * @param {gridColumn} a column a
10117 * @param {gridColumn} b column b
10118 * @returns {number} normal sort function, returns -ve, 0, +ve
10120 rowSorter.prioritySort = function (a, b) {
10121 // Both columns have a sort priority
10122 if (a.sort.priority !== undefined && b.sort.priority !== undefined) {
10123 // A is higher priority
10124 if (a.sort.priority < b.sort.priority) {
10128 else if (a.sort.priority === b.sort.priority) {
10136 // Only A has a priority
10137 else if (a.sort.priority || a.sort.priority === 0) {
10140 // Only B has a priority
10141 else if (b.sort.priority || b.sort.priority === 0) {
10144 // Neither has a priority
10153 * @name useExternalSorting
10154 * @propertyOf ui.grid.class:GridOptions
10155 * @description Prevents the internal sorting from executing. Events will
10156 * still be fired when the sort changes, and the sort information on
10157 * the columns will be updated, allowing an external sorter (for example,
10158 * server sorting) to be implemented. Defaults to false.
10163 * @methodOf ui.grid.class:RowSorter
10165 * @description sorts the grid
10166 * @param {Object} grid the grid itself
10167 * @param {array} rows the rows to be sorted
10168 * @param {array} columns the columns in which to look
10169 * for sort criteria
10170 * @returns {array} sorted rows
10172 rowSorter.sort = function rowSorterSort(grid, rows, columns) {
10173 // first make sure we are even supposed to do work
10178 if (grid.options.useExternalSorting){
10182 // Build the list of columns to sort by
10184 columns.forEach(function (col) {
10185 if (col.sort && !col.sort.ignoreSort && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
10186 sortCols.push(col);
10190 // Sort the "sort columns" by their sort priority
10191 sortCols = sortCols.sort(rowSorter.prioritySort);
10193 // Now rows to sort by, maintain original order
10194 if (sortCols.length === 0) {
10198 // Re-usable variables
10199 var col, direction;
10201 // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10202 var setIndex = function( row, idx ){
10203 row.entity.$$uiGridIndex = idx;
10205 rows.forEach(setIndex);
10207 // IE9-11 HACK.... the 'rows' variable would be empty where we call rowSorter.getSortFn(...) below. We have to use a separate reference
10208 // var d = data.slice(0);
10209 var r = rows.slice(0);
10211 // Now actually sort the data
10212 var rowSortFn = function (rowA, rowB) {
10217 while (tem === 0 && idx < sortCols.length) {
10218 // grab the metadata for the rest of the logic
10219 col = sortCols[idx];
10220 direction = sortCols[idx].sort.direction;
10222 sortFn = rowSorter.getSortFn(grid, col, r);
10226 if ( col.sortCellFiltered ){
10227 propA = grid.getCellDisplayValue(rowA, col);
10228 propB = grid.getCellDisplayValue(rowB, col);
10230 propA = grid.getCellValue(rowA, col);
10231 propB = grid.getCellValue(rowB, col);
10234 tem = sortFn(propA, propB, rowA, rowB, direction);
10239 // Chrome doesn't implement a stable sort function. If our sort returns 0
10240 // (i.e. the items are equal), and we're at the last sort column in the list,
10241 // then return the previous order using our custom
10244 return rowA.entity.$$uiGridIndex - rowB.entity.$$uiGridIndex;
10247 // Made it this far, we don't have to worry about null & undefined
10248 if (direction === uiGridConstants.ASC) {
10255 var newRows = rows.sort(rowSortFn);
10257 // remove the custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10258 var clearIndex = function( row, idx ){
10259 delete row.entity.$$uiGridIndex;
10261 rows.forEach(clearIndex);
10273 var module = angular.module('ui.grid');
10276 if (typeof Function.prototype.bind !== "function") {
10277 bindPolyfill = function() {
10278 var slice = Array.prototype.slice;
10279 return function(context) {
10281 args = slice.call(arguments, 1);
10283 return function() {
10284 return arguments.length ? fn.apply(context, args.concat(slice.call(arguments))) : fn.apply(context, args);
10287 return function() {
10288 return arguments.length ? fn.apply(context, arguments) : fn.call(context);
10294 function getStyles (elem) {
10296 if (typeof(e.length) !== 'undefined' && e.length) {
10300 return e.ownerDocument.defaultView.getComputedStyle(e, null);
10303 var rnumnonpx = new RegExp( "^(" + (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source + ")(?!px)[a-z%]+$", "i" ),
10304 // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
10305 // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
10306 rdisplayswap = /^(block|none|table(?!-c[ea]).+)/,
10307 cssShow = { position: "absolute", visibility: "hidden", display: "block" };
10309 function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
10310 var i = extra === ( isBorderBox ? 'border' : 'content' ) ?
10311 // If we already have the right measurement, avoid augmentation
10313 // Otherwise initialize for horizontal or vertical properties
10314 name === 'width' ? 1 : 0,
10318 var sides = ['Top', 'Right', 'Bottom', 'Left'];
10320 for ( ; i < 4; i += 2 ) {
10321 var side = sides[i];
10322 // dump('side', side);
10324 // both box models exclude margin, so add it if we want it
10325 if ( extra === 'margin' ) {
10326 var marg = parseFloat(styles[extra + side]);
10327 if (!isNaN(marg)) {
10331 // dump('val1', val);
10333 if ( isBorderBox ) {
10334 // border-box includes padding, so remove it if we want content
10335 if ( extra === 'content' ) {
10336 var padd = parseFloat(styles['padding' + side]);
10337 if (!isNaN(padd)) {
10339 // dump('val2', val);
10343 // at this point, extra isn't border nor margin, so remove border
10344 if ( extra !== 'margin' ) {
10345 var bordermarg = parseFloat(styles['border' + side + 'Width']);
10346 if (!isNaN(bordermarg)) {
10348 // dump('val3', val);
10353 // at this point, extra isn't content, so add padding
10354 var nocontentPad = parseFloat(styles['padding' + side]);
10355 if (!isNaN(nocontentPad)) {
10356 val += nocontentPad;
10357 // dump('val4', val);
10360 // at this point, extra isn't content nor padding, so add border
10361 if ( extra !== 'padding') {
10362 var nocontentnopad = parseFloat(styles['border' + side + 'Width']);
10363 if (!isNaN(nocontentnopad)) {
10364 val += nocontentnopad;
10365 // dump('val5', val);
10371 // dump('augVal', val);
10376 function getWidthOrHeight( elem, name, extra ) {
10377 // Start with offset property, which is equivalent to the border-box value
10378 var valueIsBorderBox = true,
10379 val, // = name === 'width' ? elem.offsetWidth : elem.offsetHeight,
10380 styles = getStyles(elem),
10381 isBorderBox = styles['boxSizing'] === 'border-box';
10383 // some non-html elements return undefined for offsetWidth, so check for null/undefined
10384 // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
10385 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
10386 if ( val <= 0 || val == null ) {
10387 // Fall back to computed then uncomputed css if necessary
10388 val = styles[name];
10389 if ( val < 0 || val == null ) {
10390 val = elem.style[ name ];
10393 // Computed unit is not pixels. Stop here and return.
10394 if ( rnumnonpx.test(val) ) {
10398 // we need the check for style in case a browser which returns unreliable values
10399 // for getComputedStyle silently falls back to the reliable elem.style
10400 valueIsBorderBox = isBorderBox &&
10401 ( true || val === elem.style[ name ] ); // use 'true' instead of 'support.boxSizingReliable()'
10403 // Normalize "", auto, and prepare for extra
10404 val = parseFloat( val ) || 0;
10407 // use the active box-sizing model to add/subtract irrelevant styles
10409 augmentWidthOrHeight(
10412 extra || ( isBorderBox ? "border" : "content" ),
10418 // dump('ret', ret, val);
10422 function getLineHeight(elm) {
10423 elm = angular.element(elm)[0];
10424 var parent = elm.parentElement;
10427 parent = document.getElementsByTagName('body')[0];
10430 return parseInt( getStyles(parent).fontSize ) || parseInt( getStyles(elm).fontSize ) || 16;
10433 var uid = ['0', '0', '0', '0'];
10434 var uidPrefix = 'uiGrid-';
10438 * @name ui.grid.service:GridUtil
10440 * @description Grid utility functions
10442 module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateCache', '$timeout', '$interval', '$injector', '$q', '$interpolate', 'uiGridConstants',
10443 function ($log, $window, $document, $http, $templateCache, $timeout, $interval, $injector, $q, $interpolate, uiGridConstants) {
10446 augmentWidthOrHeight: augmentWidthOrHeight,
10448 getStyles: getStyles,
10452 * @name createBoundedWrapper
10453 * @methodOf ui.grid.service:GridUtil
10455 * @param {object} Object to bind 'this' to
10456 * @param {method} Method to bind
10457 * @returns {Function} The wrapper that performs the binding
10460 * Binds given method to given object.
10462 * By means of a wrapper, ensures that ``method`` is always bound to
10463 * ``object`` regardless of its calling environment.
10464 * Iow, inside ``method``, ``this`` always points to ``object``.
10466 * See http://alistapart.com/article/getoutbindingsituations
10469 createBoundedWrapper: function(object, method) {
10470 return function() {
10471 return method.apply(object, arguments);
10478 * @name readableColumnName
10479 * @methodOf ui.grid.service:GridUtil
10481 * @param {string} columnName Column name as a string
10482 * @returns {string} Column name appropriately capitalized and split apart
10485 <example module="app">
10486 <file name="app.js">
10487 var app = angular.module('app', ['ui.grid']);
10489 app.controller('MainCtrl', ['$scope', 'gridUtil', function ($scope, gridUtil) {
10490 $scope.name = 'firstName';
10491 $scope.columnName = function(name) {
10492 return gridUtil.readableColumnName(name);
10496 <file name="index.html">
10497 <div ng-controller="MainCtrl">
10498 <strong>Column name:</strong> <input ng-model="name" />
10500 <strong>Output:</strong> <span ng-bind="columnName(name)"></span>
10505 readableColumnName: function (columnName) {
10506 // Convert underscores to spaces
10507 if (typeof(columnName) === 'undefined' || columnName === undefined || columnName === null) { return columnName; }
10509 if (typeof(columnName) !== 'string') {
10510 columnName = String(columnName);
10513 return columnName.replace(/_+/g, ' ')
10514 // Replace a completely all-capsed word with a first-letter-capitalized version
10515 .replace(/^[A-Z]+$/, function (match) {
10516 return angular.lowercase(angular.uppercase(match.charAt(0)) + match.slice(1));
10518 // Capitalize the first letter of words
10519 .replace(/([\w\u00C0-\u017F]+)/g, function (match) {
10520 return angular.uppercase(match.charAt(0)) + match.slice(1);
10522 // Put a space in between words that have partial capilizations (i.e. 'firstName' becomes 'First Name')
10523 // .replace(/([A-Z]|[A-Z]\w+)([A-Z])/g, "$1 $2");
10524 // .replace(/(\w+?|\w)([A-Z])/g, "$1 $2");
10525 .replace(/(\w+?(?=[A-Z]))/g, '$1 ');
10530 * @name getColumnsFromData
10531 * @methodOf ui.grid.service:GridUtil
10532 * @description Return a list of column names, given a data set
10534 * @param {string} data Data array for grid
10535 * @returns {Object} Column definitions with field accessor and column name
10540 { firstName: 'Bob', lastName: 'Jones' },
10541 { firstName: 'Frank', lastName: 'Smith' }
10544 var columnDefs = GridUtil.getColumnsFromData(data, excludeProperties);
10548 field: 'firstName',
10558 getColumnsFromData: function (data, excludeProperties) {
10559 var columnDefs = [];
10561 if (!data || typeof(data[0]) === 'undefined' || data[0] === undefined) { return []; }
10562 if (angular.isUndefined(excludeProperties)) { excludeProperties = []; }
10564 var item = data[0];
10566 angular.forEach(item,function (prop, propName) {
10567 if ( excludeProperties.indexOf(propName) === -1){
10580 * @methodOf ui.grid.service:GridUtil
10581 * @description Return a unique ID string
10583 * @returns {string} Unique string
10587 var id = GridUtil.newId();
10592 newId: (function() {
10593 var seedId = new Date().getTime();
10594 return function() {
10595 return seedId += 1;
10602 * @name getTemplate
10603 * @methodOf ui.grid.service:GridUtil
10604 * @description Get's template from cache / element / url
10606 * @param {string|element|promise} Either a string representing the template id, a string representing the template url,
10607 * an jQuery/Angualr element, or a promise that returns the template contents to use.
10608 * @returns {object} a promise resolving to template contents
10612 GridUtil.getTemplate(url).then(function (contents) {
10617 getTemplate: function (template) {
10618 // Try to fetch the template out of the templateCache
10619 if ($templateCache.get(template)) {
10620 return s.postProcessTemplate($templateCache.get(template));
10623 // See if the template is itself a promise
10624 if (template.hasOwnProperty('then')) {
10625 return template.then(s.postProcessTemplate);
10628 // If the template is an element, return the element
10630 if (angular.element(template).length > 0) {
10631 return $q.when(template).then(s.postProcessTemplate);
10635 //do nothing; not valid html
10638 s.logDebug('fetching url', template);
10640 // Default to trying to fetch the template as a url with $http
10641 return $http({ method: 'GET', url: template})
10643 function (result) {
10644 var templateHtml = result.data.trim();
10645 //put in templateCache for next call
10646 $templateCache.put(template, templateHtml);
10647 return templateHtml;
10650 throw new Error("Could not get template " + template + ": " + err);
10653 .then(s.postProcessTemplate);
10657 postProcessTemplate: function (template) {
10658 var startSym = $interpolate.startSymbol(),
10659 endSym = $interpolate.endSymbol();
10661 // If either of the interpolation symbols have been changed, we need to alter this template
10662 if (startSym !== '{{' || endSym !== '}}') {
10663 template = template.replace(/\{\{/g, startSym);
10664 template = template.replace(/\}\}/g, endSym);
10667 return $q.when(template);
10673 * @methodOf ui.grid.service:GridUtil
10674 * @description guesses the type of an argument
10676 * @param {string/number/bool/object} item variable to examine
10677 * @returns {string} one of the following
10684 guessType : function (item) {
10685 var itemType = typeof(item);
10687 // Check for numbers and booleans
10688 switch (itemType) {
10694 if (angular.isDate(item)) {
10704 * @name elementWidth
10705 * @methodOf ui.grid.service:GridUtil
10707 * @param {element} element DOM element
10708 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10710 * @returns {number} Element width in pixels, accounting for any borders, etc.
10712 elementWidth: function (elem) {
10718 * @name elementHeight
10719 * @methodOf ui.grid.service:GridUtil
10721 * @param {element} element DOM element
10722 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10724 * @returns {number} Element height in pixels, accounting for any borders, etc.
10726 elementHeight: function (elem) {
10730 // Thanks to http://stackoverflow.com/a/13382873/888165
10731 getScrollbarWidth: function() {
10732 var outer = document.createElement("div");
10733 outer.style.visibility = "hidden";
10734 outer.style.width = "100px";
10735 outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
10737 document.body.appendChild(outer);
10739 var widthNoScroll = outer.offsetWidth;
10740 // force scrollbars
10741 outer.style.overflow = "scroll";
10744 var inner = document.createElement("div");
10745 inner.style.width = "100%";
10746 outer.appendChild(inner);
10748 var widthWithScroll = inner.offsetWidth;
10751 outer.parentNode.removeChild(outer);
10753 return widthNoScroll - widthWithScroll;
10756 swap: function( elem, options, callback, args ) {
10760 // Remember the old values, and insert the new ones
10761 for ( name in options ) {
10762 old[ name ] = elem.style[ name ];
10763 elem.style[ name ] = options[ name ];
10766 ret = callback.apply( elem, args || [] );
10768 // Revert the old values
10769 for ( name in options ) {
10770 elem.style[ name ] = old[ name ];
10776 fakeElement: function( elem, options, callback, args ) {
10778 newElement = angular.element(elem).clone()[0];
10780 for ( name in options ) {
10781 newElement.style[ name ] = options[ name ];
10784 angular.element(document.body).append(newElement);
10786 ret = callback.call( newElement, newElement );
10788 angular.element(newElement).remove();
10795 * @name normalizeWheelEvent
10796 * @methodOf ui.grid.service:GridUtil
10798 * @param {event} event A mouse wheel event
10800 * @returns {event} A normalized event
10803 * Given an event from this list:
10805 * `wheel, mousewheel, DomMouseScroll, MozMousePixelScroll`
10808 * so that it stays consistent no matter what browser it comes from (i.e. scale it correctly and make sure the direction is right.)
10810 normalizeWheelEvent: function (event) {
10811 // var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];
10812 // var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
10813 var lowestDelta, lowestDeltaXY;
10815 var orgEvent = event || window.event,
10816 args = [].slice.call(arguments, 1),
10824 // event = $.event.fix(orgEvent);
10825 // event.type = 'mousewheel';
10827 // NOTE: jQuery masks the event and stores it in the event as originalEvent
10828 if (orgEvent.originalEvent) {
10829 orgEvent = orgEvent.originalEvent;
10832 // Old school scrollwheel delta
10833 if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; }
10834 if ( orgEvent.detail ) { delta = orgEvent.detail * -1; }
10836 // At a minimum, setup the deltaY to be delta
10839 // Firefox < 17 related to DOMMouseScroll event
10840 if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
10842 deltaX = delta * -1;
10845 // New school wheel delta (wheel event)
10846 if ( orgEvent.deltaY ) {
10847 deltaY = orgEvent.deltaY * -1;
10850 if ( orgEvent.deltaX ) {
10851 deltaX = orgEvent.deltaX;
10852 delta = deltaX * -1;
10856 if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; }
10857 if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX; }
10859 // Look for lowest delta to normalize the delta values
10860 absDelta = Math.abs(delta);
10861 if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; }
10862 absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX));
10863 if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; }
10865 // Get a whole value for the deltas
10866 fn = delta > 0 ? 'floor' : 'ceil';
10867 delta = Math[fn](delta / lowestDelta);
10868 deltaX = Math[fn](deltaX / lowestDeltaXY);
10869 deltaY = Math[fn](deltaY / lowestDeltaXY);
10878 // Stolen from Modernizr
10879 // TODO: make this, and everythign that flows from it, robust
10880 //http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
10881 isTouchEnabled: function() {
10884 if (('ontouchstart' in $window) || $window.DocumentTouch && $document instanceof DocumentTouch) {
10891 isNullOrUndefined: function(obj) {
10892 if (obj === undefined || obj === null) {
10898 endsWith: function(str, suffix) {
10899 if (!str || !suffix || typeof str !== "string") {
10902 return str.indexOf(suffix, str.length - suffix.length) !== -1;
10905 arrayContainsObjectWithProperty: function(array, propertyName, propertyValue) {
10907 angular.forEach(array, function (object) {
10908 if (object[propertyName] === propertyValue) {
10915 //// Shim requestAnimationFrame
10916 //requestAnimationFrame: $window.requestAnimationFrame && $window.requestAnimationFrame.bind($window) ||
10917 // $window.webkitRequestAnimationFrame && $window.webkitRequestAnimationFrame.bind($window) ||
10919 // return $timeout(fn, 10, false);
10922 numericAndNullSort: function (a, b) {
10923 if (a === null) { return 1; }
10924 if (b === null) { return -1; }
10925 if (a === null && b === null) { return 0; }
10929 // Disable ngAnimate animations on an element
10930 disableAnimations: function (element) {
10933 $animate = $injector.get('$animate');
10934 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10935 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10936 $animate.enabled(element, false);
10938 $animate.enabled(false, element);
10944 enableAnimations: function (element) {
10947 $animate = $injector.get('$animate');
10948 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10949 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10950 $animate.enabled(element, true);
10952 $animate.enabled(true, element);
10959 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
10960 nextUid: function nextUid() {
10961 var index = uid.length;
10966 digit = uid[index].charCodeAt(0);
10967 if (digit === 57 /*'9'*/) {
10969 return uidPrefix + uid.join('');
10971 if (digit === 90 /*'Z'*/) {
10974 uid[index] = String.fromCharCode(digit + 1);
10975 return uidPrefix + uid.join('');
10980 return uidPrefix + uid.join('');
10983 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
10984 hashKey: function hashKey(obj) {
10985 var objType = typeof obj,
10988 if (objType === 'object' && obj !== null) {
10989 if (typeof (key = obj.$$hashKey) === 'function') {
10990 // must invoke on object to keep the right this
10991 key = obj.$$hashKey();
10993 else if (typeof(obj.$$hashKey) !== 'undefined' && obj.$$hashKey) {
10994 key = obj.$$hashKey;
10996 else if (key === undefined) {
10997 key = obj.$$hashKey = s.nextUid();
11004 return objType + ':' + key;
11007 resetUids: function () {
11008 uid = ['0', '0', '0'];
11013 * @methodOf ui.grid.service:GridUtil
11015 * @description wraps the $log method, allowing us to choose different
11016 * treatment within ui-grid if we so desired. At present we only log
11017 * error messages if uiGridConstants.LOG_ERROR_MESSAGES is set to true
11018 * @param {string} logMessage message to be logged to the console
11021 logError: function( logMessage ){
11022 if ( uiGridConstants.LOG_ERROR_MESSAGES ){
11023 $log.error( logMessage );
11029 * @methodOf ui.grid.service:GridUtil
11031 * @description wraps the $log method, allowing us to choose different
11032 * treatment within ui-grid if we so desired. At present we only log
11033 * warning messages if uiGridConstants.LOG_WARN_MESSAGES is set to true
11034 * @param {string} logMessage message to be logged to the console
11037 logWarn: function( logMessage ){
11038 if ( uiGridConstants.LOG_WARN_MESSAGES ){
11039 $log.warn( logMessage );
11045 * @methodOf ui.grid.service:GridUtil
11047 * @description wraps the $log method, allowing us to choose different
11048 * treatment within ui-grid if we so desired. At present we only log
11049 * debug messages if uiGridConstants.LOG_DEBUG_MESSAGES is set to true
11052 logDebug: function() {
11053 if ( uiGridConstants.LOG_DEBUG_MESSAGES ){
11054 $log.debug.apply($log, arguments);
11063 * @propertyOf ui.grid.service:GridUtil
11064 * @description Provies a set of methods to set the document focus inside the grid.
11065 * See {@link ui.grid.service:GridUtil.focus} for more information.
11070 * @name ui.grid.service:GridUtil.focus
11071 * @description Provies a set of methods to set the document focus inside the grid.
11072 * Timeouts are utilized to ensure that the focus is invoked after any other event has been triggered.
11073 * e.g. click events that need to run before the focus or
11074 * inputs elements that are in a disabled state but are enabled when those events
11079 //http://stackoverflow.com/questions/25596399/set-element-focus-in-angular-way
11082 * @methodOf ui.grid.service:GridUtil.focus
11084 * @description Sets the focus of the document to the given id value.
11085 * If provided with the grid object it will automatically append the grid id.
11086 * This is done to encourage unique dom id's as it allows for multiple grids on a
11088 * @param {String} id the id of the dom element to set the focus on
11089 * @param {Object=} Grid the grid object for this grid instance. See: {@link ui.grid.class:Grid}
11090 * @param {Number} Grid.id the unique id for this grid. Already set on an initialized grid object.
11091 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11092 * then the promise will fail with the `'canceled'` reason.
11094 byId: function (id, Grid) {
11095 this._purgeQueue();
11096 var promise = $timeout(function() {
11097 var elementID = (Grid && Grid.id ? Grid.id + '-' : '') + id;
11098 var element = $window.document.getElementById(elementID);
11102 s.logWarn('[focus.byId] Element id ' + elementID + ' was not found.');
11105 this.queue.push(promise);
11111 * @methodOf ui.grid.service:GridUtil.focus
11113 * @description Sets the focus of the document to the given dom element.
11114 * @param {(element|angular.element)} element the DOM element to set the focus on
11115 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11116 * then the promise will fail with the `'canceled'` reason.
11118 byElement: function(element){
11119 if (!angular.isElement(element)){
11120 s.logWarn("Trying to focus on an element that isn\'t an element.");
11121 return $q.reject('not-element');
11123 element = angular.element(element);
11124 this._purgeQueue();
11125 var promise = $timeout(function(){
11127 element[0].focus();
11130 this.queue.push(promise);
11135 * @methodOf ui.grid.service:GridUtil.focus
11137 * @description Sets the focus of the document to the given dom element.
11138 * @param {(element|angular.element)} parentElement the parent/ancestor of the dom element that you are selecting using the query selector
11139 * @param {String} querySelector finds the dom element using the {@link http://www.w3schools.com/jsref/met_document_queryselector.asp querySelector}
11140 * @param {boolean} [aSync=false] If true then the selector will be querried inside of a timeout. Otherwise the selector will be querried imidately
11141 * then the focus will be called.
11142 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11143 * then the promise will fail with the `'canceled'` reason.
11145 bySelector: function(parentElement, querySelector, aSync){
11147 if (!angular.isElement(parentElement)){
11148 throw new Error("The parent element is not an element.");
11150 // Ensure that this is an angular element.
11151 // It is fine if this is already an angular element.
11152 parentElement = angular.element(parentElement);
11153 var focusBySelector = function(){
11154 var element = parentElement[0].querySelector(querySelector);
11155 return self.byElement(element);
11157 this._purgeQueue();
11158 if (aSync){ //Do this asynchronysly
11159 var promise = $timeout(focusBySelector);
11160 this.queue.push($timeout(focusBySelector));
11163 return focusBySelector();
11166 _purgeQueue: function(){
11167 this.queue.forEach(function(element){
11168 $timeout.cancel(element);
11175 ['width', 'height'].forEach(function (name) {
11176 var capsName = angular.uppercase(name.charAt(0)) + name.substr(1);
11177 s['element' + capsName] = function (elem, extra) {
11179 if (e && typeof(e.length) !== 'undefined' && e.length) {
11184 var styles = getStyles(e);
11185 return e.offsetWidth === 0 && rdisplayswap.test(styles.display) ?
11186 s.swap(e, cssShow, function() {
11187 return getWidthOrHeight(e, name, extra );
11189 getWidthOrHeight( e, name, extra );
11196 s['outerElement' + capsName] = function (elem, margin) {
11197 return elem ? s['element' + capsName].call(this, elem, margin ? 'margin' : 'border') : null;
11201 // http://stackoverflow.com/a/24107550/888165
11202 s.closestElm = function closestElm(el, selector) {
11203 if (typeof(el.length) !== 'undefined' && el.length) {
11209 // find vendor prefix
11210 ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) {
11211 if (typeof document.body[fn] === 'function') {
11218 // traverse parents
11220 while (el !== null) {
11221 parent = el.parentElement;
11222 if (parent !== null && parent[matchesFn](selector)) {
11231 s.type = function (obj) {
11232 var text = Function.prototype.toString.call(obj.constructor);
11233 return text.match(/function (.*?)\(/)[1];
11236 s.getBorderSize = function getBorderSize(elem, borderType) {
11237 if (typeof(elem.length) !== 'undefined' && elem.length) {
11241 var styles = getStyles(elem);
11243 // If a specific border is supplied, like 'top', read the 'borderTop' style property
11245 borderType = 'border' + borderType.charAt(0).toUpperCase() + borderType.slice(1);
11248 borderType = 'border';
11251 borderType += 'Width';
11253 var val = parseInt(styles[borderType], 10);
11263 // http://stackoverflow.com/a/22948274/888165
11264 // TODO: Opera? Mobile?
11265 s.detectBrowser = function detectBrowser() {
11266 var userAgent = $window.navigator.userAgent;
11268 var browsers = {chrome: /chrome/i, safari: /safari/i, firefox: /firefox/i, ie: /internet explorer|trident\//i};
11270 for (var key in browsers) {
11271 if (browsers[key].test(userAgent)) {
11279 // Borrowed from https://github.com/othree/jquery.rtl-scroll-type
11280 // Determine the scroll "type" this browser is using for RTL
11281 s.rtlScrollType = function rtlScrollType() {
11282 if (rtlScrollType.type) {
11283 return rtlScrollType.type;
11286 var definer = angular.element('<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>')[0],
11289 document.body.appendChild(definer);
11291 if (definer.scrollLeft > 0) {
11295 definer.scrollLeft = 1;
11296 if (definer.scrollLeft === 0) {
11301 angular.element(definer).remove();
11302 rtlScrollType.type = type;
11309 * @name normalizeScrollLeft
11310 * @methodOf ui.grid.service:GridUtil
11312 * @param {element} element The element to get the `scrollLeft` from.
11313 * @param {grid} grid - grid used to normalize (uses the rtl property)
11315 * @returns {number} A normalized scrollLeft value for the current browser.
11318 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method normalizes them
11320 s.normalizeScrollLeft = function normalizeScrollLeft(element, grid) {
11321 if (typeof(element.length) !== 'undefined' && element.length) {
11322 element = element[0];
11325 var scrollLeft = element.scrollLeft;
11327 if (grid.isRTL()) {
11328 switch (s.rtlScrollType()) {
11330 return element.scrollWidth - scrollLeft - element.clientWidth;
11332 return Math.abs(scrollLeft);
11343 * @name denormalizeScrollLeft
11344 * @methodOf ui.grid.service:GridUtil
11346 * @param {element} element The element to normalize the `scrollLeft` value for
11347 * @param {number} scrollLeft The `scrollLeft` value to denormalize.
11348 * @param {grid} grid The grid that owns the scroll event.
11350 * @returns {number} A normalized scrollLeft value for the current browser.
11353 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method denormalizes a value for the current browser.
11355 s.denormalizeScrollLeft = function denormalizeScrollLeft(element, scrollLeft, grid) {
11356 if (typeof(element.length) !== 'undefined' && element.length) {
11357 element = element[0];
11360 if (grid.isRTL()) {
11361 switch (s.rtlScrollType()) {
11363 // Get the max scroll for the element
11364 var maxScrollLeft = element.scrollWidth - element.clientWidth;
11366 // Subtract the current scroll amount from the max scroll
11367 return maxScrollLeft - scrollLeft;
11369 return scrollLeft * -1;
11381 * @methodOf ui.grid.service:GridUtil
11383 * @param {string} path Path to evaluate
11385 * @returns {string} A path that is normalized.
11388 * Takes a field path and converts it to bracket notation to allow for special characters in path
11391 * gridUtil.preEval('property') == 'property'
11392 * gridUtil.preEval('nested.deep.prop-erty') = "nested['deep']['prop-erty']"
11395 s.preEval = function (path) {
11396 var m = uiGridConstants.BRACKET_REGEXP.exec(path);
11398 return (m[1] ? s.preEval(m[1]) : m[1]) + m[2] + (m[3] ? s.preEval(m[3]) : m[3]);
11400 path = path.replace(uiGridConstants.APOS_REGEXP, '\\\'');
11401 var parts = path.split(uiGridConstants.DOT_REGEXP);
11402 var preparsed = [parts.shift()]; // first item must be var notation, thus skip
11403 angular.forEach(parts, function (part) {
11404 preparsed.push(part.replace(uiGridConstants.FUNC_REGEXP, '\']$1'));
11406 return preparsed.join('[\'');
11413 * @methodOf ui.grid.service:GridUtil
11415 * @param {function} func function to debounce
11416 * @param {number} wait milliseconds to delay
11417 * @param {boolean} immediate execute before delay
11419 * @returns {function} A function that can be executed as debounced function
11422 * Copied from https://github.com/shahata/angular-debounce
11423 * Takes a function, decorates it to execute only 1 time after multiple calls, and returns the decorated function
11426 * var debouncedFunc = gridUtil.debounce(function(){alert('debounced');}, 500);
11432 s.debounce = function (func, wait, immediate) {
11433 var timeout, args, context, result;
11434 function debounce() {
11435 /* jshint validthis:true */
11438 var later = function () {
11441 result = func.apply(context, args);
11444 var callNow = immediate && !timeout;
11446 $timeout.cancel(timeout);
11448 timeout = $timeout(later, wait);
11450 result = func.apply(context, args);
11454 debounce.cancel = function () {
11455 $timeout.cancel(timeout);
11464 * @methodOf ui.grid.service:GridUtil
11466 * @param {function} func function to throttle
11467 * @param {number} wait milliseconds to delay after first trigger
11468 * @param {Object} params to use in throttle.
11470 * @returns {function} A function that can be executed as throttled function
11473 * Adapted from debounce function (above)
11474 * Potential keys for Params Object are:
11475 * trailing (bool) - whether to trigger after throttle time ends if called multiple times
11476 * Updated to use $interval rather than $timeout, as protractor (e2e tests) is able to work with $interval,
11477 * but not with $timeout
11479 * Note that when using throttle, you need to use throttle to create a new function upfront, then use the function
11480 * return from that call each time you need to call throttle. If you call throttle itself repeatedly, the lastCall
11481 * variable will get overwritten and the throttling won't work
11485 * var throttledFunc = gridUtil.throttle(function(){console.log('throttled');}, 500, {trailing: true});
11486 * throttledFunc(); //=> logs throttled
11487 * throttledFunc(); //=> queues attempt to log throttled for ~500ms (since trailing param is truthy)
11488 * throttledFunc(); //=> updates arguments to keep most-recent request, but does not do anything else.
11491 s.throttle = function(func, wait, options){
11492 options = options || {};
11493 var lastCall = 0, queued = null, context, args;
11495 function runFunc(endDate){
11496 lastCall = +new Date();
11497 func.apply(context, args);
11498 $interval(function(){ queued = null; }, 0, 1);
11502 /* jshint validthis:true */
11505 if (queued === null){
11506 var sinceLast = +new Date() - lastCall;
11507 if (sinceLast > wait){
11510 else if (options.trailing){
11511 queued = $interval(runFunc, wait - sinceLast, 1);
11521 s.addOff = function (eventName) {
11522 s.off[eventName] = function (elm, fn) {
11523 var idx = s._events[eventName].indexOf(fn);
11525 s._events[eventName].removeAt(idx);
11530 var mouseWheeltoBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
11531 nullLowestDeltaTimeout,
11534 s.on.mousewheel = function (elm, fn) {
11535 if (!elm || !fn) { return; }
11537 var $elm = angular.element(elm);
11539 // Store the line height and page height for this particular element
11540 $elm.data('mousewheel-line-height', getLineHeight($elm));
11541 $elm.data('mousewheel-page-height', s.elementHeight($elm));
11542 if (!$elm.data('mousewheel-callbacks')) { $elm.data('mousewheel-callbacks', {}); }
11544 var cbs = $elm.data('mousewheel-callbacks');
11545 cbs[fn] = (Function.prototype.bind || bindPolyfill).call(mousewheelHandler, $elm[0], fn);
11547 // Bind all the mousew heel events
11548 for ( var i = mouseWheeltoBind.length; i; ) {
11549 $elm.on(mouseWheeltoBind[--i], cbs[fn]);
11552 s.off.mousewheel = function (elm, fn) {
11553 var $elm = angular.element(elm);
11555 var cbs = $elm.data('mousewheel-callbacks');
11556 var handler = cbs[fn];
11559 for ( var i = mouseWheeltoBind.length; i; ) {
11560 $elm.off(mouseWheeltoBind[--i], handler);
11566 if (Object.keys(cbs).length === 0) {
11567 $elm.removeData('mousewheel-line-height');
11568 $elm.removeData('mousewheel-page-height');
11569 $elm.removeData('mousewheel-callbacks');
11573 function mousewheelHandler(fn, event) {
11574 var $elm = angular.element(this);
11583 // jQuery masks events
11584 if (event.originalEvent) { event = event.originalEvent; }
11586 if ( 'detail' in event ) { deltaY = event.detail * -1; }
11587 if ( 'wheelDelta' in event ) { deltaY = event.wheelDelta; }
11588 if ( 'wheelDeltaY' in event ) { deltaY = event.wheelDeltaY; }
11589 if ( 'wheelDeltaX' in event ) { deltaX = event.wheelDeltaX * -1; }
11591 // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
11592 if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
11593 deltaX = deltaY * -1;
11597 // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
11598 delta = deltaY === 0 ? deltaX : deltaY;
11600 // New school wheel delta (wheel event)
11601 if ( 'deltaY' in event ) {
11602 deltaY = event.deltaY * -1;
11605 if ( 'deltaX' in event ) {
11606 deltaX = event.deltaX;
11607 if ( deltaY === 0 ) { delta = deltaX * -1; }
11610 // No change actually happened, no reason to go any further
11611 if ( deltaY === 0 && deltaX === 0 ) { return; }
11613 // Need to convert lines and pages to pixels if we aren't already in pixels
11614 // There are three delta modes:
11615 // * deltaMode 0 is by pixels, nothing to do
11616 // * deltaMode 1 is by lines
11617 // * deltaMode 2 is by pages
11618 if ( event.deltaMode === 1 ) {
11619 var lineHeight = $elm.data('mousewheel-line-height');
11620 delta *= lineHeight;
11621 deltaY *= lineHeight;
11622 deltaX *= lineHeight;
11624 else if ( event.deltaMode === 2 ) {
11625 var pageHeight = $elm.data('mousewheel-page-height');
11626 delta *= pageHeight;
11627 deltaY *= pageHeight;
11628 deltaX *= pageHeight;
11631 // Store lowest absolute delta to normalize the delta values
11632 absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );
11634 if ( !lowestDelta || absDelta < lowestDelta ) {
11635 lowestDelta = absDelta;
11637 // Adjust older deltas if necessary
11638 if ( shouldAdjustOldDeltas(event, absDelta) ) {
11643 // Get a whole, normalized value for the deltas
11644 delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta);
11645 deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
11646 deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);
11648 event.deltaMode = 0;
11650 // Normalise offsetX and offsetY properties
11651 // if ($elm[0].getBoundingClientRect ) {
11652 // var boundingRect = $(elm)[0].getBoundingClientRect();
11653 // offsetX = event.clientX - boundingRect.left;
11654 // offsetY = event.clientY - boundingRect.top;
11657 // event.deltaX = deltaX;
11658 // event.deltaY = deltaY;
11659 // event.deltaFactor = lowestDelta;
11662 originalEvent: event,
11665 deltaFactor: lowestDelta,
11666 preventDefault: function () { event.preventDefault(); },
11667 stopPropagation: function () { event.stopPropagation(); }
11670 // Clearout lowestDelta after sometime to better
11671 // handle multiple device types that give
11672 // a different lowestDelta
11673 // Ex: trackpad = 3 and mouse wheel = 120
11674 if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
11675 nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);
11677 fn.call($elm[0], newEvent);
11680 function nullLowestDelta() {
11681 lowestDelta = null;
11684 function shouldAdjustOldDeltas(orgEvent, absDelta) {
11685 // If this is an older event and the delta is divisable by 120,
11686 // then we are assuming that the browser is treating this as an
11687 // older mouse wheel event and that we should divide the deltas
11688 // by 40 to try and get a more usable deltaFactor.
11689 // Side note, this actually impacts the reported scroll distance
11690 // in older browsers and can cause scrolling to be slower than native.
11691 // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
11692 return orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
11698 // Add 'px' to the end of a number string if it doesn't have it already
11699 module.filter('px', function() {
11700 return function(str) {
11701 if (str.match(/^[\d\.]+$/)) {
11713 angular.module('ui.grid').config(['$provide', function($provide) {
11714 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11720 description: 'Přesuňte záhlaví zde pro vytvoření skupiny dle sloupce.'
11723 placeholder: 'Hledat...',
11724 showingItems: 'Zobrazuji položky:',
11725 selectedItems: 'Vybrané položky:',
11726 totalItems: 'Celkem položek:',
11727 size: 'Velikost strany:',
11728 first: 'První strana',
11729 next: 'Další strana',
11730 previous: 'Předchozí strana',
11731 last: 'Poslední strana'
11734 text: 'Vyberte sloupec:'
11737 ascending: 'Seřadit od A-Z',
11738 descending: 'Seřadit od Z-A',
11739 remove: 'Odebrat seřazení'
11742 hide: 'Schovat sloupec'
11745 count: 'celkem řádků: ',
11752 pinLeft: 'Zamknout vlevo',
11753 pinRight: 'Zamknout vpravo',
11757 columns: 'Sloupce:',
11758 importerTitle: 'Importovat soubor',
11759 exporterAllAsCsv: 'Exportovat všechna data do csv',
11760 exporterVisibleAsCsv: 'Exportovat viditelná data do csv',
11761 exporterSelectedAsCsv: 'Exportovat vybraná data do csv',
11762 exporterAllAsPdf: 'Exportovat všechna data do pdf',
11763 exporterVisibleAsPdf: 'Exportovat viditelná data do pdf',
11764 exporterSelectedAsPdf: 'Exportovat vybraná data do pdf',
11765 clearAllFilters: 'Odstranit všechny filtry'
11768 noHeaders: 'Názvy sloupců se nepodařilo získat, obsahuje soubor záhlaví?',
11769 noObjects: 'Data se nepodařilo zpracovat, obsahuje soubor řádky mimo záhlaví?',
11770 invalidCsv: 'Soubor nelze zpracovat, jedná se o CSV?',
11771 invalidJson: 'Soubor nelze zpracovat, je to JSON?',
11772 jsonNotArray: 'Soubor musí obsahovat json. Ukončuji..'
11775 sizes: 'položek na stránku',
11776 totalItems: 'položek'
11780 ungroup: 'Odebrat seskupení',
11781 aggregate_count: 'Agregace: Count',
11782 aggregate_sum: 'Agregace: Sum',
11783 aggregate_max: 'Agregace: Max',
11784 aggregate_min: 'Agregace: Min',
11785 aggregate_avg: 'Agregace: Avg',
11786 aggregate_remove: 'Agregace: Odebrat'
11790 // support varianty of different czech keys.
11791 $delegate.add('cs', lang);
11792 $delegate.add('cz', lang);
11793 $delegate.add('cs-cz', lang);
11794 $delegate.add('cs-CZ', lang);
11801 angular.module('ui.grid').config(['$provide', function($provide) {
11802 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11803 $delegate.add('da', {
11808 description: 'Grupér rækker udfra en kolonne ved at trække dens overskift hertil.'
11811 placeholder: 'Søg...',
11812 showingItems: 'Viste rækker:',
11813 selectedItems: 'Valgte rækker:',
11814 totalItems: 'Rækker totalt:',
11815 size: 'Side størrelse:',
11816 first: 'Første side',
11817 next: 'Næste side',
11818 previous: 'Forrige side',
11819 last: 'Sidste side'
11822 text: 'Vælg kolonner:'
11825 hide: 'Skjul kolonne'
11828 count: 'samlede rækker: ',
11835 columns: 'Columns:',
11836 importerTitle: 'Import file',
11837 exporterAllAsCsv: 'Export all data as csv',
11838 exporterVisibleAsCsv: 'Export visible data as csv',
11839 exporterSelectedAsCsv: 'Export selected data as csv',
11840 exporterAllAsPdf: 'Export all data as pdf',
11841 exporterVisibleAsPdf: 'Export visible data as pdf',
11842 exporterSelectedAsPdf: 'Export selected data as pdf',
11843 clearAllFilters: 'Clear all filters'
11846 noHeaders: 'Column names were unable to be derived, does the file have a header?',
11847 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
11848 invalidCsv: 'File was unable to be processed, is it valid CSV?',
11849 invalidJson: 'File was unable to be processed, is it valid Json?',
11850 jsonNotArray: 'Imported json file must contain an array, aborting.'
11859 angular.module('ui.grid').config(['$provide', function ($provide) {
11860 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
11861 $delegate.add('de', {
11866 description: 'Ziehen Sie eine Spaltenüberschrift hierhin, um nach dieser Spalte zu gruppieren.'
11869 placeholder: 'Suche...',
11870 showingItems: 'Zeige Einträge:',
11871 selectedItems: 'Ausgewählte Einträge:',
11872 totalItems: 'Einträge gesamt:',
11873 size: 'Einträge pro Seite:',
11874 first: 'Erste Seite',
11875 next: 'Nächste Seite',
11876 previous: 'Vorherige Seite',
11877 last: 'Letzte Seite'
11880 text: 'Spalten auswählen:'
11883 ascending: 'aufsteigend sortieren',
11884 descending: 'absteigend sortieren',
11885 remove: 'Sortierung zurücksetzen'
11888 hide: 'Spalte ausblenden'
11891 count: 'Zeilen insgesamt: ',
11893 avg: 'Durchschnitt: ',
11898 pinLeft: 'Links anheften',
11899 pinRight: 'Rechts anheften',
11903 columns: 'Spalten:',
11904 importerTitle: 'Datei importieren',
11905 exporterAllAsCsv: 'Alle Daten als CSV exportieren',
11906 exporterVisibleAsCsv: 'sichtbare Daten als CSV exportieren',
11907 exporterSelectedAsCsv: 'markierte Daten als CSV exportieren',
11908 exporterAllAsPdf: 'Alle Daten als PDF exportieren',
11909 exporterVisibleAsPdf: 'sichtbare Daten als PDF exportieren',
11910 exporterSelectedAsPdf: 'markierte Daten als CSV exportieren',
11911 clearAllFilters: 'Alle filter reinigen'
11914 noHeaders: 'Es konnten keine Spaltennamen ermittelt werden. Sind in der Datei Spaltendefinitionen enthalten?',
11915 noObjects: 'Es konnten keine Zeileninformationen gelesen werden, Sind in der Datei außer den Spaltendefinitionen auch Daten enthalten?',
11916 invalidCsv: 'Die Datei konnte nicht eingelesen werden, ist es eine gültige CSV-Datei?',
11917 invalidJson: 'Die Datei konnte nicht eingelesen werden. Enthält sie gültiges JSON?',
11918 jsonNotArray: 'Die importierte JSON-Datei muß ein Array enthalten. Breche Import ab.'
11921 sizes: 'Einträge pro Seite',
11922 totalItems: 'Einträge'
11925 group: 'Gruppieren',
11926 ungroup: 'Gruppierung aufheben',
11927 aggregate_count: 'Agg: Anzahl',
11928 aggregate_sum: 'Agg: Summe',
11929 aggregate_max: 'Agg: Maximum',
11930 aggregate_min: 'Agg: Minimum',
11931 aggregate_avg: 'Agg: Mittelwert',
11932 aggregate_remove: 'Aggregation entfernen'
11941 angular.module('ui.grid').config(['$provide', function($provide) {
11942 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11943 $delegate.add('en', {
11946 defaultFilterLabel: 'Filter for column',
11947 removeFilter: 'Remove Filter',
11948 columnMenuButtonLabel: 'Column Menu'
11950 priority: 'Priority:',
11951 filterLabel: "Filter for column: "
11957 description: 'Drag a column header here and drop it to group by that column.'
11960 placeholder: 'Search...',
11961 showingItems: 'Showing Items:',
11962 selectedItems: 'Selected Items:',
11963 totalItems: 'Total Items:',
11964 size: 'Page Size:',
11965 first: 'First Page',
11967 previous: 'Previous Page',
11971 text: 'Choose Columns:'
11974 ascending: 'Sort Ascending',
11975 descending: 'Sort Descending',
11977 remove: 'Remove Sort'
11980 hide: 'Hide Column'
11983 count: 'total rows: ',
11990 pinLeft: 'Pin Left',
11991 pinRight: 'Pin Right',
11999 buttonLabel: 'Grid Menu'
12001 columns: 'Columns:',
12002 importerTitle: 'Import file',
12003 exporterAllAsCsv: 'Export all data as csv',
12004 exporterVisibleAsCsv: 'Export visible data as csv',
12005 exporterSelectedAsCsv: 'Export selected data as csv',
12006 exporterAllAsPdf: 'Export all data as pdf',
12007 exporterVisibleAsPdf: 'Export visible data as pdf',
12008 exporterSelectedAsPdf: 'Export selected data as pdf',
12009 clearAllFilters: 'Clear all filters'
12012 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12013 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12014 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12015 invalidJson: 'File was unable to be processed, is it valid Json?',
12016 jsonNotArray: 'Imported json file must contain an array, aborting.'
12020 pageToFirst: 'Page to first',
12021 pageBack: 'Page back',
12022 pageSelected: 'Selected page',
12023 pageForward: 'Page forward',
12024 pageToLast: 'Page to last'
12026 sizes: 'items per page',
12027 totalItems: 'items',
12028 through: 'through',
12033 ungroup: 'Ungroup',
12034 aggregate_count: 'Agg: Count',
12035 aggregate_sum: 'Agg: Sum',
12036 aggregate_max: 'Agg: Max',
12037 aggregate_min: 'Agg: Min',
12038 aggregate_avg: 'Agg: Avg',
12039 aggregate_remove: 'Agg: Remove'
12048 angular.module('ui.grid').config(['$provide', function($provide) {
12049 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12050 $delegate.add('es', {
12055 description: 'Arrastre un encabezado de columna aquí y suéltelo para agrupar por esa columna.'
12058 placeholder: 'Buscar...',
12059 showingItems: 'Artículos Mostrados:',
12060 selectedItems: 'Artículos Seleccionados:',
12061 totalItems: 'Artículos Totales:',
12062 size: 'Tamaño de Página:',
12063 first: 'Primera Página',
12064 next: 'Página Siguiente',
12065 previous: 'Página Anterior',
12066 last: 'Última Página'
12069 text: 'Elegir columnas:'
12072 ascending: 'Orden Ascendente',
12073 descending: 'Orden Descendente',
12074 remove: 'Sin Ordenar'
12077 hide: 'Ocultar la columna'
12080 count: 'filas totales: ',
12087 pinLeft: 'Fijar a la Izquierda',
12088 pinRight: 'Fijar a la Derecha',
12089 unpin: 'Quitar Fijación'
12092 columns: 'Columnas:',
12093 importerTitle: 'Importar archivo',
12094 exporterAllAsCsv: 'Exportar todo como csv',
12095 exporterVisibleAsCsv: 'Exportar vista como csv',
12096 exporterSelectedAsCsv: 'Exportar selección como csv',
12097 exporterAllAsPdf: 'Exportar todo como pdf',
12098 exporterVisibleAsPdf: 'Exportar vista como pdf',
12099 exporterSelectedAsPdf: 'Exportar selección como pdf',
12100 clearAllFilters: 'Limpiar todos los filtros'
12103 noHeaders: 'No fue posible derivar los nombres de las columnas, ¿tiene encabezados el archivo?',
12104 noObjects: 'No fue posible obtener registros, ¿contiene datos el archivo, aparte de los encabezados?',
12105 invalidCsv: 'No fue posible procesar el archivo, ¿es un CSV válido?',
12106 invalidJson: 'No fue posible procesar el archivo, ¿es un Json válido?',
12107 jsonNotArray: 'El archivo json importado debe contener un array, abortando.'
12110 sizes: 'registros por página',
12111 totalItems: 'registros',
12116 ungroup: 'Desagrupar',
12117 aggregate_count: 'Agr: Cont',
12118 aggregate_sum: 'Agr: Sum',
12119 aggregate_max: 'Agr: Máx',
12120 aggregate_min: 'Agr: Min',
12121 aggregate_avg: 'Agr: Prom',
12122 aggregate_remove: 'Agr: Quitar'
12131 * Translated by: R. Salarmehr
12133 * Using Vajje.com online dictionary.
12136 angular.module('ui.grid').config(['$provide', function ($provide) {
12137 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12138 $delegate.add('fa', {
12143 description: 'عنوان یک ستون را بگیر و به گروهی از آن ستون رها کن.'
12146 placeholder: 'جستجو...',
12147 showingItems: 'نمایش اقلام:',
12148 selectedItems: 'قلم\u200cهای انتخاب شده:',
12149 totalItems: 'مجموع اقلام:',
12150 size: 'اندازه\u200cی صفحه:',
12151 first: 'اولین صفحه',
12152 next: 'صفحه\u200cی\u200cبعدی',
12153 previous: 'صفحه\u200cی\u200c قبلی',
12157 text: 'ستون\u200cهای انتخابی:'
12160 ascending: 'ترتیب صعودی',
12161 descending: 'ترتیب نزولی',
12162 remove: 'حذف مرتب کردن'
12165 hide: 'پنهان\u200cکردن ستون'
12175 pinLeft: 'پین کردن سمت چپ',
12176 pinRight: 'پین کردن سمت راست',
12180 columns: 'ستون\u200cها:',
12181 importerTitle: 'وارد کردن فایل',
12182 exporterAllAsCsv: 'خروجی تمام داده\u200cها در فایل csv',
12183 exporterVisibleAsCsv: 'خروجی داده\u200cهای قابل مشاهده در فایل csv',
12184 exporterSelectedAsCsv: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل csv',
12185 exporterAllAsPdf: 'خروجی تمام داده\u200cها در فایل pdf',
12186 exporterVisibleAsPdf: 'خروجی داده\u200cهای قابل مشاهده در فایل pdf',
12187 exporterSelectedAsPdf: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل pdf',
12188 clearAllFilters: 'پاک کردن تمام فیلتر'
12191 noHeaders: 'نام ستون قابل استخراج نیست. آیا فایل عنوان دارد؟',
12192 noObjects: 'اشیا قابل استخراج نیستند. آیا به جز عنوان\u200cها در فایل داده وجود دارد؟',
12193 invalidCsv: 'فایل قابل پردازش نیست. آیا فرمت csv معتبر است؟',
12194 invalidJson: 'فایل قابل پردازش نیست. آیا فرمت json معتبر است؟',
12195 jsonNotArray: 'فایل json وارد شده باید حاوی آرایه باشد. عملیات ساقط شد.'
12198 sizes: 'اقلام در هر صفحه',
12199 totalItems: 'اقلام',
12203 group: 'گروه\u200cبندی',
12204 ungroup: 'حذف گروه\u200cبندی',
12205 aggregate_count: 'Agg: تعداد',
12206 aggregate_sum: 'Agg: جمع',
12207 aggregate_max: 'Agg: بیشینه',
12208 aggregate_min: 'Agg: کمینه',
12209 aggregate_avg: 'Agg: میانگین',
12210 aggregate_remove: 'Agg: حذف'
12219 angular.module('ui.grid').config(['$provide', function($provide) {
12220 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12221 $delegate.add('fi', {
12226 description: 'Raahaa ja pudota otsikko tähän ryhmittääksesi sarakkeen mukaan.'
12229 placeholder: 'Hae...',
12230 showingItems: 'Näytetään rivejä:',
12231 selectedItems: 'Valitut rivit:',
12232 totalItems: 'Rivejä yht.:',
12234 first: 'Ensimmäinen sivu',
12235 next: 'Seuraava sivu',
12236 previous: 'Edellinen sivu',
12237 last: 'Viimeinen sivu'
12240 text: 'Valitse sarakkeet:'
12243 ascending: 'Järjestä nouseva',
12244 descending: 'Järjestä laskeva',
12245 remove: 'Poista järjestys'
12248 hide: 'Piilota sarake'
12251 count: 'Rivejä yht.: ',
12258 pinLeft: 'Lukitse vasemmalle',
12259 pinRight: 'Lukitse oikealle',
12260 unpin: 'Poista lukitus'
12263 columns: 'Sarakkeet:',
12264 importerTitle: 'Tuo tiedosto',
12265 exporterAllAsCsv: 'Vie tiedot csv-muodossa',
12266 exporterVisibleAsCsv: 'Vie näkyvä tieto csv-muodossa',
12267 exporterSelectedAsCsv: 'Vie valittu tieto csv-muodossa',
12268 exporterAllAsPdf: 'Vie tiedot pdf-muodossa',
12269 exporterVisibleAsPdf: 'Vie näkyvä tieto pdf-muodossa',
12270 exporterSelectedAsPdf: 'Vie valittu tieto pdf-muodossa',
12271 clearAllFilters: 'Puhdista kaikki suodattimet'
12274 noHeaders: 'Sarakkeen nimiä ei voitu päätellä, onko tiedostossa otsikkoriviä?',
12275 noObjects: 'Tietoja ei voitu lukea, onko tiedostossa muuta kuin otsikkot?',
12276 invalidCsv: 'Tiedostoa ei voitu käsitellä, oliko se CSV-muodossa?',
12277 invalidJson: 'Tiedostoa ei voitu käsitellä, oliko se JSON-muodossa?',
12278 jsonNotArray: 'Tiedosto ei sisältänyt taulukkoa, lopetetaan.'
12287 angular.module('ui.grid').config(['$provide', function($provide) {
12288 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12289 $delegate.add('fr', {
12294 description: 'Faites glisser une en-tête de colonne ici pour créer un groupe de colonnes.'
12297 placeholder: 'Recherche...',
12298 showingItems: 'Affichage des éléments :',
12299 selectedItems: 'Éléments sélectionnés :',
12300 totalItems: 'Nombre total d\'éléments:',
12301 size: 'Taille de page:',
12302 first: 'Première page',
12303 next: 'Page Suivante',
12304 previous: 'Page précédente',
12305 last: 'Dernière page'
12308 text: 'Choisir des colonnes :'
12311 ascending: 'Trier par ordre croissant',
12312 descending: 'Trier par ordre décroissant',
12313 remove: 'Enlever le tri'
12316 hide: 'Cacher la colonne'
12319 count: 'lignes totales: ',
12326 pinLeft: 'Épingler à gauche',
12327 pinRight: 'Épingler à droite',
12331 columns: 'Colonnes:',
12332 importerTitle: 'Importer un fichier',
12333 exporterAllAsCsv: 'Exporter toutes les données en CSV',
12334 exporterVisibleAsCsv: 'Exporter les données visibles en CSV',
12335 exporterSelectedAsCsv: 'Exporter les données sélectionnées en CSV',
12336 exporterAllAsPdf: 'Exporter toutes les données en PDF',
12337 exporterVisibleAsPdf: 'Exporter les données visibles en PDF',
12338 exporterSelectedAsPdf: 'Exporter les données sélectionnées en PDF',
12339 clearAllFilters: 'Nettoyez tous les filtres'
12342 noHeaders: 'Impossible de déterminer le nom des colonnes, le fichier possède-t-il une en-tête ?',
12343 noObjects: 'Aucun objet trouvé, le fichier possède-t-il des données autres que l\'en-tête ?',
12344 invalidCsv: 'Le fichier n\'a pas pu être traité, le CSV est-il valide ?',
12345 invalidJson: 'Le fichier n\'a pas pu être traité, le JSON est-il valide ?',
12346 jsonNotArray: 'Le fichier JSON importé doit contenir un tableau, abandon.'
12349 sizes: 'éléments par page',
12350 totalItems: 'éléments',
12355 ungroup: 'Dégrouper',
12356 aggregate_count: 'Agg: Compte',
12357 aggregate_sum: 'Agg: Somme',
12358 aggregate_max: 'Agg: Max',
12359 aggregate_min: 'Agg: Min',
12360 aggregate_avg: 'Agg: Moy',
12361 aggregate_remove: 'Agg: Retirer'
12370 angular.module('ui.grid').config(['$provide', function ($provide) {
12371 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12372 $delegate.add('he', {
12377 description: 'גרור עמודה לכאן ושחרר בכדי לקבץ עמודה זו.'
12380 placeholder: 'חפש...',
12381 showingItems: 'מציג:',
12382 selectedItems: 'סה"כ נבחרו:',
12383 totalItems: 'סה"כ רשומות:',
12384 size: 'תוצאות בדף:',
12387 previous: 'דף קודם',
12391 text: 'בחר עמודות:'
12394 ascending: 'סדר עולה',
12395 descending: 'סדר יורד',
12402 count: 'total rows: ',
12409 columns: 'Columns:',
12410 importerTitle: 'Import file',
12411 exporterAllAsCsv: 'Export all data as csv',
12412 exporterVisibleAsCsv: 'Export visible data as csv',
12413 exporterSelectedAsCsv: 'Export selected data as csv',
12414 exporterAllAsPdf: 'Export all data as pdf',
12415 exporterVisibleAsPdf: 'Export visible data as pdf',
12416 exporterSelectedAsPdf: 'Export selected data as pdf',
12417 clearAllFilters: 'Clean all filters'
12420 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12421 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12422 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12423 invalidJson: 'File was unable to be processed, is it valid Json?',
12424 jsonNotArray: 'Imported json file must contain an array, aborting.'
12433 angular.module('ui.grid').config(['$provide', function($provide) {
12434 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12435 $delegate.add('hy', {
12440 description: 'Ըստ սյան խմբավորելու համար քաշեք և գցեք վերնագիրն այստեղ։'
12443 placeholder: 'Փնտրում...',
12444 showingItems: 'Ցուցադրված տվյալներ՝',
12445 selectedItems: 'Ընտրված:',
12446 totalItems: 'Ընդամենը՝',
12447 size: 'Տողերի քանակը էջում՝',
12448 first: 'Առաջին էջ',
12450 previous: 'Նախորդ էջ',
12454 text: 'Ընտրել սյուները:'
12457 ascending: 'Աճման կարգով',
12458 descending: 'Նվազման կարգով',
12462 hide: 'Թաքցնել սյունը'
12465 count: 'ընդամենը տող՝ ',
12472 pinLeft: 'Կպցնել ձախ կողմում',
12473 pinRight: 'Կպցնել աջ կողմում',
12477 columns: 'Սյուներ:',
12478 importerTitle: 'Ներմուծել ֆայլ',
12479 exporterAllAsCsv: 'Արտահանել ամբողջը CSV',
12480 exporterVisibleAsCsv: 'Արտահանել երևացող տվյալները CSV',
12481 exporterSelectedAsCsv: 'Արտահանել ընտրված տվյալները CSV',
12482 exporterAllAsPdf: 'Արտահանել PDF',
12483 exporterVisibleAsPdf: 'Արտահանել երևացող տվյալները PDF',
12484 exporterSelectedAsPdf: 'Արտահանել ընտրված տվյալները PDF',
12485 clearAllFilters: 'Մաքրել բոլոր ֆիլտրերը'
12488 noHeaders: 'Հնարավոր չեղավ որոշել սյան վերնագրերը։ Արդյո՞ք ֆայլը ունի վերնագրեր։',
12489 noObjects: 'Հնարավոր չեղավ կարդալ տվյալները։ Արդյո՞ք ֆայլում կան տվյալներ։',
12490 invalidCsv: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր CSV է։',
12491 invalidJson: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր Json է։',
12492 jsonNotArray: 'Ներմուծված json ֆայլը պետք է պարունակի զանգված, կասեցվում է։'
12501 angular.module('ui.grid').config(['$provide', function($provide) {
12502 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12503 $delegate.add('it', {
12508 description: 'Trascina un\'intestazione all\'interno del gruppo della colonna.'
12511 placeholder: 'Ricerca...',
12512 showingItems: 'Mostra:',
12513 selectedItems: 'Selezionati:',
12514 totalItems: 'Totali:',
12515 size: 'Tot Pagine:',
12518 previous: 'Precedente',
12522 text: 'Scegli le colonne:'
12526 descending: 'Desc.',
12527 remove: 'Annulla ordinamento'
12533 count: 'righe totali: ',
12540 pinLeft: 'Blocca a sx',
12541 pinRight: 'Blocca a dx',
12542 unpin: 'Blocca in alto'
12545 columns: 'Colonne:',
12546 importerTitle: 'Importa',
12547 exporterAllAsCsv: 'Esporta tutti i dati in CSV',
12548 exporterVisibleAsCsv: 'Esporta i dati visibili in CSV',
12549 exporterSelectedAsCsv: 'Esporta i dati selezionati in CSV',
12550 exporterAllAsPdf: 'Esporta tutti i dati in PDF',
12551 exporterVisibleAsPdf: 'Esporta i dati visibili in PDF',
12552 exporterSelectedAsPdf: 'Esporta i dati selezionati in PDF',
12553 clearAllFilters: 'Pulire tutti i filtri'
12556 noHeaders: 'Impossibile reperire i nomi delle colonne, sicuro che siano indicati all\'interno del file?',
12557 noObjects: 'Impossibile reperire gli oggetti, sicuro che siano indicati all\'interno del file?',
12558 invalidCsv: 'Impossibile elaborare il file, sicuro che sia un CSV?',
12559 invalidJson: 'Impossibile elaborare il file, sicuro che sia un JSON valido?',
12560 jsonNotArray: 'Errore! Il file JSON da importare deve contenere un array.'
12563 group: 'Raggruppa',
12565 aggregate_count: 'Agg: N. Elem.',
12566 aggregate_sum: 'Agg: Somma',
12567 aggregate_max: 'Agg: Massimo',
12568 aggregate_min: 'Agg: Minimo',
12569 aggregate_avg: 'Agg: Media',
12570 aggregate_remove: 'Agg: Rimuovi'
12579 angular.module('ui.grid').config(['$provide', function($provide) {
12580 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12581 $delegate.add('ja', {
12586 description: 'ここに列ヘッダをドラッグアンドドロップして、その列でグループ化します。'
12589 placeholder: '検索...',
12590 showingItems: '表示中の項目:',
12591 selectedItems: '選択した項目:',
12592 totalItems: '項目の総数:',
12603 ascending: '昇順に並べ替え',
12604 descending: '降順に並べ替え',
12624 importerTitle: 'ファイルのインポート',
12625 exporterAllAsCsv: 'すべてのデータをCSV形式でエクスポート',
12626 exporterVisibleAsCsv: '表示中のデータをCSV形式でエクスポート',
12627 exporterSelectedAsCsv: '選択したデータをCSV形式でエクスポート',
12628 exporterAllAsPdf: 'すべてのデータをPDF形式でエクスポート',
12629 exporterVisibleAsPdf: '表示中のデータをPDF形式でエクスポート',
12630 exporterSelectedAsPdf: '選択したデータをPDF形式でエクスポート',
12631 clearAllFilters: 'すべてのフィルタを清掃してください'
12634 noHeaders: '列名を取得できません。ファイルにヘッダが含まれていることを確認してください。',
12635 noObjects: 'オブジェクトを取得できません。ファイルにヘッダ以外のデータが含まれていることを確認してください。',
12636 invalidCsv: 'ファイルを処理できません。ファイルが有効なCSV形式であることを確認してください。',
12637 invalidJson: 'ファイルを処理できません。ファイルが有効なJSON形式であることを確認してください。',
12638 jsonNotArray: 'インポートしたJSONファイルには配列が含まれている必要があります。処理を中止します。'
12651 angular.module('ui.grid').config(['$provide', function($provide) {
12652 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12653 $delegate.add('ko', {
12658 description: '컬럼으로 그룹핑하기 위해서는 컬럼 헤더를 끌어 떨어뜨려 주세요.'
12661 placeholder: '검색...',
12662 showingItems: '항목 보여주기:',
12663 selectedItems: '선택 항목:',
12664 totalItems: '전체 항목:',
12668 previous: '이전 페이지',
12675 ascending: '오름차순 정렬',
12676 descending: '내림차순 정렬',
12696 importerTitle: '파일 가져오기',
12697 exporterAllAsCsv: 'csv로 모든 데이터 내보내기',
12698 exporterVisibleAsCsv: 'csv로 보이는 데이터 내보내기',
12699 exporterSelectedAsCsv: 'csv로 선택된 데이터 내보내기',
12700 exporterAllAsPdf: 'pdf로 모든 데이터 내보내기',
12701 exporterVisibleAsPdf: 'pdf로 보이는 데이터 내보내기',
12702 exporterSelectedAsPdf: 'pdf로 선택 데이터 내보내기',
12703 clearAllFilters: '모든 필터를 청소'
12706 noHeaders: '컬럼명이 지정되어 있지 않습니다. 파일에 헤더가 명시되어 있는지 확인해 주세요.',
12707 noObjects: '데이터가 지정되어 있지 않습니다. 데이터가 파일에 있는지 확인해 주세요.',
12708 invalidCsv: '파일을 처리할 수 없습니다. 올바른 csv인지 확인해 주세요.',
12709 invalidJson: '파일을 처리할 수 없습니다. 올바른 json인지 확인해 주세요.',
12710 jsonNotArray: 'json 파일은 배열을 포함해야 합니다.'
12714 totalItems: '전체 항목'
12723 angular.module('ui.grid').config(['$provide', function($provide) {
12724 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12725 $delegate.add('nl', {
12730 description: 'Sleep hier een kolomnaam heen om op te groeperen.'
12733 placeholder: 'Zoeken...',
12734 showingItems: 'Getoonde items:',
12735 selectedItems: 'Geselecteerde items:',
12736 totalItems: 'Totaal aantal items:',
12737 size: 'Items per pagina:',
12738 first: 'Eerste pagina',
12739 next: 'Volgende pagina',
12740 previous: 'Vorige pagina',
12741 last: 'Laatste pagina'
12744 text: 'Kies kolommen:'
12747 ascending: 'Sorteer oplopend',
12748 descending: 'Sorteer aflopend',
12749 remove: 'Verwijder sortering'
12752 hide: 'Verberg kolom'
12755 count: 'Aantal rijen: ',
12757 avg: 'Gemiddelde: ',
12762 pinLeft: 'Zet links vast',
12763 pinRight: 'Zet rechts vast',
12767 columns: 'Kolommen:',
12768 importerTitle: 'Importeer bestand',
12769 exporterAllAsCsv: 'Exporteer alle data als csv',
12770 exporterVisibleAsCsv: 'Exporteer zichtbare data als csv',
12771 exporterSelectedAsCsv: 'Exporteer geselecteerde data als csv',
12772 exporterAllAsPdf: 'Exporteer alle data als pdf',
12773 exporterVisibleAsPdf: 'Exporteer zichtbare data als pdf',
12774 exporterSelectedAsPdf: 'Exporteer geselecteerde data als pdf',
12775 clearAllFilters: 'Reinig alle filters'
12778 noHeaders: 'Kolomnamen kunnen niet worden afgeleid. Heeft het bestand een header?',
12779 noObjects: 'Objecten kunnen niet worden afgeleid. Bevat het bestand data naast de headers?',
12780 invalidCsv: 'Het bestand kan niet verwerkt worden. Is het een valide csv bestand?',
12781 invalidJson: 'Het bestand kan niet verwerkt worden. Is het valide json?',
12782 jsonNotArray: 'Het json bestand moet een array bevatten. De actie wordt geannuleerd.'
12785 sizes: 'items per pagina',
12786 totalItems: 'items',
12791 ungroup: 'Groepering opheffen',
12792 aggregate_count: 'Agg: Aantal',
12793 aggregate_sum: 'Agg: Som',
12794 aggregate_max: 'Agg: Max',
12795 aggregate_min: 'Agg: Min',
12796 aggregate_avg: 'Agg: Gem',
12797 aggregate_remove: 'Agg: Verwijder'
12806 angular.module('ui.grid').config(['$provide', function($provide) {
12807 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12808 $delegate.add('pt-br', {
12813 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
12816 placeholder: 'Procurar...',
12817 showingItems: 'Mostrando os Itens:',
12818 selectedItems: 'Items Selecionados:',
12819 totalItems: 'Total de Itens:',
12820 size: 'Tamanho da Página:',
12821 first: 'Primeira Página',
12822 next: 'Próxima Página',
12823 previous: 'Página Anterior',
12824 last: 'Última Página'
12827 text: 'Selecione as colunas:'
12830 ascending: 'Ordenar Ascendente',
12831 descending: 'Ordenar Descendente',
12832 remove: 'Remover Ordenação'
12835 hide: 'Esconder coluna'
12838 count: 'total de linhas: ',
12845 pinLeft: 'Fixar Esquerda',
12846 pinRight: 'Fixar Direita',
12847 unpin: 'Desprender'
12850 columns: 'Colunas:',
12851 importerTitle: 'Importar arquivo',
12852 exporterAllAsCsv: 'Exportar todos os dados como csv',
12853 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
12854 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
12855 exporterAllAsPdf: 'Exportar todos os dados como pdf',
12856 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
12857 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
12858 clearAllFilters: 'Limpar todos os filtros'
12861 noHeaders: 'Nomes de colunas não puderam ser derivados. O arquivo tem um cabeçalho?',
12862 noObjects: 'Objetos não puderam ser derivados. Havia dados no arquivo, além dos cabeçalhos?',
12863 invalidCsv: 'Arquivo não pode ser processado. É um CSV válido?',
12864 invalidJson: 'Arquivo não pode ser processado. É um Json válido?',
12865 jsonNotArray: 'Arquivo json importado tem que conter um array. Abortando.'
12868 sizes: 'itens por página',
12869 totalItems: 'itens'
12873 ungroup: 'Desagrupar',
12874 aggregate_count: 'Agr: Contar',
12875 aggregate_sum: 'Agr: Soma',
12876 aggregate_max: 'Agr: Max',
12877 aggregate_min: 'Agr: Min',
12878 aggregate_avg: 'Agr: Med',
12879 aggregate_remove: 'Agr: Remover'
12888 angular.module('ui.grid').config(['$provide', function($provide) {
12889 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12890 $delegate.add('pt', {
12895 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
12898 placeholder: 'Procurar...',
12899 showingItems: 'Mostrando os Itens:',
12900 selectedItems: 'Itens Selecionados:',
12901 totalItems: 'Total de Itens:',
12902 size: 'Tamanho da Página:',
12903 first: 'Primeira Página',
12904 next: 'Próxima Página',
12905 previous: 'Página Anterior',
12906 last: 'Última Página'
12909 text: 'Selecione as colunas:'
12912 ascending: 'Ordenar Ascendente',
12913 descending: 'Ordenar Descendente',
12914 remove: 'Remover Ordenação'
12917 hide: 'Esconder coluna'
12920 count: 'total de linhas: ',
12927 pinLeft: 'Fixar Esquerda',
12928 pinRight: 'Fixar Direita',
12929 unpin: 'Desprender'
12932 columns: 'Colunas:',
12933 importerTitle: 'Importar ficheiro',
12934 exporterAllAsCsv: 'Exportar todos os dados como csv',
12935 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
12936 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
12937 exporterAllAsPdf: 'Exportar todos os dados como pdf',
12938 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
12939 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
12940 clearAllFilters: 'Limpar todos os filtros'
12943 noHeaders: 'Nomes de colunas não puderam ser derivados. O ficheiro tem um cabeçalho?',
12944 noObjects: 'Objetos não puderam ser derivados. Havia dados no ficheiro, além dos cabeçalhos?',
12945 invalidCsv: 'Ficheiro não pode ser processado. É um CSV válido?',
12946 invalidJson: 'Ficheiro não pode ser processado. É um Json válido?',
12947 jsonNotArray: 'Ficheiro json importado tem que conter um array. Interrompendo.'
12950 sizes: 'itens por página',
12951 totalItems: 'itens',
12956 ungroup: 'Desagrupar',
12957 aggregate_count: 'Agr: Contar',
12958 aggregate_sum: 'Agr: Soma',
12959 aggregate_max: 'Agr: Max',
12960 aggregate_min: 'Agr: Min',
12961 aggregate_avg: 'Agr: Med',
12962 aggregate_remove: 'Agr: Remover'
12971 angular.module('ui.grid').config(['$provide', function($provide) {
12972 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12973 $delegate.add('ru', {
12978 description: 'Для группировки по столбцу перетащите сюда его название.'
12981 placeholder: 'Поиск...',
12982 showingItems: 'Показать элементы:',
12983 selectedItems: 'Выбранные элементы:',
12984 totalItems: 'Всего элементов:',
12985 size: 'Размер страницы:',
12986 first: 'Первая страница',
12987 next: 'Следующая страница',
12988 previous: 'Предыдущая страница',
12989 last: 'Последняя страница'
12992 text: 'Выбрать столбцы:'
12995 ascending: 'По возрастанию',
12996 descending: 'По убыванию',
12997 remove: 'Убрать сортировку'
13000 hide: 'Спрятать столбец'
13003 count: 'всего строк: ',
13010 pinLeft: 'Закрепить слева',
13011 pinRight: 'Закрепить справа',
13015 columns: 'Столбцы:',
13016 importerTitle: 'Import file',
13017 exporterAllAsCsv: 'Экспортировать всё в CSV',
13018 exporterVisibleAsCsv: 'Экспортировать видимые данные в CSV',
13019 exporterSelectedAsCsv: 'Экспортировать выбранные данные в CSV',
13020 exporterAllAsPdf: 'Экспортировать всё в PDF',
13021 exporterVisibleAsPdf: 'Экспортировать видимые данные в PDF',
13022 exporterSelectedAsPdf: 'Экспортировать выбранные данные в PDF',
13023 clearAllFilters: 'Очистите все фильтры'
13026 noHeaders: 'Column names were unable to be derived, does the file have a header?',
13027 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
13028 invalidCsv: 'File was unable to be processed, is it valid CSV?',
13029 invalidJson: 'File was unable to be processed, is it valid Json?',
13030 jsonNotArray: 'Imported json file must contain an array, aborting.'
13039 angular.module('ui.grid').config(['$provide', function($provide) {
13040 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13041 $delegate.add('sk', {
13046 description: 'Pretiahni sem názov stĺpca pre zoskupenie podľa toho stĺpca.'
13049 placeholder: 'Hľadaj...',
13050 showingItems: 'Zobrazujem položky:',
13051 selectedItems: 'Vybraté položky:',
13052 totalItems: 'Počet položiek:',
13054 first: 'Prvá strana',
13055 next: 'Ďalšia strana',
13056 previous: 'Predchádzajúca strana',
13057 last: 'Posledná strana'
13060 text: 'Vyberte stĺpce:'
13063 ascending: 'Zotriediť vzostupne',
13064 descending: 'Zotriediť zostupne',
13065 remove: 'Vymazať triedenie'
13068 count: 'total rows: ',
13075 columns: 'Columns:',
13076 importerTitle: 'Import file',
13077 exporterAllAsCsv: 'Export all data as csv',
13078 exporterVisibleAsCsv: 'Export visible data as csv',
13079 exporterSelectedAsCsv: 'Export selected data as csv',
13080 exporterAllAsPdf: 'Export all data as pdf',
13081 exporterVisibleAsPdf: 'Export visible data as pdf',
13082 exporterSelectedAsPdf: 'Export selected data as pdf',
13083 clearAllFilters: 'Clear all filters'
13086 noHeaders: 'Column names were unable to be derived, does the file have a header?',
13087 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
13088 invalidCsv: 'File was unable to be processed, is it valid CSV?',
13089 invalidJson: 'File was unable to be processed, is it valid Json?',
13090 jsonNotArray: 'Imported json file must contain an array, aborting.'
13099 angular.module('ui.grid').config(['$provide', function($provide) {
13100 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13101 $delegate.add('sv', {
13106 description: 'Dra en kolumnrubrik hit och släpp den för gruppera efter den kolumnen.'
13109 placeholder: 'Sök...',
13110 showingItems: 'Visar artiklar:',
13111 selectedItems: 'Valda artiklar:',
13112 totalItems: 'Antal artiklar:',
13113 size: 'Sidstorlek:',
13114 first: 'Första sidan',
13115 next: 'Nästa sida',
13116 previous: 'Föregående sida',
13117 last: 'Sista sidan'
13120 text: 'Välj kolumner:'
13123 ascending: 'Sortera stigande',
13124 descending: 'Sortera fallande',
13125 remove: 'Inaktivera sortering'
13131 count: 'Antal rader: ',
13133 avg: 'Genomsnitt: ',
13138 pinLeft: 'Fäst vänster',
13139 pinRight: 'Fäst höger',
13143 columns: 'Kolumner:',
13144 importerTitle: 'Importera fil',
13145 exporterAllAsCsv: 'Exportera all data som CSV',
13146 exporterVisibleAsCsv: 'Exportera synlig data som CSV',
13147 exporterSelectedAsCsv: 'Exportera markerad data som CSV',
13148 exporterAllAsPdf: 'Exportera all data som PDF',
13149 exporterVisibleAsPdf: 'Exportera synlig data som PDF',
13150 exporterSelectedAsPdf: 'Exportera markerad data som PDF',
13151 clearAllFilters: 'Rengör alla filter'
13154 noHeaders: 'Kolumnnamn kunde inte härledas. Har filen ett sidhuvud?',
13155 noObjects: 'Objekt kunde inte härledas. Har filen data undantaget sidhuvud?',
13156 invalidCsv: 'Filen kunde inte behandlas, är den en giltig CSV?',
13157 invalidJson: 'Filen kunde inte behandlas, är den en giltig JSON?',
13158 jsonNotArray: 'Importerad JSON-fil måste innehålla ett fält. Import avbruten.'
13161 sizes: 'Artiklar per sida',
13162 totalItems: 'Artiklar'
13171 angular.module('ui.grid').config(['$provide', function($provide) {
13172 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13173 $delegate.add('ta', {
13175 label: 'உருப்படிகள்'
13178 description: 'ஒரு பத்தியை குழுவாக அமைக்க அப்பத்தியின் தலைப்பை இங்கே இழுத்து வரவும் '
13181 placeholder: 'தேடல் ...',
13182 showingItems: 'உருப்படிகளை காண்பித்தல்:',
13183 selectedItems: 'தேர்ந்தெடுக்கப்பட்ட உருப்படிகள்:',
13184 totalItems: 'மொத்த உருப்படிகள்:',
13185 size: 'பக்க அளவு: ',
13186 first: 'முதல் பக்கம்',
13187 next: 'அடுத்த பக்கம்',
13188 previous: 'முந்தைய பக்கம் ',
13189 last: 'இறுதி பக்கம்'
13192 text: 'பத்திகளை தேர்ந்தெடு:'
13195 ascending: 'மேலிருந்து கீழாக',
13196 descending: 'கீழிருந்து மேலாக',
13197 remove: 'வரிசையை நீக்கு'
13200 hide: 'பத்தியை மறைத்து வை '
13203 count: 'மொத்த வரிகள்:',
13206 min: 'குறைந்தபட்ச: ',
13210 pinLeft: 'இடதுபுறமாக தைக்க ',
13211 pinRight: 'வலதுபுறமாக தைக்க',
13215 columns: 'பத்திகள்:',
13216 importerTitle: 'கோப்பு : படித்தல்',
13217 exporterAllAsCsv: 'எல்லா தரவுகளையும் கோப்பாக்கு: csv',
13218 exporterVisibleAsCsv: 'இருக்கும் தரவுகளை கோப்பாக்கு: csv',
13219 exporterSelectedAsCsv: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: csv',
13220 exporterAllAsPdf: 'எல்லா தரவுகளையும் கோப்பாக்கு: pdf',
13221 exporterVisibleAsPdf: 'இருக்கும் தரவுகளை கோப்பாக்கு: pdf',
13222 exporterSelectedAsPdf: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: pdf',
13223 clearAllFilters: 'Clear all filters'
13226 noHeaders: 'பத்தியின் தலைப்புகளை பெற இயலவில்லை, கோப்பிற்கு தலைப்பு உள்ளதா?',
13227 noObjects: 'இலக்குகளை உருவாக்க முடியவில்லை, கோப்பில் தலைப்புகளை தவிர தரவு ஏதேனும் உள்ளதா? ',
13228 invalidCsv: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - csv',
13229 invalidJson: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - json',
13230 jsonNotArray: 'படித்த கோப்பில் வரிசைகள் உள்ளது, நடைமுறை ரத்து செய் : json'
13233 sizes : 'உருப்படிகள் / பக்கம்',
13234 totalItems : 'உருப்படிகள் '
13239 aggregate_count : 'மதிப்பீட்டு : எண்ணு',
13240 aggregate_sum : 'மதிப்பீட்டு : கூட்டல்',
13241 aggregate_max : 'மதிப்பீட்டு : அதிகபட்சம்',
13242 aggregate_min : 'மதிப்பீட்டு : குறைந்தபட்சம்',
13243 aggregate_avg : 'மதிப்பீட்டு : சராசரி',
13244 aggregate_remove : 'மதிப்பீட்டு : நீக்கு'
13254 * @name ui.grid.i18n
13258 * This module provides i18n functions to ui.grid and any application that wants to use it
13261 * <div doc-module-components="ui.grid.i18n"></div>
13265 var DIRECTIVE_ALIASES = ['uiT', 'uiTranslate'];
13266 var FILTER_ALIASES = ['t', 'uiTranslate'];
13268 var module = angular.module('ui.grid.i18n');
13273 * @name ui.grid.i18n.constant:i18nConstants
13275 * @description constants available in i18n module
13277 module.constant('i18nConstants', {
13278 MISSING: '[MISSING]',
13279 UPDATE_EVENT: '$uiI18n',
13281 LOCALE_DIRECTIVE_ALIAS: 'uiI18n',
13282 // default to english
13286 // module.config(['$provide', function($provide) {
13287 // $provide.decorator('i18nService', ['$delegate', function($delegate) {}])}]);
13291 * @name ui.grid.i18n.service:i18nService
13293 * @description Services for i18n
13295 module.service('i18nService', ['$log', 'i18nConstants', '$rootScope',
13296 function ($log, i18nConstants, $rootScope) {
13301 get: function (lang) {
13302 return this._langs[lang.toLowerCase()];
13304 add: function (lang, strings) {
13305 var lower = lang.toLowerCase();
13306 if (!this._langs[lower]) {
13307 this._langs[lower] = {};
13309 angular.extend(this._langs[lower], strings);
13311 getAllLangs: function () {
13313 if (!this._langs) {
13317 for (var key in this._langs) {
13323 setCurrent: function (lang) {
13324 this.current = lang.toLowerCase();
13326 getCurrentLang: function () {
13327 return this.current;
13336 * @methodOf ui.grid.i18n.service:i18nService
13337 * @description Adds the languages and strings to the cache. Decorate this service to
13338 * add more translation strings
13339 * @param {string} lang language to add
13340 * @param {object} stringMaps of strings to add grouped by property names
13343 * i18nService.add('en', {
13346 * label2: 'some more items'
13350 * description: 'Drag a column header here and drop it to group by that column.'
13355 add: function (langs, stringMaps) {
13356 if (typeof(langs) === 'object') {
13357 angular.forEach(langs, function (lang) {
13359 langCache.add(lang, stringMaps);
13363 langCache.add(langs, stringMaps);
13369 * @name getAllLangs
13370 * @methodOf ui.grid.i18n.service:i18nService
13371 * @description return all currently loaded languages
13372 * @returns {array} string
13374 getAllLangs: function () {
13375 return langCache.getAllLangs();
13381 * @methodOf ui.grid.i18n.service:i18nService
13382 * @description return all currently loaded languages
13383 * @param {string} lang to return. If not specified, returns current language
13384 * @returns {object} the translation string maps for the language
13386 get: function (lang) {
13387 var language = lang ? lang : service.getCurrentLang();
13388 return langCache.get(language);
13393 * @name getSafeText
13394 * @methodOf ui.grid.i18n.service:i18nService
13395 * @description returns the text specified in the path or a Missing text if text is not found
13396 * @param {string} path property path to use for retrieving text from string map
13397 * @param {string} lang to return. If not specified, returns current language
13398 * @returns {object} the translation for the path
13401 * i18nService.getSafeText('sort.ascending')
13404 getSafeText: function (path, lang) {
13405 var language = lang ? lang : service.getCurrentLang();
13406 var trans = langCache.get(language);
13409 return i18nConstants.MISSING;
13412 var paths = path.split('.');
13413 var current = trans;
13415 for (var i = 0; i < paths.length; ++i) {
13416 if (current[paths[i]] === undefined || current[paths[i]] === null) {
13417 return i18nConstants.MISSING;
13419 current = current[paths[i]];
13429 * @name setCurrentLang
13430 * @methodOf ui.grid.i18n.service:i18nService
13431 * @description sets the current language to use in the application
13432 * $broadcasts the Update_Event on the $rootScope
13433 * @param {string} lang to set
13436 * i18nService.setCurrentLang('fr');
13440 setCurrentLang: function (lang) {
13442 langCache.setCurrent(lang);
13443 $rootScope.$broadcast(i18nConstants.UPDATE_EVENT);
13449 * @name getCurrentLang
13450 * @methodOf ui.grid.i18n.service:i18nService
13451 * @description returns the current language used in the application
13453 getCurrentLang: function () {
13454 var lang = langCache.getCurrentLang();
13456 lang = i18nConstants.DEFAULT_LANG;
13457 langCache.setCurrent(lang);
13468 var localeDirective = function (i18nService, i18nConstants) {
13470 compile: function () {
13472 pre: function ($scope, $elm, $attrs) {
13473 var alias = i18nConstants.LOCALE_DIRECTIVE_ALIAS;
13474 // check for watchable property
13475 var lang = $scope.$eval($attrs[alias]);
13477 $scope.$watch($attrs[alias], function () {
13478 i18nService.setCurrentLang(lang);
13480 } else if ($attrs.$$observers) {
13481 $attrs.$observe(alias, function () {
13482 i18nService.setCurrentLang($attrs[alias] || i18nConstants.DEFAULT_LANG);
13491 module.directive('uiI18n', ['i18nService', 'i18nConstants', localeDirective]);
13493 // directive syntax
13494 var uitDirective = function ($parse, i18nService, i18nConstants) {
13497 compile: function () {
13499 pre: function ($scope, $elm, $attrs) {
13500 var alias1 = DIRECTIVE_ALIASES[0],
13501 alias2 = DIRECTIVE_ALIASES[1];
13502 var token = $attrs[alias1] || $attrs[alias2] || $elm.html();
13503 var missing = i18nConstants.MISSING + token;
13505 if ($attrs.$$observers) {
13506 var prop = $attrs[alias1] ? alias1 : alias2;
13507 observer = $attrs.$observe(prop, function (result) {
13509 $elm.html($parse(result)(i18nService.getCurrentLang()) || missing);
13513 var getter = $parse(token);
13514 var listener = $scope.$on(i18nConstants.UPDATE_EVENT, function (evt) {
13516 observer($attrs[alias1] || $attrs[alias2]);
13518 // set text based on i18n current language
13519 $elm.html(getter(i18nService.get()) || missing);
13522 $scope.$on('$destroy', listener);
13524 $elm.html(getter(i18nService.get()) || missing);
13531 angular.forEach( DIRECTIVE_ALIASES, function ( alias ) {
13532 module.directive( alias, ['$parse', 'i18nService', 'i18nConstants', uitDirective] );
13535 // optional filter syntax
13536 var uitFilter = function ($parse, i18nService, i18nConstants) {
13537 return function (data) {
13538 var getter = $parse(data);
13539 // set text based on i18n current language
13540 return getter(i18nService.get()) || i18nConstants.MISSING + data;
13544 angular.forEach( FILTER_ALIASES, function ( alias ) {
13545 module.filter( alias, ['$parse', 'i18nService', 'i18nConstants', uitFilter] );
13551 angular.module('ui.grid').config(['$provide', function($provide) {
13552 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13553 $delegate.add('zh-cn', {
13556 defaultFilterLabel: '列过滤器',
13557 removeFilter: '移除过滤器',
13558 columnMenuButtonLabel: '列菜单'
13561 filterLabel: "列过滤器: "
13567 description: '拖曳表头到此处进行分组'
13571 showingItems: '已显示行数:',
13572 selectedItems: '已选择行数:',
13573 totalItems: '总行数:',
13609 buttonLabel: '表格菜单'
13612 importerTitle: '导入文件',
13613 exporterAllAsCsv: '导出全部数据到CSV',
13614 exporterVisibleAsCsv: '导出可见数据到CSV',
13615 exporterSelectedAsCsv: '导出已选数据到CSV',
13616 exporterAllAsPdf: '导出全部数据到PDF',
13617 exporterVisibleAsPdf: '导出可见数据到PDF',
13618 exporterSelectedAsPdf: '导出已选数据到PDF',
13619 clearAllFilters: '清除所有过滤器'
13622 noHeaders: '无法获取列名,确定文件包含表头?',
13623 noObjects: '无法获取数据,确定文件包含数据?',
13624 invalidCsv: '无法处理文件,确定是合法的CSV文件?',
13625 invalidJson: '无法处理文件,确定是合法的JSON文件?',
13626 jsonNotArray: '导入的文件不是JSON数组!'
13630 pageToFirst: '第一页',
13632 pageSelected: '当前页',
13633 pageForward: '下一页',
13644 aggregate_count: '合计: 计数',
13645 aggregate_sum: '合计: 求和',
13646 aggregate_max: '合计: 最大',
13647 aggregate_min: '合计: 最小',
13648 aggregate_avg: '合计: 平均',
13649 aggregate_remove: '合计: 移除'
13658 angular.module('ui.grid').config(['$provide', function($provide) {
13659 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13660 $delegate.add('zh-tw', {
13665 description: '拖曳表頭到此處進行分組'
13669 showingItems: '已顯示行數:',
13670 selectedItems: '已選擇行數:',
13671 totalItems: '總行數:',
13703 importerTitle: '導入文件',
13704 exporterAllAsCsv: '導出全部數據到CSV',
13705 exporterVisibleAsCsv: '導出可見數據到CSV',
13706 exporterSelectedAsCsv: '導出已選數據到CSV',
13707 exporterAllAsPdf: '導出全部數據到PDF',
13708 exporterVisibleAsPdf: '導出可見數據到PDF',
13709 exporterSelectedAsPdf: '導出已選數據到PDF',
13710 clearAllFilters: '清除所有过滤器'
13713 noHeaders: '無法獲取列名,確定文件包含表頭?',
13714 noObjects: '無法獲取數據,確定文件包含數據?',
13715 invalidCsv: '無法處理文件,確定是合法的CSV文件?',
13716 invalidJson: '無法處理文件,確定是合法的JSON文件?',
13717 jsonNotArray: '導入的文件不是JSON數組!'
13733 * @name ui.grid.autoResize
13737 * #ui.grid.autoResize
13739 * <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>
13741 * This module provides auto-resizing functionality to UI-Grid.
13743 var module = angular.module('ui.grid.autoResize', ['ui.grid']);
13746 module.directive('uiGridAutoResize', ['$timeout', 'gridUtil', function ($timeout, gridUtil) {
13750 link: function ($scope, $elm, $attrs, uiGridCtrl) {
13751 var prevGridWidth, prevGridHeight;
13753 function getDimensions() {
13754 prevGridHeight = gridUtil.elementHeight($elm);
13755 prevGridWidth = gridUtil.elementWidth($elm);
13758 // Initialize the dimensions
13761 var resizeTimeoutId;
13762 function startTimeout() {
13763 clearTimeout(resizeTimeoutId);
13765 resizeTimeoutId = setTimeout(function () {
13766 var newGridHeight = gridUtil.elementHeight($elm);
13767 var newGridWidth = gridUtil.elementWidth($elm);
13769 if (newGridHeight !== prevGridHeight || newGridWidth !== prevGridWidth) {
13770 uiGridCtrl.grid.gridHeight = newGridHeight;
13771 uiGridCtrl.grid.gridWidth = newGridWidth;
13773 $scope.$apply(function () {
13774 uiGridCtrl.grid.refresh()
13775 .then(function () {
13790 $scope.$on('$destroy', function() {
13791 clearTimeout(resizeTimeoutId);
13803 * @name ui.grid.cellNav
13809 <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>
13811 This module provides auto-resizing functionality to UI-Grid.
13813 var module = angular.module('ui.grid.cellNav', ['ui.grid']);
13817 * @name ui.grid.cellNav.constant:uiGridCellNavConstants
13819 * @description constants available in cellNav
13821 module.constant('uiGridCellNavConstants', {
13822 FEATURE_NAME: 'gridCellNav',
13823 CELL_NAV_EVENT: 'cellNav',
13824 direction: {LEFT: 0, RIGHT: 1, UP: 2, DOWN: 3, PG_UP: 4, PG_DOWN: 5},
13833 module.factory('uiGridCellNavFactory', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', 'GridRowColumn', '$q',
13834 function (gridUtil, uiGridConstants, uiGridCellNavConstants, GridRowColumn, $q) {
13837 * @name ui.grid.cellNav.object:CellNav
13838 * @description returns a CellNav prototype function
13839 * @param {object} rowContainer container for rows
13840 * @param {object} colContainer parent column container
13841 * @param {object} leftColContainer column container to the left of parent
13842 * @param {object} rightColContainer column container to the right of parent
13844 var UiGridCellNav = function UiGridCellNav(rowContainer, colContainer, leftColContainer, rightColContainer) {
13845 this.rows = rowContainer.visibleRowCache;
13846 this.columns = colContainer.visibleColumnCache;
13847 this.leftColumns = leftColContainer ? leftColContainer.visibleColumnCache : [];
13848 this.rightColumns = rightColContainer ? rightColContainer.visibleColumnCache : [];
13849 this.bodyContainer = rowContainer;
13852 /** returns focusable columns of all containers */
13853 UiGridCellNav.prototype.getFocusableCols = function () {
13854 var allColumns = this.leftColumns.concat(this.columns, this.rightColumns);
13856 return allColumns.filter(function (col) {
13857 return col.colDef.allowCellFocus;
13863 * @name ui.grid.cellNav.api:GridRow
13865 * @description GridRow settings for cellNav feature, these are available to be
13866 * set only internally (for example, by other features)
13871 * @name allowCellFocus
13872 * @propertyOf ui.grid.cellNav.api:GridRow
13873 * @description Enable focus on a cell within this row. If set to false then no cells
13874 * in this row can be focused - group header rows as an example would set this to false.
13875 * <br/>Defaults to true
13877 /** returns focusable rows */
13878 UiGridCellNav.prototype.getFocusableRows = function () {
13879 return this.rows.filter(function(row) {
13880 return row.allowCellFocus !== false;
13884 UiGridCellNav.prototype.getNextRowCol = function (direction, curRow, curCol) {
13885 switch (direction) {
13886 case uiGridCellNavConstants.direction.LEFT:
13887 return this.getRowColLeft(curRow, curCol);
13888 case uiGridCellNavConstants.direction.RIGHT:
13889 return this.getRowColRight(curRow, curCol);
13890 case uiGridCellNavConstants.direction.UP:
13891 return this.getRowColUp(curRow, curCol);
13892 case uiGridCellNavConstants.direction.DOWN:
13893 return this.getRowColDown(curRow, curCol);
13894 case uiGridCellNavConstants.direction.PG_UP:
13895 return this.getRowColPageUp(curRow, curCol);
13896 case uiGridCellNavConstants.direction.PG_DOWN:
13897 return this.getRowColPageDown(curRow, curCol);
13902 UiGridCellNav.prototype.initializeSelection = function () {
13903 var focusableCols = this.getFocusableCols();
13904 var focusableRows = this.getFocusableRows();
13905 if (focusableCols.length === 0 || focusableRows.length === 0) {
13909 var curRowIndex = 0;
13910 var curColIndex = 0;
13911 return new GridRowColumn(focusableRows[0], focusableCols[0]); //return same row
13914 UiGridCellNav.prototype.getRowColLeft = function (curRow, curCol) {
13915 var focusableCols = this.getFocusableCols();
13916 var focusableRows = this.getFocusableRows();
13917 var curColIndex = focusableCols.indexOf(curCol);
13918 var curRowIndex = focusableRows.indexOf(curRow);
13920 //could not find column in focusable Columns so set it to 1
13921 if (curColIndex === -1) {
13925 var nextColIndex = curColIndex === 0 ? focusableCols.length - 1 : curColIndex - 1;
13927 //get column to left
13928 if (nextColIndex > curColIndex) {
13929 // On the first row
13930 // if (curRowIndex === 0 && curColIndex === 0) {
13933 if (curRowIndex === 0) {
13934 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
13937 //up one row and far right column
13938 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[nextColIndex]);
13942 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
13948 UiGridCellNav.prototype.getRowColRight = function (curRow, curCol) {
13949 var focusableCols = this.getFocusableCols();
13950 var focusableRows = this.getFocusableRows();
13951 var curColIndex = focusableCols.indexOf(curCol);
13952 var curRowIndex = focusableRows.indexOf(curRow);
13954 //could not find column in focusable Columns so set it to 0
13955 if (curColIndex === -1) {
13958 var nextColIndex = curColIndex === focusableCols.length - 1 ? 0 : curColIndex + 1;
13960 if (nextColIndex < curColIndex) {
13961 if (curRowIndex === focusableRows.length - 1) {
13962 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
13965 //down one row and far left column
13966 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[nextColIndex]);
13970 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
13974 UiGridCellNav.prototype.getRowColDown = function (curRow, curCol) {
13975 var focusableCols = this.getFocusableCols();
13976 var focusableRows = this.getFocusableRows();
13977 var curColIndex = focusableCols.indexOf(curCol);
13978 var curRowIndex = focusableRows.indexOf(curRow);
13980 //could not find column in focusable Columns so set it to 0
13981 if (curColIndex === -1) {
13985 if (curRowIndex === focusableRows.length - 1) {
13986 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
13990 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[curColIndex]);
13994 UiGridCellNav.prototype.getRowColPageDown = function (curRow, curCol) {
13995 var focusableCols = this.getFocusableCols();
13996 var focusableRows = this.getFocusableRows();
13997 var curColIndex = focusableCols.indexOf(curCol);
13998 var curRowIndex = focusableRows.indexOf(curRow);
14000 //could not find column in focusable Columns so set it to 0
14001 if (curColIndex === -1) {
14005 var pageSize = this.bodyContainer.minRowsToRender();
14006 if (curRowIndex >= focusableRows.length - pageSize) {
14007 return new GridRowColumn(focusableRows[focusableRows.length - 1], focusableCols[curColIndex]); //return last row
14011 return new GridRowColumn(focusableRows[curRowIndex + pageSize], focusableCols[curColIndex]);
14015 UiGridCellNav.prototype.getRowColUp = function (curRow, curCol) {
14016 var focusableCols = this.getFocusableCols();
14017 var focusableRows = this.getFocusableRows();
14018 var curColIndex = focusableCols.indexOf(curCol);
14019 var curRowIndex = focusableRows.indexOf(curRow);
14021 //could not find column in focusable Columns so set it to 0
14022 if (curColIndex === -1) {
14026 if (curRowIndex === 0) {
14027 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
14031 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[curColIndex]);
14035 UiGridCellNav.prototype.getRowColPageUp = function (curRow, curCol) {
14036 var focusableCols = this.getFocusableCols();
14037 var focusableRows = this.getFocusableRows();
14038 var curColIndex = focusableCols.indexOf(curCol);
14039 var curRowIndex = focusableRows.indexOf(curRow);
14041 //could not find column in focusable Columns so set it to 0
14042 if (curColIndex === -1) {
14046 var pageSize = this.bodyContainer.minRowsToRender();
14047 if (curRowIndex - pageSize < 0) {
14048 return new GridRowColumn(focusableRows[0], focusableCols[curColIndex]); //return first row
14052 return new GridRowColumn(focusableRows[curRowIndex - pageSize], focusableCols[curColIndex]);
14055 return UiGridCellNav;
14060 * @name ui.grid.cellNav.service:uiGridCellNavService
14062 * @description Services for cell navigation features. If you don't like the key maps we use,
14063 * or the direction cells navigation, override with a service decorator (see angular docs)
14065 module.service('uiGridCellNavService', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q', 'uiGridCellNavFactory', 'GridRowColumn', 'ScrollEvent',
14066 function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q, UiGridCellNav, GridRowColumn, ScrollEvent) {
14070 initializeGrid: function (grid) {
14071 grid.registerColumnBuilder(service.cellNavColumnBuilder);
14076 * @name ui.grid.cellNav:Grid.cellNav
14077 * @description cellNav properties added to grid class
14080 grid.cellNav.lastRowCol = null;
14081 grid.cellNav.focusedCells = [];
14083 service.defaultGridOptions(grid.options);
14087 * @name ui.grid.cellNav.api:PublicApi
14089 * @description Public Api for cellNav feature
14097 * @eventOf ui.grid.cellNav.api:PublicApi
14098 * @description raised when the active cell is changed
14100 * gridApi.cellNav.on.navigate(scope,function(newRowcol, oldRowCol){})
14102 * @param {object} newRowCol new position
14103 * @param {object} oldRowCol old position
14105 navigate: function (newRowCol, oldRowCol) {},
14108 * @name viewPortKeyDown
14109 * @eventOf ui.grid.cellNav.api:PublicApi
14110 * @description is raised when the viewPort receives a keyDown event. Cells never get focus in uiGrid
14111 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14112 * event whenever you need a keydown event on a cell
14114 * @param {object} event keydown event
14115 * @param {object} rowCol current rowCol position
14117 viewPortKeyDown: function (event, rowCol) {},
14121 * @name viewPortKeyPress
14122 * @eventOf ui.grid.cellNav.api:PublicApi
14123 * @description is raised when the viewPort receives a keyPress event. Cells never get focus in uiGrid
14124 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14125 * event whenever you need a keypress event on a cell
14127 * @param {object} event keypress event
14128 * @param {object} rowCol current rowCol position
14130 viewPortKeyPress: function (event, rowCol) {}
14137 * @name scrollToFocus
14138 * @methodOf ui.grid.cellNav.api:PublicApi
14139 * @description brings the specified row and column into view, and sets focus
14141 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus
14142 * @param {object} colDef to make visible and set focus
14143 * @returns {promise} a promise that is resolved after any scrolling is finished
14145 scrollToFocus: function (rowEntity, colDef) {
14146 return service.scrollToFocus(grid, rowEntity, colDef);
14151 * @name getFocusedCell
14152 * @methodOf ui.grid.cellNav.api:PublicApi
14153 * @description returns the current (or last if Grid does not have focus) focused row and column
14154 * <br> value is null if no selection has occurred
14156 getFocusedCell: function () {
14157 return grid.cellNav.lastRowCol;
14162 * @name getCurrentSelection
14163 * @methodOf ui.grid.cellNav.api:PublicApi
14164 * @description returns an array containing the current selection
14165 * <br> array is empty if no selection has occurred
14167 getCurrentSelection: function () {
14168 return grid.cellNav.focusedCells;
14173 * @name rowColSelectIndex
14174 * @methodOf ui.grid.cellNav.api:PublicApi
14175 * @description returns the index in the order in which the GridRowColumn was selected, returns -1 if the GridRowColumn
14177 * @param {object} rowCol the rowCol to evaluate
14179 rowColSelectIndex: function (rowCol) {
14180 //return gridUtil.arrayContainsObjectWithProperty(grid.cellNav.focusedCells, 'col.uid', rowCol.col.uid) &&
14182 for (var i = 0; i < grid.cellNav.focusedCells.length; i++) {
14183 if (grid.cellNav.focusedCells[i].col.uid === rowCol.col.uid &&
14184 grid.cellNav.focusedCells[i].row.uid === rowCol.row.uid) {
14195 grid.api.registerEventsFromObject(publicApi.events);
14197 grid.api.registerMethodsFromObject(publicApi.methods);
14201 defaultGridOptions: function (gridOptions) {
14204 * @name ui.grid.cellNav.api:GridOptions
14206 * @description GridOptions for cellNav feature, these are available to be
14207 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
14212 * @name modifierKeysToMultiSelectCells
14213 * @propertyOf ui.grid.cellNav.api:GridOptions
14214 * @description Enable multiple cell selection only when using the ctrlKey or shiftKey.
14215 * <br/>Defaults to false
14217 gridOptions.modifierKeysToMultiSelectCells = gridOptions.modifierKeysToMultiSelectCells === true;
14223 * @name decorateRenderContainers
14224 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14225 * @description decorates grid renderContainers with cellNav functions
14227 decorateRenderContainers: function (grid) {
14229 var rightContainer = grid.hasRightContainer() ? grid.renderContainers.right : null;
14230 var leftContainer = grid.hasLeftContainer() ? grid.renderContainers.left : null;
14232 if (leftContainer !== null) {
14233 grid.renderContainers.left.cellNav = new UiGridCellNav(grid.renderContainers.body, leftContainer, rightContainer, grid.renderContainers.body);
14235 if (rightContainer !== null) {
14236 grid.renderContainers.right.cellNav = new UiGridCellNav(grid.renderContainers.body, rightContainer, grid.renderContainers.body, leftContainer);
14239 grid.renderContainers.body.cellNav = new UiGridCellNav(grid.renderContainers.body, grid.renderContainers.body, leftContainer, rightContainer);
14244 * @name getDirection
14245 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14246 * @description determines which direction to for a given keyDown event
14247 * @returns {uiGridCellNavConstants.direction} direction
14249 getDirection: function (evt) {
14250 if (evt.keyCode === uiGridConstants.keymap.LEFT ||
14251 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey)) {
14252 return uiGridCellNavConstants.direction.LEFT;
14254 if (evt.keyCode === uiGridConstants.keymap.RIGHT ||
14255 evt.keyCode === uiGridConstants.keymap.TAB) {
14256 return uiGridCellNavConstants.direction.RIGHT;
14259 if (evt.keyCode === uiGridConstants.keymap.UP ||
14260 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ) {
14261 return uiGridCellNavConstants.direction.UP;
14264 if (evt.keyCode === uiGridConstants.keymap.PG_UP){
14265 return uiGridCellNavConstants.direction.PG_UP;
14268 if (evt.keyCode === uiGridConstants.keymap.DOWN ||
14269 evt.keyCode === uiGridConstants.keymap.ENTER && !(evt.ctrlKey || evt.altKey)) {
14270 return uiGridCellNavConstants.direction.DOWN;
14273 if (evt.keyCode === uiGridConstants.keymap.PG_DOWN){
14274 return uiGridCellNavConstants.direction.PG_DOWN;
14282 * @name cellNavColumnBuilder
14283 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14284 * @description columnBuilder function that adds cell navigation properties to grid column
14285 * @returns {promise} promise that will load any needed templates when resolved
14287 cellNavColumnBuilder: function (colDef, col, gridOptions) {
14292 * @name ui.grid.cellNav.api:ColumnDef
14294 * @description Column Definitions for cellNav feature, these are available to be
14295 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
14300 * @name allowCellFocus
14301 * @propertyOf ui.grid.cellNav.api:ColumnDef
14302 * @description Enable focus on a cell within this column.
14303 * <br/>Defaults to true
14305 colDef.allowCellFocus = colDef.allowCellFocus === undefined ? true : colDef.allowCellFocus;
14307 return $q.all(promises);
14312 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14313 * @name scrollToFocus
14314 * @description Scroll the grid such that the specified
14315 * row and column is in view, and set focus to the cell in that row and column
14316 * @param {Grid} grid the grid you'd like to act upon, usually available
14317 * from gridApi.grid
14318 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus to
14319 * @param {object} colDef to make visible and set focus to
14320 * @returns {promise} a promise that is resolved after any scrolling is finished
14322 scrollToFocus: function (grid, rowEntity, colDef) {
14323 var gridRow = null, gridCol = null;
14325 if (typeof(rowEntity) !== 'undefined' && rowEntity !== null) {
14326 gridRow = grid.getRow(rowEntity);
14329 if (typeof(colDef) !== 'undefined' && colDef !== null) {
14330 gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field);
14332 return grid.api.core.scrollToIfNecessary(gridRow, gridCol).then(function () {
14333 var rowCol = { row: gridRow, col: gridCol };
14335 // Broadcast the navigation
14336 if (gridRow !== null && gridCol !== null) {
14337 grid.cellNav.broadcastCellNav(rowCol);
14348 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14349 * @name getLeftWidth
14350 * @description Get the current drawn width of the columns in the
14351 * grid up to the numbered column, and add an apportionment for the
14352 * column that we're on. So if we are on column 0, we want to scroll
14353 * 0% (i.e. exclude this column from calc). If we're on the last column
14354 * we want to scroll to 100% (i.e. include this column in the calc). So
14355 * we include (thisColIndex / totalNumberCols) % of this column width
14356 * @param {Grid} grid the grid you'd like to act upon, usually available
14357 * from gridApi.grid
14358 * @param {gridCol} upToCol the column to total up to and including
14360 getLeftWidth: function (grid, upToCol) {
14367 var lastIndex = grid.renderContainers.body.visibleColumnCache.indexOf( upToCol );
14369 // total column widths up-to but not including the passed in column
14370 grid.renderContainers.body.visibleColumnCache.forEach( function( col, index ) {
14371 if ( index < lastIndex ){
14372 width += col.drawnWidth;
14376 // pro-rata the final column based on % of total columns.
14377 var percentage = lastIndex === 0 ? 0 : (lastIndex + 1) / grid.renderContainers.body.visibleColumnCache.length;
14378 width += upToCol.drawnWidth * percentage;
14389 * @name ui.grid.cellNav.directive:uiCellNav
14393 * @description Adds cell navigation features to the grid columns
14396 <example module="app">
14397 <file name="app.js">
14398 var app = angular.module('app', ['ui.grid', 'ui.grid.cellNav']);
14400 app.controller('MainCtrl', ['$scope', function ($scope) {
14402 { name: 'Bob', title: 'CEO' },
14403 { name: 'Frank', title: 'Lowly Developer' }
14406 $scope.columnDefs = [
14412 <file name="index.html">
14413 <div ng-controller="MainCtrl">
14414 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-cellnav></div>
14419 module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn', '$timeout', '$compile',
14420 function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, GridRowColumn, $timeout, $compile) {
14424 require: '^uiGrid',
14426 controller: function () {},
14427 compile: function () {
14429 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
14430 var _scope = $scope;
14432 var grid = uiGridCtrl.grid;
14433 uiGridCellNavService.initializeGrid(grid);
14435 uiGridCtrl.cellNav = {};
14437 //Ensure that the object has all of the methods we expect it to
14438 uiGridCtrl.cellNav.makeRowCol = function (obj) {
14439 if (!(obj instanceof GridRowColumn)) {
14440 obj = new GridRowColumn(obj.row, obj.col);
14445 uiGridCtrl.cellNav.getActiveCell = function () {
14446 var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus');
14447 if (elms.length > 0){
14454 uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown, originEvt) {
14455 modifierDown = !(modifierDown === undefined || !modifierDown);
14457 newRowCol = uiGridCtrl.cellNav.makeRowCol(newRowCol);
14459 uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown, originEvt);
14460 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, newRowCol, modifierDown, originEvt);
14463 uiGridCtrl.cellNav.clearFocus = grid.cellNav.clearFocus = function () {
14464 grid.cellNav.focusedCells = [];
14465 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT);
14468 uiGridCtrl.cellNav.broadcastFocus = function (rowCol, modifierDown, originEvt) {
14469 modifierDown = !(modifierDown === undefined || !modifierDown);
14471 rowCol = uiGridCtrl.cellNav.makeRowCol(rowCol);
14473 var row = rowCol.row,
14476 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14478 if (grid.cellNav.lastRowCol === null || rowColSelectIndex === -1) {
14479 var newRowCol = new GridRowColumn(row, col);
14481 grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol);
14482 grid.cellNav.lastRowCol = newRowCol;
14483 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) {
14484 grid.cellNav.focusedCells.push(rowCol);
14486 grid.cellNav.focusedCells = [rowCol];
14488 } else if (grid.options.modifierKeysToMultiSelectCells && modifierDown &&
14489 rowColSelectIndex >= 0) {
14491 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14495 uiGridCtrl.cellNav.handleKeyDown = function (evt) {
14496 var direction = uiGridCellNavService.getDirection(evt);
14497 if (direction === null) {
14501 var containerId = 'body';
14502 if (evt.uiGridTargetRenderContainerId) {
14503 containerId = evt.uiGridTargetRenderContainerId;
14506 // Get the last-focused row+col combo
14507 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14509 // Figure out which new row+combo we're navigating to
14510 var rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(direction, lastRowCol.row, lastRowCol.col);
14511 var focusableCols = uiGridCtrl.grid.renderContainers[containerId].cellNav.getFocusableCols();
14512 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14513 // Shift+tab on top-left cell should exit cellnav on render container
14516 direction === uiGridCellNavConstants.direction.LEFT &&
14517 // New col is last col (i.e. wrap around)
14518 rowCol.col === focusableCols[focusableCols.length - 1] &&
14519 // Staying on same row, which means we're at first row
14520 rowCol.row === lastRowCol.row &&
14521 evt.keyCode === uiGridConstants.keymap.TAB &&
14524 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14525 uiGridCtrl.cellNav.clearFocus();
14528 // Tab on bottom-right cell should exit cellnav on render container
14530 direction === uiGridCellNavConstants.direction.RIGHT &&
14531 // New col is first col (i.e. wrap around)
14532 rowCol.col === focusableCols[0] &&
14533 // Staying on same row, which means we're at first row
14534 rowCol.row === lastRowCol.row &&
14535 evt.keyCode === uiGridConstants.keymap.TAB &&
14538 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14539 uiGridCtrl.cellNav.clearFocus();
14543 // Scroll to the new cell, if it's not completely visible within the render container's viewport
14544 grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () {
14545 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
14549 evt.stopPropagation();
14550 evt.preventDefault();
14556 post: function ($scope, $elm, $attrs, uiGridCtrl) {
14557 var _scope = $scope;
14558 var grid = uiGridCtrl.grid;
14560 function addAriaLiveRegion(){
14561 // Thanks to google docs for the inspiration behind how to do this
14562 // XXX: Why is this entire mess nessasary?
14563 // Because browsers take a lot of coercing to get them to read out live regions
14564 //http://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
14565 var ariaNotifierDomElt = '<div ' +
14566 'id="' + grid.id +'-aria-speakable" ' +
14567 'class="ui-grid-a11y-ariascreenreader-speakable ui-grid-offscreen" ' +
14568 'aria-live="assertive" ' +
14570 'aria-atomic="true" ' +
14571 'aria-hidden="false" ' +
14572 'aria-relevant="additions" ' +
14577 var ariaNotifier = $compile(ariaNotifierDomElt)($scope);
14578 $elm.prepend(ariaNotifier);
14579 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown, originEvt) {
14581 * If the cell nav event was because of a focus event then we don't want to
14582 * change the notifier text.
14583 * Reasoning: Voice Over fires a focus events when moving arround the grid.
14584 * If the screen reader is handing the grid nav properly then we don't need to
14585 * use the alert to notify the user of the movement.
14586 * In all other cases we do want a notification event.
14588 if (originEvt && originEvt.type === 'focus'){return;}
14590 function setNotifyText(text){
14591 if (text === ariaNotifier.text()){return;}
14592 ariaNotifier[0].style.clip = 'rect(0px,0px,0px,0px)';
14594 * This is how google docs handles clearing the div. Seems to work better than setting the text of the div to ''
14596 ariaNotifier[0].innerHTML = "";
14597 ariaNotifier[0].style.visibility = 'hidden';
14598 ariaNotifier[0].style.visibility = 'visible';
14600 ariaNotifier[0].style.clip = 'auto';
14602 * The space after the text is something that google docs does.
14604 ariaNotifier[0].appendChild(document.createTextNode(text + " "));
14605 ariaNotifier[0].style.visibility = 'hidden';
14606 ariaNotifier[0].style.visibility = 'visible';
14611 var currentSelection = grid.api.cellNav.getCurrentSelection();
14612 for (var i = 0; i < currentSelection.length; i++) {
14613 values.push(currentSelection[i].getIntersectionValueFiltered());
14615 var cellText = values.toString();
14616 setNotifyText(cellText);
14620 addAriaLiveRegion();
14627 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', '$compile','uiGridCellNavConstants',
14628 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, $compile, uiGridCellNavConstants) {
14631 priority: -99999, //this needs to run very last
14632 require: ['^uiGrid', 'uiGridRenderContainer', '?^uiGridCellnav'],
14634 compile: function () {
14636 post: function ($scope, $elm, $attrs, controllers) {
14637 var uiGridCtrl = controllers[0],
14638 renderContainerCtrl = controllers[1],
14639 uiGridCellnavCtrl = controllers[2];
14641 // Skip attaching cell-nav specific logic if the directive is not attached above us
14642 if (!uiGridCtrl.grid.api.cellNav) { return; }
14644 var containerId = renderContainerCtrl.containerId;
14646 var grid = uiGridCtrl.grid;
14648 //run each time a render container is created
14649 uiGridCellNavService.decorateRenderContainers(grid);
14651 // focusser only created for body
14652 if (containerId !== 'body') {
14658 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells){
14659 $elm.attr('aria-multiselectable', true);
14661 $elm.attr('aria-multiselectable', false);
14664 //add an element with no dimensions that can be used to set focus and capture keystrokes
14665 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);
14666 $elm.append(focuser);
14668 focuser.on('focus', function (evt) {
14669 evt.uiGridTargetRenderContainerId = containerId;
14670 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14671 if (rowCol === null) {
14672 rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(uiGridCellNavConstants.direction.DOWN, null, null);
14673 if (rowCol.row && rowCol.col) {
14674 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
14679 uiGridCellnavCtrl.setAriaActivedescendant = function(id){
14680 $elm.attr('aria-activedescendant', id);
14683 uiGridCellnavCtrl.removeAriaActivedescendant = function(id){
14684 if ($elm.attr('aria-activedescendant') === id){
14685 $elm.attr('aria-activedescendant', '');
14690 uiGridCtrl.focus = function () {
14691 gridUtil.focus.byElement(focuser[0]);
14692 //allow for first time grid focus
14695 var viewPortKeyDownWasRaisedForRowCol = null;
14696 // Bind to keydown events in the render container
14697 focuser.on('keydown', function (evt) {
14698 evt.uiGridTargetRenderContainerId = containerId;
14699 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14700 var result = uiGridCtrl.cellNav.handleKeyDown(evt);
14701 if (result === null) {
14702 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyDown(evt, rowCol);
14703 viewPortKeyDownWasRaisedForRowCol = rowCol;
14706 //Bind to keypress events in the render container
14707 //keypress events are needed by edit function so the key press
14708 //that initiated an edit is not lost
14709 //must fire the event in a timeout so the editor can
14710 //initialize and subscribe to the event on another event loop
14711 focuser.on('keypress', function (evt) {
14712 if (viewPortKeyDownWasRaisedForRowCol) {
14713 $timeout(function () {
14714 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyPress(evt, viewPortKeyDownWasRaisedForRowCol);
14717 viewPortKeyDownWasRaisedForRowCol = null;
14721 $scope.$on('$destroy', function(){
14722 //Remove all event handlers associated with this focuser.
14732 module.directive('uiGridViewport', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log','$compile',
14733 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log, $compile) {
14736 priority: -99999, //this needs to run very last
14737 require: ['^uiGrid', '^uiGridRenderContainer', '?^uiGridCellnav'],
14739 compile: function () {
14741 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
14743 post: function ($scope, $elm, $attrs, controllers) {
14744 var uiGridCtrl = controllers[0],
14745 renderContainerCtrl = controllers[1];
14747 // Skip attaching cell-nav specific logic if the directive is not attached above us
14748 if (!uiGridCtrl.grid.api.cellNav) { return; }
14750 var containerId = renderContainerCtrl.containerId;
14751 //no need to process for other containers
14752 if (containerId !== 'body') {
14756 var grid = uiGridCtrl.grid;
14758 grid.api.core.on.scrollBegin($scope, function (args) {
14760 // Skip if there's no currently-focused cell
14761 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14762 if (lastRowCol === null) {
14766 //if not in my container, move on
14767 //todo: worry about horiz scroll
14768 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
14772 uiGridCtrl.cellNav.clearFocus();
14776 grid.api.core.on.scrollEnd($scope, function (args) {
14777 // Skip if there's no currently-focused cell
14778 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14779 if (lastRowCol === null) {
14783 //if not in my container, move on
14784 //todo: worry about horiz scroll
14785 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
14789 uiGridCtrl.cellNav.broadcastCellNav(lastRowCol);
14793 grid.api.cellNav.on.navigate($scope, function () {
14794 //focus again because it can be lost
14795 uiGridCtrl.focus();
14806 * @name ui.grid.cellNav.directive:uiGridCell
14809 * @description Stacks on top of ui.grid.uiGridCell to provide cell navigation
14811 module.directive('uiGridCell', ['$timeout', '$document', 'uiGridCellNavService', 'gridUtil', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn',
14812 function ($timeout, $document, uiGridCellNavService, gridUtil, uiGridCellNavConstants, uiGridConstants, GridRowColumn) {
14814 priority: -150, // run after default uiGridCell directive and ui.grid.edit uiGridCell
14816 require: ['^uiGrid', '?^uiGridCellnav'],
14818 link: function ($scope, $elm, $attrs, controllers) {
14819 var uiGridCtrl = controllers[0],
14820 uiGridCellnavCtrl = controllers[1];
14821 // Skip attaching cell-nav specific logic if the directive is not attached above us
14822 if (!uiGridCtrl.grid.api.cellNav) { return; }
14824 if (!$scope.col.colDef.allowCellFocus) {
14828 //Convinience local variables
14829 var grid = uiGridCtrl.grid;
14830 $scope.focused = false;
14832 // Make this cell focusable but only with javascript/a mouse click
14833 $elm.attr('tabindex', -1);
14835 // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused
14836 $elm.find('div').on('click', function (evt) {
14837 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), evt.ctrlKey || evt.metaKey, evt);
14839 evt.stopPropagation();
14845 * XXX Hack for screen readers.
14846 * This allows the grid to focus using only the screen reader cursor.
14847 * Since the focus event doesn't include key press information we can't use it
14848 * as our primary source of the event.
14850 $elm.on('mousedown', preventMouseDown);
14852 //turn on and off for edit events
14853 if (uiGridCtrl.grid.api.edit) {
14854 uiGridCtrl.grid.api.edit.on.beginCellEdit($scope, function () {
14855 $elm.off('mousedown', preventMouseDown);
14858 uiGridCtrl.grid.api.edit.on.afterCellEdit($scope, function () {
14859 $elm.on('mousedown', preventMouseDown);
14862 uiGridCtrl.grid.api.edit.on.cancelCellEdit($scope, function () {
14863 $elm.on('mousedown', preventMouseDown);
14867 function preventMouseDown(evt) {
14868 //Prevents the foucus event from firing if the click event is already going to fire.
14869 //If both events fire it will cause bouncing behavior.
14870 evt.preventDefault();
14873 //You can only focus on elements with a tabindex value
14874 $elm.on('focus', function (evt) {
14875 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), false, evt);
14876 evt.stopPropagation();
14880 // This event is fired for all cells. If the cell matches, then focus is set
14881 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown) {
14882 var isFocused = grid.cellNav.focusedCells.some(function(focusedRowCol, index){
14883 return (focusedRowCol.row === $scope.row && focusedRowCol.col === $scope.col);
14892 function setFocused() {
14893 if (!$scope.focused){
14894 var div = $elm.find('div');
14895 div.addClass('ui-grid-cell-focus');
14896 $elm.attr('aria-selected', true);
14897 uiGridCellnavCtrl.setAriaActivedescendant($elm.attr('id'));
14898 $scope.focused = true;
14902 function clearFocus() {
14903 if ($scope.focused){
14904 var div = $elm.find('div');
14905 div.removeClass('ui-grid-cell-focus');
14906 $elm.attr('aria-selected', false);
14907 uiGridCellnavCtrl.removeAriaActivedescendant($elm.attr('id'));
14908 $scope.focused = false;
14912 $scope.$on('$destroy', function () {
14913 //.off withouth paramaters removes all handlers
14914 $elm.find('div').off();
14928 * @name ui.grid.edit
14933 * <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>
14935 * This module provides cell editing capability to ui.grid. The goal was to emulate keying data in a spreadsheet via
14939 * To really get the full spreadsheet-like data entry, the ui.grid.cellNav module should be used. This will allow the
14940 * user to key data and then tab, arrow, or enter to the cells beside or below.
14942 * <div doc-module-components="ui.grid.edit"></div>
14945 var module = angular.module('ui.grid.edit', ['ui.grid']);
14949 * @name ui.grid.edit.constant:uiGridEditConstants
14951 * @description constants available in edit module
14953 module.constant('uiGridEditConstants', {
14954 EDITABLE_CELL_TEMPLATE: /EDITABLE_CELL_TEMPLATE/g,
14955 //must be lowercase because template bulder converts to lower
14956 EDITABLE_CELL_DIRECTIVE: /editable_cell_directive/g,
14958 BEGIN_CELL_EDIT: 'uiGridEventBeginCellEdit',
14959 END_CELL_EDIT: 'uiGridEventEndCellEdit',
14960 CANCEL_CELL_EDIT: 'uiGridEventCancelCellEdit'
14966 * @name ui.grid.edit.service:uiGridEditService
14968 * @description Services for editing features
14970 module.service('uiGridEditService', ['$q', 'uiGridConstants', 'gridUtil',
14971 function ($q, uiGridConstants, gridUtil) {
14975 initializeGrid: function (grid) {
14977 service.defaultGridOptions(grid.options);
14979 grid.registerColumnBuilder(service.editColumnBuilder);
14984 * @name ui.grid.edit.api:PublicApi
14986 * @description Public Api for edit feature
14993 * @name afterCellEdit
14994 * @eventOf ui.grid.edit.api:PublicApi
14995 * @description raised when cell editing is complete
14997 * gridApi.edit.on.afterCellEdit(scope,function(rowEntity, colDef){})
14999 * @param {object} rowEntity the options.data element that was edited
15000 * @param {object} colDef the column that was edited
15001 * @param {object} newValue new value
15002 * @param {object} oldValue old value
15004 afterCellEdit: function (rowEntity, colDef, newValue, oldValue) {
15008 * @name beginCellEdit
15009 * @eventOf ui.grid.edit.api:PublicApi
15010 * @description raised when cell editing starts on a cell
15012 * gridApi.edit.on.beginCellEdit(scope,function(rowEntity, colDef){})
15014 * @param {object} rowEntity the options.data element that was edited
15015 * @param {object} colDef the column that was edited
15016 * @param {object} triggerEvent the event that triggered the edit. Useful to prevent losing keystrokes on some
15019 beginCellEdit: function (rowEntity, colDef, triggerEvent) {
15023 * @name cancelCellEdit
15024 * @eventOf ui.grid.edit.api:PublicApi
15025 * @description raised when cell editing is cancelled on a cell
15027 * gridApi.edit.on.cancelCellEdit(scope,function(rowEntity, colDef){})
15029 * @param {object} rowEntity the options.data element that was edited
15030 * @param {object} colDef the column that was edited
15032 cancelCellEdit: function (rowEntity, colDef) {
15041 grid.api.registerEventsFromObject(publicApi.events);
15042 //grid.api.registerMethodsFromObject(publicApi.methods);
15046 defaultGridOptions: function (gridOptions) {
15050 * @name ui.grid.edit.api:GridOptions
15052 * @description Options for configuring the edit feature, these are available to be
15053 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
15058 * @name enableCellEdit
15059 * @propertyOf ui.grid.edit.api:GridOptions
15060 * @description If defined, sets the default value for the editable flag on each individual colDefs
15061 * if their individual enableCellEdit configuration is not defined. Defaults to undefined.
15066 * @name cellEditableCondition
15067 * @propertyOf ui.grid.edit.api:GridOptions
15068 * @description If specified, either a value or function to be used by all columns before editing.
15069 * If falsy, then editing of cell is not allowed.
15072 * function($scope){
15073 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15078 gridOptions.cellEditableCondition = gridOptions.cellEditableCondition === undefined ? true : gridOptions.cellEditableCondition;
15082 * @name editableCellTemplate
15083 * @propertyOf ui.grid.edit.api:GridOptions
15084 * @description If specified, cellTemplate to use as the editor for all columns.
15085 * <br/> defaults to 'ui-grid/cellTextEditor'
15090 * @name enableCellEditOnFocus
15091 * @propertyOf ui.grid.edit.api:GridOptions
15092 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15093 * <br/>_requires cellNav feature and the edit feature to be enabled_
15095 //enableCellEditOnFocus can only be used if cellnav module is used
15096 gridOptions.enableCellEditOnFocus = gridOptions.enableCellEditOnFocus === undefined ? false : gridOptions.enableCellEditOnFocus;
15101 * @name editColumnBuilder
15102 * @methodOf ui.grid.edit.service:uiGridEditService
15103 * @description columnBuilder function that adds edit properties to grid column
15104 * @returns {promise} promise that will load any needed templates when resolved
15106 editColumnBuilder: function (colDef, col, gridOptions) {
15112 * @name ui.grid.edit.api:ColumnDef
15114 * @description Column Definition for edit feature, these are available to be
15115 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
15120 * @name enableCellEdit
15121 * @propertyOf ui.grid.edit.api:ColumnDef
15122 * @description enable editing on column
15124 colDef.enableCellEdit = colDef.enableCellEdit === undefined ? (gridOptions.enableCellEdit === undefined ?
15125 (colDef.type !== 'object') : gridOptions.enableCellEdit) : colDef.enableCellEdit;
15129 * @name cellEditableCondition
15130 * @propertyOf ui.grid.edit.api:ColumnDef
15131 * @description If specified, either a value or function evaluated before editing cell. If falsy, then editing of cell is not allowed.
15134 * function($scope){
15135 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15140 colDef.cellEditableCondition = colDef.cellEditableCondition === undefined ? gridOptions.cellEditableCondition : colDef.cellEditableCondition;
15144 * @name editableCellTemplate
15145 * @propertyOf ui.grid.edit.api:ColumnDef
15146 * @description cell template to be used when editing this column. Can be Url or text template
15147 * <br/>Defaults to gridOptions.editableCellTemplate
15149 if (colDef.enableCellEdit) {
15150 colDef.editableCellTemplate = colDef.editableCellTemplate || gridOptions.editableCellTemplate || 'ui-grid/cellEditor';
15152 promises.push(gridUtil.getTemplate(colDef.editableCellTemplate)
15154 function (template) {
15155 col.editableCellTemplate = template;
15158 // Todo handle response error here?
15159 throw new Error("Couldn't fetch/use colDef.editableCellTemplate '" + colDef.editableCellTemplate + "'");
15165 * @name enableCellEditOnFocus
15166 * @propertyOf ui.grid.edit.api:ColumnDef
15167 * @requires ui.grid.cellNav
15168 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15169 * <br>_requires both the cellNav feature and the edit feature to be enabled_
15171 //enableCellEditOnFocus can only be used if cellnav module is used
15172 colDef.enableCellEditOnFocus = colDef.enableCellEditOnFocus === undefined ? gridOptions.enableCellEditOnFocus : colDef.enableCellEditOnFocus;
15177 * @name editModelField
15178 * @propertyOf ui.grid.edit.api:ColumnDef
15179 * @description a bindable string value that is used when binding to edit controls instead of colDef.field
15180 * <br/> example: You have a complex property on and object like state:{abbrev:'MS',name:'Mississippi'}. The
15181 * grid should display state.name in the cell and sort/filter based on the state.name property but the editor
15182 * requires the full state object.
15183 * <br/>colDef.field = 'state.name'
15184 * <br/>colDef.editModelField = 'state'
15186 //colDef.editModelField
15188 return $q.all(promises);
15193 * @name isStartEditKey
15194 * @methodOf ui.grid.edit.service:uiGridEditService
15195 * @description Determines if a keypress should start editing. Decorate this service to override with your
15196 * own key events. See service decorator in angular docs.
15197 * @param {Event} evt keydown event
15198 * @returns {boolean} true if an edit should start
15200 isStartEditKey: function (evt) {
15202 evt.keyCode === uiGridConstants.keymap.ESC ||
15203 evt.keyCode === uiGridConstants.keymap.SHIFT ||
15204 evt.keyCode === uiGridConstants.keymap.CTRL ||
15205 evt.keyCode === uiGridConstants.keymap.ALT ||
15206 evt.keyCode === uiGridConstants.keymap.WIN ||
15207 evt.keyCode === uiGridConstants.keymap.CAPSLOCK ||
15209 evt.keyCode === uiGridConstants.keymap.LEFT ||
15210 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey) ||
15212 evt.keyCode === uiGridConstants.keymap.RIGHT ||
15213 evt.keyCode === uiGridConstants.keymap.TAB ||
15215 evt.keyCode === uiGridConstants.keymap.UP ||
15216 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ||
15218 evt.keyCode === uiGridConstants.keymap.DOWN ||
15219 evt.keyCode === uiGridConstants.keymap.ENTER) {
15235 * @name ui.grid.edit.directive:uiGridEdit
15239 * @description Adds editing features to the ui-grid directive.
15242 <example module="app">
15243 <file name="app.js">
15244 var app = angular.module('app', ['ui.grid', 'ui.grid.edit']);
15246 app.controller('MainCtrl', ['$scope', function ($scope) {
15248 { name: 'Bob', title: 'CEO' },
15249 { name: 'Frank', title: 'Lowly Developer' }
15252 $scope.columnDefs = [
15253 {name: 'name', enableCellEdit: true},
15254 {name: 'title', enableCellEdit: true}
15258 <file name="index.html">
15259 <div ng-controller="MainCtrl">
15260 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit></div>
15265 module.directive('uiGridEdit', ['gridUtil', 'uiGridEditService', function (gridUtil, uiGridEditService) {
15269 require: '^uiGrid',
15271 compile: function () {
15273 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
15274 uiGridEditService.initializeGrid(uiGridCtrl.grid);
15276 post: function ($scope, $elm, $attrs, uiGridCtrl) {
15285 * @name ui.grid.edit.directive:uiGridRenderContainer
15289 * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits
15292 module.directive('uiGridViewport', [ 'uiGridEditConstants',
15293 function ( uiGridEditConstants) {
15296 priority: -99998, //run before cellNav
15297 require: ['^uiGrid', '^uiGridRenderContainer'],
15299 compile: function () {
15301 post: function ($scope, $elm, $attrs, controllers) {
15302 var uiGridCtrl = controllers[0];
15304 // Skip attaching if edit and cellNav is not enabled
15305 if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; }
15307 var containerId = controllers[1].containerId;
15308 //no need to process for other containers
15309 if (containerId !== 'body') {
15313 //refocus on the grid
15314 $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
15315 uiGridCtrl.focus();
15317 $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
15318 uiGridCtrl.focus();
15329 * @name ui.grid.edit.directive:uiGridCell
15333 * @description Stacks on top of ui.grid.uiGridCell to provide in-line editing capabilities to the cell
15336 * Binds edit start events to the uiGridCell element. When the events fire, the gridCell element is appended
15337 * with the columnDef.editableCellTemplate element ('cellEditor.html' by default).
15339 * The editableCellTemplate should respond to uiGridEditConstants.events.BEGIN\_CELL\_EDIT angular event
15340 * and do the initial steps needed to edit the cell (setfocus on input element, etc).
15342 * When the editableCellTemplate recognizes that the editing is ended (blur event, Enter key, etc.)
15343 * it should emit the uiGridEditConstants.events.END\_CELL\_EDIT event.
15345 * If editableCellTemplate recognizes that the editing has been cancelled (esc key)
15346 * it should emit the uiGridEditConstants.events.CANCEL\_CELL\_EDIT event. The original value
15347 * will be set back on the model by the uiGridCell directive.
15349 * Events that invoke editing:
15351 * - F2 keydown (when using cell selection)
15353 * Events that end editing:
15354 * - Dependent on the specific editableCellTemplate
15355 * - Standards should be blur and enter keydown
15357 * Events that cancel editing:
15358 * - Dependent on the specific editableCellTemplate
15359 * - Standards should be Esc keydown
15361 * Grid Events that end editing:
15362 * - uiGridConstants.events.GRID_SCROLL
15368 * @name ui.grid.edit.api:GridRow
15370 * @description GridRow options for edit feature, these are available to be
15371 * set internally only, by other features
15376 * @name enableCellEdit
15377 * @propertyOf ui.grid.edit.api:GridRow
15378 * @description enable editing on row, grouping for example might disable editing on group header rows
15381 module.directive('uiGridCell',
15382 ['$compile', '$injector', '$timeout', 'uiGridConstants', 'uiGridEditConstants', 'gridUtil', '$parse', 'uiGridEditService', '$rootScope',
15383 function ($compile, $injector, $timeout, uiGridConstants, uiGridEditConstants, gridUtil, $parse, uiGridEditService, $rootScope) {
15384 var touchstartTimeout = 500;
15385 if ($injector.has('uiGridCellNavService')) {
15386 var uiGridCellNavService = $injector.get('uiGridCellNavService');
15390 priority: -100, // run after default uiGridCell directive
15393 require: '?^uiGrid',
15394 link: function ($scope, $elm, $attrs, uiGridCtrl) {
15397 var inEdit = false;
15399 var cancelTouchstartTimeout;
15403 if (!$scope.col.colDef.enableCellEdit) {
15407 var cellNavNavigateDereg = function() {};
15408 var viewPortKeyDownDereg = function() {};
15411 var setEditable = function() {
15412 if ($scope.col.colDef.enableCellEdit && $scope.row.enableCellEdit !== false) {
15413 if (!$scope.beginEditEventsWired) { //prevent multiple attachments
15414 registerBeginEditEvents();
15417 if ($scope.beginEditEventsWired) {
15418 cancelBeginEditEvents();
15425 var rowWatchDereg = $scope.$watch('row', function (n, o) {
15432 $scope.$on( '$destroy', rowWatchDereg );
15434 function registerBeginEditEvents() {
15435 $elm.on('dblclick', beginEdit);
15437 // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit
15438 $elm.on('touchstart', touchStart);
15440 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15442 viewPortKeyDownDereg = uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
15443 if (rowCol === null) {
15447 if (rowCol.row === $scope.row && rowCol.col === $scope.col && !$scope.col.colDef.enableCellEditOnFocus) {
15448 //important to do this before scrollToIfNecessary
15449 beginEditKeyDown(evt);
15453 cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) {
15454 if ($scope.col.colDef.enableCellEditOnFocus) {
15455 // Don't begin edit if the cell hasn't changed
15456 if ((!oldRowCol || newRowCol.row !== oldRowCol.row || newRowCol.col !== oldRowCol.col) &&
15457 newRowCol.row === $scope.row && newRowCol.col === $scope.col) {
15458 $timeout(function () {
15466 $scope.beginEditEventsWired = true;
15470 function touchStart(event) {
15471 // jQuery masks events
15472 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
15473 event = event.originalEvent;
15476 // Bind touchend handler
15477 $elm.on('touchend', touchEnd);
15480 cancelTouchstartTimeout = $timeout(function() { }, touchstartTimeout);
15482 // Timeout's done! Start the edit
15483 cancelTouchstartTimeout.then(function () {
15484 // Use setTimeout to start the edit because beginEdit expects to be outside of $digest
15485 setTimeout(beginEdit, 0);
15487 // Undbind the touchend handler, we don't need it anymore
15488 $elm.off('touchend', touchEnd);
15492 // Cancel any touchstart timeout
15493 function touchEnd(event) {
15494 $timeout.cancel(cancelTouchstartTimeout);
15495 $elm.off('touchend', touchEnd);
15498 function cancelBeginEditEvents() {
15499 $elm.off('dblclick', beginEdit);
15500 $elm.off('keydown', beginEditKeyDown);
15501 $elm.off('touchstart', touchStart);
15502 cellNavNavigateDereg();
15503 viewPortKeyDownDereg();
15504 $scope.beginEditEventsWired = false;
15507 function beginEditKeyDown(evt) {
15508 if (uiGridEditService.isStartEditKey(evt)) {
15513 function shouldEdit(col, row) {
15514 return !row.isSaving &&
15515 ( angular.isFunction(col.colDef.cellEditableCondition) ?
15516 col.colDef.cellEditableCondition($scope) :
15517 col.colDef.cellEditableCondition );
15521 function beginEdit(triggerEvent) {
15522 //we need to scroll the cell into focus before invoking the editor
15523 $scope.grid.api.core.scrollToIfNecessary($scope.row, $scope.col)
15524 .then(function () {
15525 beginEditAfterScroll(triggerEvent);
15531 * @name editDropdownOptionsArray
15532 * @propertyOf ui.grid.edit.api:ColumnDef
15533 * @description an array of values in the format
15534 * [ {id: xxx, value: xxx} ], which is populated
15535 * into the edit dropdown
15540 * @name editDropdownIdLabel
15541 * @propertyOf ui.grid.edit.api:ColumnDef
15542 * @description the label for the "id" field
15543 * in the editDropdownOptionsArray. Defaults
15547 * $scope.gridOptions = {
15549 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15550 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
15551 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
15558 * @name editDropdownRowEntityOptionsArrayPath
15559 * @propertyOf ui.grid.edit.api:ColumnDef
15560 * @description a path to a property on row.entity containing an
15561 * array of values in the format
15562 * [ {id: xxx, value: xxx} ], which will be used to populate
15563 * the edit dropdown. This can be used when the dropdown values are dependent on
15564 * the backing row entity.
15565 * If this property is set then editDropdownOptionsArray will be ignored.
15568 * $scope.gridOptions = {
15570 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15571 * editDropdownRowEntityOptionsArrayPath: 'foo.bars[0].baz',
15572 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
15579 * @name editDropdownValueLabel
15580 * @propertyOf ui.grid.edit.api:ColumnDef
15581 * @description the label for the "value" field
15582 * in the editDropdownOptionsArray. Defaults
15586 * $scope.gridOptions = {
15588 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15589 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
15590 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
15597 * @name editDropdownFilter
15598 * @propertyOf ui.grid.edit.api:ColumnDef
15599 * @description A filter that you would like to apply to the values in the options list
15600 * of the dropdown. For example if you were using angular-translate you might set this
15604 * $scope.gridOptions = {
15606 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15607 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
15608 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status', editDropdownFilter: 'translate' }
15613 function beginEditAfterScroll(triggerEvent) {
15614 // If we are already editing, then just skip this so we don't try editing twice...
15619 if (!shouldEdit($scope.col, $scope.row)) {
15624 cellModel = $parse($scope.row.getQualifiedColField($scope.col));
15625 //get original value from the cell
15626 origCellValue = cellModel($scope);
15628 html = $scope.col.editableCellTemplate;
15630 if ($scope.col.colDef.editModelField) {
15631 html = html.replace(uiGridConstants.MODEL_COL_FIELD, gridUtil.preEval('row.entity.' + $scope.col.colDef.editModelField));
15634 html = html.replace(uiGridConstants.MODEL_COL_FIELD, $scope.row.getQualifiedColField($scope.col));
15637 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
15639 var optionFilter = $scope.col.colDef.editDropdownFilter ? '|' + $scope.col.colDef.editDropdownFilter : '';
15640 html = html.replace(uiGridConstants.CUSTOM_FILTERS, optionFilter);
15642 var inputType = 'text';
15643 switch ($scope.col.colDef.type){
15645 inputType = 'checkbox';
15648 inputType = 'number';
15651 inputType = 'date';
15654 html = html.replace('INPUT_TYPE', inputType);
15656 var editDropdownRowEntityOptionsArrayPath = $scope.col.colDef.editDropdownRowEntityOptionsArrayPath;
15657 if (editDropdownRowEntityOptionsArrayPath) {
15658 $scope.editDropdownOptionsArray = resolveObjectFromPath($scope.row.entity, editDropdownRowEntityOptionsArrayPath);
15661 $scope.editDropdownOptionsArray = $scope.col.colDef.editDropdownOptionsArray;
15663 $scope.editDropdownIdLabel = $scope.col.colDef.editDropdownIdLabel ? $scope.col.colDef.editDropdownIdLabel : 'id';
15664 $scope.editDropdownValueLabel = $scope.col.colDef.editDropdownValueLabel ? $scope.col.colDef.editDropdownValueLabel : 'value';
15667 var createEditor = function(){
15669 cancelBeginEditEvents();
15670 var cellElement = angular.element(html);
15671 $elm.append(cellElement);
15672 editCellScope = $scope.$new();
15673 $compile(cellElement)(editCellScope);
15674 var gridCellContentsEl = angular.element($elm.children()[0]);
15675 gridCellContentsEl.addClass('ui-grid-cell-contents-hidden');
15677 if (!$rootScope.$$phase) {
15678 $scope.$apply(createEditor);
15683 //stop editing when grid is scrolled
15684 var deregOnGridScroll = $scope.col.grid.api.core.on.scrollBegin($scope, function () {
15685 if ($scope.grid.disableScrolling) {
15689 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
15690 deregOnGridScroll();
15691 deregOnEndCellEdit();
15692 deregOnCancelCellEdit();
15696 var deregOnEndCellEdit = $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
15698 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
15699 deregOnEndCellEdit();
15700 deregOnGridScroll();
15701 deregOnCancelCellEdit();
15705 var deregOnCancelCellEdit = $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
15707 deregOnCancelCellEdit();
15708 deregOnGridScroll();
15709 deregOnEndCellEdit();
15712 $scope.$broadcast(uiGridEditConstants.events.BEGIN_CELL_EDIT, triggerEvent);
15713 $timeout(function () {
15714 //execute in a timeout to give any complex editor templates a cycle to completely render
15715 $scope.grid.api.edit.raise.beginCellEdit($scope.row.entity, $scope.col.colDef, triggerEvent);
15719 function endEdit() {
15720 $scope.grid.disableScrolling = false;
15725 //sometimes the events can't keep up with the keyboard and grid focus is lost, so always focus
15726 //back to grid here. The focus call needs to be before the $destroy and removal of the control,
15727 //otherwise ng-model-options of UpdateOn: 'blur' will not work.
15728 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15729 uiGridCtrl.focus();
15732 var gridCellContentsEl = angular.element($elm.children()[0]);
15733 //remove edit element
15734 editCellScope.$destroy();
15735 angular.element($elm.children()[1]).remove();
15736 gridCellContentsEl.removeClass('ui-grid-cell-contents-hidden');
15738 registerBeginEditEvents();
15739 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.EDIT );
15742 function cancelEdit() {
15743 $scope.grid.disableScrolling = false;
15747 cellModel.assign($scope, origCellValue);
15750 $scope.grid.api.edit.raise.cancelCellEdit($scope.row.entity, $scope.col.colDef);
15754 // resolves a string path against the given object
15755 // shamelessly borrowed from
15756 // http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
15757 function resolveObjectFromPath(object, path) {
15758 path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
15759 path = path.replace(/^\./, ''); // strip a leading dot
15760 var a = path.split('.');
15764 object = object[n];
15778 * @name ui.grid.edit.directive:uiGridEditor
15782 * @description input editor directive for editable fields.
15783 * Provides EndEdit and CancelEdit events
15785 * Events that end editing:
15786 * blur and enter keydown
15788 * Events that cancel editing:
15792 module.directive('uiGridEditor',
15793 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', 'uiGridEditService',
15794 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout, uiGridEditService) {
15797 require: ['?^uiGrid', '?^uiGridRenderContainer', 'ngModel'],
15798 compile: function () {
15800 pre: function ($scope, $elm, $attrs) {
15803 post: function ($scope, $elm, $attrs, controllers) {
15804 var uiGridCtrl, renderContainerCtrl, ngModel;
15805 if (controllers[0]) { uiGridCtrl = controllers[0]; }
15806 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
15807 if (controllers[2]) { ngModel = controllers[2]; }
15809 //set focus at start of edit
15810 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function (evt,triggerEvent) {
15811 $timeout(function () {
15813 //only select text if it is not being replaced below in the cellNav viewPortKeyPress
15814 if ($scope.col.colDef.enableCellEditOnFocus || !(uiGridCtrl && uiGridCtrl.grid.api.cellNav)) {
15818 //some browsers (Chrome) stupidly, imo, support the w3 standard that number, email, ...
15819 //fields should not allow setSelectionRange. We ignore the error for those browsers
15820 //https://www.w3.org/Bugs/Public/show_bug.cgi?id=24796
15822 $elm[0].setSelectionRange($elm[0].value.length, $elm[0].value.length);
15830 //set the keystroke that started the edit event
15831 //we must do this because the BeginEdit is done in a different event loop than the intitial
15833 //fire this event for the keypress that is received
15834 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15835 var viewPortKeyDownUnregister = uiGridCtrl.grid.api.cellNav.on.viewPortKeyPress($scope, function (evt, rowCol) {
15836 if (uiGridEditService.isStartEditKey(evt)) {
15837 ngModel.$setViewValue(String.fromCharCode(evt.keyCode), evt);
15840 viewPortKeyDownUnregister();
15844 $elm.on('blur', function (evt) {
15845 $scope.stopEdit(evt);
15850 $scope.deepEdit = false;
15852 $scope.stopEdit = function (evt) {
15853 if ($scope.inputForm && !$scope.inputForm.$valid) {
15854 evt.stopPropagation();
15855 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
15858 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
15860 $scope.deepEdit = false;
15864 $elm.on('click', function (evt) {
15865 if ($elm[0].type !== 'checkbox') {
15866 $scope.deepEdit = true;
15867 $timeout(function () {
15868 $scope.grid.disableScrolling = true;
15873 $elm.on('keydown', function (evt) {
15874 switch (evt.keyCode) {
15875 case uiGridConstants.keymap.ESC:
15876 evt.stopPropagation();
15877 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
15881 if ($scope.deepEdit &&
15882 (evt.keyCode === uiGridConstants.keymap.LEFT ||
15883 evt.keyCode === uiGridConstants.keymap.RIGHT ||
15884 evt.keyCode === uiGridConstants.keymap.UP ||
15885 evt.keyCode === uiGridConstants.keymap.DOWN)) {
15886 evt.stopPropagation();
15888 // Pass the keydown event off to the cellNav service, if it exists
15889 else if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15890 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
15891 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
15892 $scope.stopEdit(evt);
15896 //handle enter and tab for editing not using cellNav
15897 switch (evt.keyCode) {
15898 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
15899 case uiGridConstants.keymap.TAB:
15900 evt.stopPropagation();
15901 evt.preventDefault();
15902 $scope.stopEdit(evt);
15917 * @name ui.grid.edit.directive:input
15921 * @description directive to provide binding between input[date] value and ng-model for angular 1.2
15922 * It is similar to input[date] directive of angular 1.3
15924 * Supported date format for input is 'yyyy-MM-dd'
15925 * The directive will set the $valid property of input element and the enclosing form to false if
15926 * model is invalid date or value of input is entered wrong.
15929 module.directive('uiGridEditor', ['$filter', function ($filter) {
15930 function parseDateString(dateString) {
15931 if (typeof(dateString) === 'undefined' || dateString === '') {
15934 var parts = dateString.split('-');
15935 if (parts.length !== 3) {
15938 var year = parseInt(parts[0], 10);
15939 var month = parseInt(parts[1], 10);
15940 var day = parseInt(parts[2], 10);
15942 if (month < 1 || year < 1 || day < 1) {
15945 return new Date(year, (month - 1), day);
15948 priority: -100, // run after default uiGridEditor directive
15949 require: '?ngModel',
15950 link: function (scope, element, attrs, ngModel) {
15952 if (angular.version.minor === 2 && attrs.type && attrs.type === 'date' && ngModel) {
15954 ngModel.$formatters.push(function (modelValue) {
15955 ngModel.$setValidity(null,(!modelValue || !isNaN(modelValue.getTime())));
15956 return $filter('date')(modelValue, 'yyyy-MM-dd');
15959 ngModel.$parsers.push(function (viewValue) {
15960 if (viewValue && viewValue.length > 0) {
15961 var dateValue = parseDateString(viewValue);
15962 ngModel.$setValidity(null, (dateValue && !isNaN(dateValue.getTime())));
15966 ngModel.$setValidity(null, true);
15978 * @name ui.grid.edit.directive:uiGridEditDropdown
15982 * @description dropdown editor for editable fields.
15983 * Provides EndEdit and CancelEdit events
15985 * Events that end editing:
15986 * blur and enter keydown, and any left/right nav
15988 * Events that cancel editing:
15992 module.directive('uiGridEditDropdown',
15993 ['uiGridConstants', 'uiGridEditConstants',
15994 function (uiGridConstants, uiGridEditConstants) {
15996 require: ['?^uiGrid', '?^uiGridRenderContainer'],
15998 compile: function () {
16000 pre: function ($scope, $elm, $attrs) {
16003 post: function ($scope, $elm, $attrs, controllers) {
16004 var uiGridCtrl = controllers[0];
16005 var renderContainerCtrl = controllers[1];
16007 //set focus at start of edit
16008 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16010 $elm[0].style.width = ($elm[0].parentElement.offsetWidth - 1) + 'px';
16011 $elm.on('blur', function (evt) {
16012 $scope.stopEdit(evt);
16017 $scope.stopEdit = function (evt) {
16018 // no need to validate a dropdown - invalid values shouldn't be
16019 // available in the list
16020 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16023 $elm.on('keydown', function (evt) {
16024 switch (evt.keyCode) {
16025 case uiGridConstants.keymap.ESC:
16026 evt.stopPropagation();
16027 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16030 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16031 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
16032 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
16033 $scope.stopEdit(evt);
16037 //handle enter and tab for editing not using cellNav
16038 switch (evt.keyCode) {
16039 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
16040 case uiGridConstants.keymap.TAB:
16041 evt.stopPropagation();
16042 evt.preventDefault();
16043 $scope.stopEdit(evt);
16057 * @name ui.grid.edit.directive:uiGridEditFileChooser
16061 * @description input editor directive for editable fields.
16062 * Provides EndEdit and CancelEdit events
16064 * Events that end editing:
16065 * blur and enter keydown
16067 * Events that cancel editing:
16071 module.directive('uiGridEditFileChooser',
16072 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout',
16073 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) {
16076 require: ['?^uiGrid', '?^uiGridRenderContainer'],
16077 compile: function () {
16079 pre: function ($scope, $elm, $attrs) {
16082 post: function ($scope, $elm, $attrs, controllers) {
16083 var uiGridCtrl, renderContainerCtrl;
16084 if (controllers[0]) { uiGridCtrl = controllers[0]; }
16085 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
16086 var grid = uiGridCtrl.grid;
16088 var handleFileSelect = function( event ){
16089 var target = event.srcElement || event.target;
16091 if (target && target.files && target.files.length > 0) {
16094 * @name editFileChooserCallback
16095 * @propertyOf ui.grid.edit.api:ColumnDef
16096 * @description A function that should be called when any files have been chosen
16097 * by the user. You should use this to process the files appropriately for your
16100 * It passes the gridCol, the gridRow (from which you can get gridRow.entity),
16101 * and the files. The files are in the format as returned from the file chooser,
16102 * an array of files, with each having useful information such as:
16103 * - `files[0].lastModifiedDate`
16104 * - `files[0].name`
16105 * - `files[0].size` (appears to be in bytes)
16106 * - `files[0].type` (MIME type by the looks)
16108 * Typically you would do something with these files - most commonly you would
16109 * use the filename or read the file itself in. The example function does both.
16113 * editFileChooserCallBack: function(gridRow, gridCol, files ){
16114 * // ignore all but the first file, it can only choose one anyway
16115 * // set the filename into this column
16116 * gridRow.entity.filename = file[0].name;
16118 * // read the file and set it into a hidden column, which we may do stuff with later
16119 * var setFile = function(fileContent){
16120 * gridRow.entity.file = fileContent.currentTarget.result;
16122 * var reader = new FileReader();
16123 * reader.onload = setFile;
16124 * reader.readAsText( files[0] );
16128 if ( typeof($scope.col.colDef.editFileChooserCallback) === 'function' ) {
16129 $scope.col.colDef.editFileChooserCallback($scope.row, $scope.col, target.files);
16131 gridUtil.logError('You need to set colDef.editFileChooserCallback to use the file chooser');
16134 target.form.reset();
16135 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16137 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16141 $elm[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
16143 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16147 $elm.on('blur', function (evt) {
16148 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16165 * @name ui.grid.expandable
16168 * # ui.grid.expandable
16170 * <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>
16172 * This module provides the ability to create subgrids with the ability to expand a row
16173 * to show the subgrid.
16175 * <div doc-module-components="ui.grid.expandable"></div>
16177 var module = angular.module('ui.grid.expandable', ['ui.grid']);
16181 * @name ui.grid.expandable.service:uiGridExpandableService
16183 * @description Services for the expandable grid
16185 module.service('uiGridExpandableService', ['gridUtil', '$compile', function (gridUtil, $compile) {
16187 initializeGrid: function (grid) {
16189 grid.expandable = {};
16190 grid.expandable.expandedAll = false;
16194 * @name enableExpandable
16195 * @propertyOf ui.grid.expandable.api:GridOptions
16196 * @description Whether or not to use expandable feature, allows you to turn off expandable on specific grids
16197 * within your application, or in specific modes on _this_ grid. Defaults to true.
16200 * $scope.gridOptions = {
16201 * enableExpandable: false
16205 grid.options.enableExpandable = grid.options.enableExpandable !== false;
16209 * @name expandableRowHeight
16210 * @propertyOf ui.grid.expandable.api:GridOptions
16211 * @description Height in pixels of the expanded subgrid. Defaults to
16215 * $scope.gridOptions = {
16216 * expandableRowHeight: 150
16220 grid.options.expandableRowHeight = grid.options.expandableRowHeight || 150;
16225 * @propertyOf ui.grid.expandable.api:GridOptions
16226 * @description Width in pixels of the expandable column. Defaults to 40
16229 * $scope.gridOptions = {
16230 * expandableRowHeaderWidth: 40
16234 grid.options.expandableRowHeaderWidth = grid.options.expandableRowHeaderWidth || 40;
16238 * @name expandableRowTemplate
16239 * @propertyOf ui.grid.expandable.api:GridOptions
16240 * @description Mandatory. The template for your expanded row
16243 * $scope.gridOptions = {
16244 * expandableRowTemplate: 'expandableRowTemplate.html'
16248 if ( grid.options.enableExpandable && !grid.options.expandableRowTemplate ){
16249 gridUtil.logError( 'You have not set the expandableRowTemplate, disabling expandable module' );
16250 grid.options.enableExpandable = false;
16255 * @name ui.grid.expandable.api:PublicApi
16257 * @description Public Api for expandable feature
16261 * @name ui.grid.expandable.api:GridOptions
16263 * @description Options for configuring the expandable feature, these are available to be
16264 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
16271 * @name rowExpandedStateChanged
16272 * @eventOf ui.grid.expandable.api:PublicApi
16273 * @description raised when cell editing is complete
16275 * gridApi.expandable.on.rowExpandedStateChanged(scope,function(row){})
16277 * @param {GridRow} row the row that was expanded
16279 rowExpandedBeforeStateChanged: function(scope,row){
16281 rowExpandedStateChanged: function (scope, row) {
16290 * @name toggleRowExpansion
16291 * @methodOf ui.grid.expandable.api:PublicApi
16292 * @description Toggle a specific row
16294 * gridApi.expandable.toggleRowExpansion(rowEntity);
16296 * @param {object} rowEntity the data entity for the row you want to expand
16298 toggleRowExpansion: function (rowEntity) {
16299 var row = grid.getRow(rowEntity);
16300 if (row !== null) {
16301 service.toggleRowExpansion(grid, row);
16307 * @name expandAllRows
16308 * @methodOf ui.grid.expandable.api:PublicApi
16309 * @description Expand all subgrids.
16311 * gridApi.expandable.expandAllRows();
16314 expandAllRows: function() {
16315 service.expandAllRows(grid);
16320 * @name collapseAllRows
16321 * @methodOf ui.grid.expandable.api:PublicApi
16322 * @description Collapse all subgrids.
16324 * gridApi.expandable.collapseAllRows();
16327 collapseAllRows: function() {
16328 service.collapseAllRows(grid);
16333 * @name toggleAllRows
16334 * @methodOf ui.grid.expandable.api:PublicApi
16335 * @description Toggle all subgrids.
16337 * gridApi.expandable.toggleAllRows();
16340 toggleAllRows: function() {
16341 service.toggleAllRows(grid);
16346 grid.api.registerEventsFromObject(publicApi.events);
16347 grid.api.registerMethodsFromObject(publicApi.methods);
16350 toggleRowExpansion: function (grid, row) {
16351 // trigger the "before change" event. Can change row height dynamically this way.
16352 grid.api.expandable.raise.rowExpandedBeforeStateChanged(row);
16353 row.isExpanded = !row.isExpanded;
16354 if (angular.isUndefined(row.expandedRowHeight)){
16355 row.expandedRowHeight = grid.options.expandableRowHeight;
16358 if (row.isExpanded) {
16359 row.height = row.grid.options.rowHeight + row.expandedRowHeight;
16362 row.height = row.grid.options.rowHeight;
16363 grid.expandable.expandedAll = false;
16365 grid.api.expandable.raise.rowExpandedStateChanged(row);
16368 expandAllRows: function(grid, $scope) {
16369 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16370 if (!row.isExpanded) {
16371 service.toggleRowExpansion(grid, row);
16374 grid.expandable.expandedAll = true;
16375 grid.queueGridRefresh();
16378 collapseAllRows: function(grid) {
16379 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16380 if (row.isExpanded) {
16381 service.toggleRowExpansion(grid, row);
16384 grid.expandable.expandedAll = false;
16385 grid.queueGridRefresh();
16388 toggleAllRows: function(grid) {
16389 if (grid.expandable.expandedAll) {
16390 service.collapseAllRows(grid);
16393 service.expandAllRows(grid);
16402 * @name enableExpandableRowHeader
16403 * @propertyOf ui.grid.expandable.api:GridOptions
16404 * @description Show a rowHeader to provide the expandable buttons. If set to false then implies
16405 * you're going to use a custom method for expanding and collapsing the subgrids. Defaults to true.
16408 * $scope.gridOptions = {
16409 * enableExpandableRowHeader: false
16413 module.directive('uiGridExpandable', ['uiGridExpandableService', '$templateCache',
16414 function (uiGridExpandableService, $templateCache) {
16418 require: '^uiGrid',
16420 compile: function () {
16422 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16423 if ( uiGridCtrl.grid.options.enableExpandableRowHeader !== false ) {
16424 var expandableRowHeaderColDef = {
16425 name: 'expandableButtons',
16427 exporterSuppressExport: true,
16428 enableColumnResizing: false,
16429 enableColumnMenu: false,
16430 width: uiGridCtrl.grid.options.expandableRowHeaderWidth || 40
16432 expandableRowHeaderColDef.cellTemplate = $templateCache.get('ui-grid/expandableRowHeader');
16433 expandableRowHeaderColDef.headerCellTemplate = $templateCache.get('ui-grid/expandableTopRowHeader');
16434 uiGridCtrl.grid.addRowHeaderColumn(expandableRowHeaderColDef);
16436 uiGridExpandableService.initializeGrid(uiGridCtrl.grid);
16438 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16447 * @name ui.grid.expandable.directive:uiGrid
16448 * @description stacks on the uiGrid directive to register child grid with parent row when child is created
16450 module.directive('uiGrid', ['uiGridExpandableService', '$templateCache',
16451 function (uiGridExpandableService, $templateCache) {
16455 require: '^uiGrid',
16457 compile: function () {
16459 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16461 uiGridCtrl.grid.api.core.on.renderingComplete($scope, function() {
16462 //if a parent grid row is on the scope, then add the parentRow property to this childGrid
16463 if ($scope.row && $scope.row.grid && $scope.row.grid.options && $scope.row.grid.options.enableExpandable) {
16467 * @name ui.grid.expandable.class:Grid
16468 * @description Additional Grid properties added by expandable module
16474 * @propertyOf ui.grid.expandable.class:Grid
16475 * @description reference to the expanded parent row that owns this grid
16477 uiGridCtrl.grid.parentRow = $scope.row;
16479 //todo: adjust height on parent row when child grid height changes. we need some sort of gridHeightChanged event
16480 // uiGridCtrl.grid.core.on.canvasHeightChanged($scope, function(oldHeight, newHeight) {
16481 // uiGridCtrl.grid.parentRow = newHeight;
16487 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16497 * @name ui.grid.expandable.directive:uiGridExpandableRow
16498 * @description directive to render the expandable row template
16500 module.directive('uiGridExpandableRow',
16501 ['uiGridExpandableService', '$timeout', '$compile', 'uiGridConstants','gridUtil','$interval', '$log',
16502 function (uiGridExpandableService, $timeout, $compile, uiGridConstants, gridUtil, $interval, $log) {
16509 compile: function () {
16511 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16512 gridUtil.getTemplate($scope.grid.options.expandableRowTemplate).then(
16513 function (template) {
16514 if ($scope.grid.options.expandableRowScope) {
16515 var expandableRowScope = $scope.grid.options.expandableRowScope;
16516 for (var property in expandableRowScope) {
16517 if (expandableRowScope.hasOwnProperty(property)) {
16518 $scope[property] = expandableRowScope[property];
16522 var expandedRowElement = $compile(template)($scope);
16523 $elm.append(expandedRowElement);
16524 $scope.row.expandedRendered = true;
16528 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16529 $scope.$on('$destroy', function() {
16530 $scope.row.expandedRendered = false;
16540 * @name ui.grid.expandable.directive:uiGridRow
16541 * @description stacks on the uiGridRow directive to add support for expandable rows
16543 module.directive('uiGridRow',
16544 ['$compile', 'gridUtil', '$templateCache',
16545 function ($compile, gridUtil, $templateCache) {
16549 compile: function ($elm, $attrs) {
16551 pre: function ($scope, $elm, $attrs, controllers) {
16553 $scope.expandableRow = {};
16555 $scope.expandableRow.shouldRenderExpand = function () {
16556 var ret = $scope.colContainer.name === 'body' && $scope.grid.options.enableExpandable !== false && $scope.row.isExpanded && (!$scope.grid.isScrollingVertically || $scope.row.expandedRendered);
16560 $scope.expandableRow.shouldRenderFiller = function () {
16561 var ret = $scope.row.isExpanded && ( $scope.colContainer.name !== 'body' || ($scope.grid.isScrollingVertically && !$scope.row.expandedRendered));
16566 * Commented out @PaulL1. This has no purpose that I can see, and causes #2964. If this code needs to be reinstated for some
16567 * reason it needs to use drawnWidth, not width, and needs to check column visibility. It should really use render container
16568 * visible column cache also instead of checking column.renderContainer.
16569 function updateRowContainerWidth() {
16570 var grid = $scope.grid;
16572 grid.columns.forEach( function (column) {
16573 if (column.renderContainer === 'left') {
16574 colWidth += column.width;
16577 colWidth = Math.floor(colWidth);
16578 return '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.colContainer.name + ', .grid' + grid.id +
16579 ' .ui-grid-pinned-container-' + $scope.colContainer.name + ' .ui-grid-render-container-' + $scope.colContainer.name +
16580 ' .ui-grid-viewport .ui-grid-canvas .ui-grid-row { width: ' + colWidth + 'px; }';
16583 if ($scope.colContainer.name === 'left') {
16584 $scope.grid.registerStyleComputation({
16586 func: updateRowContainerWidth
16591 post: function ($scope, $elm, $attrs, controllers) {
16600 * @name ui.grid.expandable.directive:uiGridViewport
16601 * @description stacks on the uiGridViewport directive to append the expandable row html elements to the
16602 * default gridRow template
16604 module.directive('uiGridViewport',
16605 ['$compile', 'gridUtil', '$templateCache',
16606 function ($compile, gridUtil, $templateCache) {
16610 compile: function ($elm, $attrs) {
16611 var rowRepeatDiv = angular.element($elm.children().children()[0]);
16612 var expandedRowFillerElement = $templateCache.get('ui-grid/expandableScrollFiller');
16613 var expandedRowElement = $templateCache.get('ui-grid/expandableRow');
16614 rowRepeatDiv.append(expandedRowElement);
16615 rowRepeatDiv.append(expandedRowFillerElement);
16617 pre: function ($scope, $elm, $attrs, controllers) {
16619 post: function ($scope, $elm, $attrs, controllers) {
16628 /* global console */
16635 * @name ui.grid.exporter
16638 * # ui.grid.exporter
16640 * <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>
16642 * This module provides the ability to exporter data from the grid.
16644 * Data can be exported in a range of formats, and all data, visible
16645 * data, or selected rows can be exported, with all columns or visible
16648 * No UI is provided, the caller should provide their own UI/buttons
16649 * as appropriate, or enable the gridMenu
16654 * <div doc-module-components="ui.grid.exporter"></div>
16657 var module = angular.module('ui.grid.exporter', ['ui.grid']);
16661 * @name ui.grid.exporter.constant:uiGridExporterConstants
16663 * @description constants available in exporter module
16667 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
16669 * @description export all data, including data not visible. Can
16670 * be set for either rowTypes or colTypes
16674 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
16676 * @description export only visible data, including data not visible. Can
16677 * be set for either rowTypes or colTypes
16681 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
16683 * @description export all data, including data not visible. Can
16684 * be set only for rowTypes, selection of only some columns is
16687 module.constant('uiGridExporterConstants', {
16688 featureName: 'exporter',
16690 VISIBLE: 'visible',
16691 SELECTED: 'selected',
16692 CSV_CONTENT: 'CSV_CONTENT',
16693 BUTTON_LABEL: 'BUTTON_LABEL',
16694 FILE_NAME: 'FILE_NAME'
16699 * @name ui.grid.exporter.service:uiGridExporterService
16701 * @description Services for exporter feature
16703 module.service('uiGridExporterService', ['$q', 'uiGridExporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService',
16704 function ($q, uiGridExporterConstants, gridUtil, $compile, $interval, i18nService) {
16710 initializeGrid: function (grid) {
16712 //add feature namespace and any properties to grid for needed state
16713 grid.exporter = {};
16714 this.defaultGridOptions(grid.options);
16718 * @name ui.grid.exporter.api:PublicApi
16720 * @description Public Api for exporter feature
16732 * @methodOf ui.grid.exporter.api:PublicApi
16733 * @description Exports rows from the grid in csv format,
16734 * the data exported is selected based on the provided options
16735 * @param {string} rowTypes which rows to export, valid values are
16736 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
16737 * uiGridExporterConstants.SELECTED
16738 * @param {string} colTypes which columns to export, valid values are
16739 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
16741 csvExport: function (rowTypes, colTypes) {
16742 service.csvExport(grid, rowTypes, colTypes);
16747 * @methodOf ui.grid.exporter.api:PublicApi
16748 * @description Exports rows from the grid in pdf format,
16749 * the data exported is selected based on the provided options
16750 * Note that this function has a dependency on pdfMake, all
16751 * going well this has been installed for you.
16752 * The resulting pdf opens in a new browser window.
16753 * @param {string} rowTypes which rows to export, valid values are
16754 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
16755 * uiGridExporterConstants.SELECTED
16756 * @param {string} colTypes which columns to export, valid values are
16757 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
16759 pdfExport: function (rowTypes, colTypes) {
16760 service.pdfExport(grid, rowTypes, colTypes);
16766 grid.api.registerEventsFromObject(publicApi.events);
16768 grid.api.registerMethodsFromObject(publicApi.methods);
16770 if (grid.api.core.addToGridMenu){
16771 service.addToMenu( grid );
16773 // order of registration is not guaranteed, register in a little while
16774 $interval( function() {
16775 if (grid.api.core.addToGridMenu){
16776 service.addToMenu( grid );
16783 defaultGridOptions: function (gridOptions) {
16784 //default option to true unless it was explicitly set to false
16787 * @name ui.grid.exporter.api:GridOptions
16789 * @description GridOptions for exporter feature, these are available to be
16790 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
16794 * @name ui.grid.exporter.api:ColumnDef
16795 * @description ColumnDef settings for exporter
16799 * @name exporterSuppressMenu
16800 * @propertyOf ui.grid.exporter.api:GridOptions
16801 * @description Don't show the export menu button, implying the user
16802 * will roll their own UI for calling the exporter
16803 * <br/>Defaults to false
16805 gridOptions.exporterSuppressMenu = gridOptions.exporterSuppressMenu === true;
16808 * @name exporterMenuLabel
16809 * @propertyOf ui.grid.exporter.api:GridOptions
16810 * @description The text to show on the exporter menu button
16812 * <br/>Defaults to 'Export'
16814 gridOptions.exporterMenuLabel = gridOptions.exporterMenuLabel ? gridOptions.exporterMenuLabel : 'Export';
16817 * @name exporterSuppressColumns
16818 * @propertyOf ui.grid.exporter.api:GridOptions
16819 * @description Columns that should not be exported. The selectionRowHeader is already automatically
16820 * suppressed, but if you had a button column or some other "system" column that shouldn't be shown in the
16821 * output then add it in this list. You should provide an array of column names.
16822 * <br/>Defaults to: []
16824 * gridOptions.exporterSuppressColumns = [ 'buttons' ];
16827 gridOptions.exporterSuppressColumns = gridOptions.exporterSuppressColumns ? gridOptions.exporterSuppressColumns : [];
16830 * @name exporterCsvColumnSeparator
16831 * @propertyOf ui.grid.exporter.api:GridOptions
16832 * @description The character to use as column separator
16834 * <br/>Defaults to ','
16836 gridOptions.exporterCsvColumnSeparator = gridOptions.exporterCsvColumnSeparator ? gridOptions.exporterCsvColumnSeparator : ',';
16839 * @name exporterCsvFilename
16840 * @propertyOf ui.grid.exporter.api:GridOptions
16841 * @description The default filename to use when saving the downloaded csv.
16842 * This will only work in some browsers.
16843 * <br/>Defaults to 'download.csv'
16845 gridOptions.exporterCsvFilename = gridOptions.exporterCsvFilename ? gridOptions.exporterCsvFilename : 'download.csv';
16848 * @name exporterPdfFilename
16849 * @propertyOf ui.grid.exporter.api:GridOptions
16850 * @description The default filename to use when saving the downloaded pdf, only used in IE (other browsers open pdfs in a new window)
16851 * <br/>Defaults to 'download.pdf'
16853 gridOptions.exporterPdfFilename = gridOptions.exporterPdfFilename ? gridOptions.exporterPdfFilename : 'download.pdf';
16856 * @name exporterOlderExcelCompatibility
16857 * @propertyOf ui.grid.exporter.api:GridOptions
16858 * @description Some versions of excel don't like the utf-16 BOM on the front, and it comes
16859 * through as  in the first column header. Setting this option to false will suppress this, at the
16860 * expense of proper utf-16 handling in applications that do recognise the BOM
16861 * <br/>Defaults to false
16863 gridOptions.exporterOlderExcelCompatibility = gridOptions.exporterOlderExcelCompatibility === true;
16866 * @name exporterPdfDefaultStyle
16867 * @propertyOf ui.grid.exporter.api:GridOptions
16868 * @description The default style in pdfMake format
16869 * <br/>Defaults to:
16876 gridOptions.exporterPdfDefaultStyle = gridOptions.exporterPdfDefaultStyle ? gridOptions.exporterPdfDefaultStyle : { fontSize: 11 };
16879 * @name exporterPdfTableStyle
16880 * @propertyOf ui.grid.exporter.api:GridOptions
16881 * @description The table style in pdfMake format
16882 * <br/>Defaults to:
16885 * margin: [0, 5, 0, 15]
16889 gridOptions.exporterPdfTableStyle = gridOptions.exporterPdfTableStyle ? gridOptions.exporterPdfTableStyle : { margin: [0, 5, 0, 15] };
16892 * @name exporterPdfTableHeaderStyle
16893 * @propertyOf ui.grid.exporter.api:GridOptions
16894 * @description The tableHeader style in pdfMake format
16895 * <br/>Defaults to:
16904 gridOptions.exporterPdfTableHeaderStyle = gridOptions.exporterPdfTableHeaderStyle ? gridOptions.exporterPdfTableHeaderStyle : { bold: true, fontSize: 12, color: 'black' };
16907 * @name exporterPdfHeader
16908 * @propertyOf ui.grid.exporter.api:GridOptions
16909 * @description The header section for pdf exports. Can be
16912 * gridOptions.exporterPdfHeader = 'My Header';
16914 * Can be a more complex object in pdfMake format:
16916 * gridOptions.exporterPdfHeader = {
16919 * { text: 'Right part', alignment: 'right' }
16923 * Or can be a function, allowing page numbers and the like
16925 * gridOptions.exporterPdfHeader: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
16928 gridOptions.exporterPdfHeader = gridOptions.exporterPdfHeader ? gridOptions.exporterPdfHeader : null;
16931 * @name exporterPdfFooter
16932 * @propertyOf ui.grid.exporter.api:GridOptions
16933 * @description The header section for pdf exports. Can be
16936 * gridOptions.exporterPdfFooter = 'My Footer';
16938 * Can be a more complex object in pdfMake format:
16940 * gridOptions.exporterPdfFooter = {
16943 * { text: 'Right part', alignment: 'right' }
16947 * Or can be a function, allowing page numbers and the like
16949 * gridOptions.exporterPdfFooter: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
16952 gridOptions.exporterPdfFooter = gridOptions.exporterPdfFooter ? gridOptions.exporterPdfFooter : null;
16955 * @name exporterPdfOrientation
16956 * @propertyOf ui.grid.exporter.api:GridOptions
16957 * @description The orientation, should be a valid pdfMake value,
16958 * 'landscape' or 'portrait'
16959 * <br/>Defaults to landscape
16961 gridOptions.exporterPdfOrientation = gridOptions.exporterPdfOrientation ? gridOptions.exporterPdfOrientation : 'landscape';
16964 * @name exporterPdfPageSize
16965 * @propertyOf ui.grid.exporter.api:GridOptions
16966 * @description The orientation, should be a valid pdfMake
16967 * paper size, usually 'A4' or 'LETTER'
16968 * {@link https://github.com/bpampuch/pdfmake/blob/master/src/standardPageSizes.js pdfMake page sizes}
16969 * <br/>Defaults to A4
16971 gridOptions.exporterPdfPageSize = gridOptions.exporterPdfPageSize ? gridOptions.exporterPdfPageSize : 'A4';
16974 * @name exporterPdfMaxGridWidth
16975 * @propertyOf ui.grid.exporter.api:GridOptions
16976 * @description The maxium grid width - the current grid width
16977 * will be scaled to match this, with any fixed width columns
16978 * being adjusted accordingly.
16979 * <br/>Defaults to 720 (for A4 landscape), use 670 for LETTER
16981 gridOptions.exporterPdfMaxGridWidth = gridOptions.exporterPdfMaxGridWidth ? gridOptions.exporterPdfMaxGridWidth : 720;
16984 * @name exporterPdfTableLayout
16985 * @propertyOf ui.grid.exporter.api:GridOptions
16986 * @description A tableLayout in pdfMake format,
16987 * controls gridlines and the like. We use the default
16989 * <br/>Defaults to null, which means no layout
16994 * @name exporterMenuAllData
16995 * @porpertyOf ui.grid.exporter.api:GridOptions
16996 * @description Add export all data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
16998 gridOptions.exporterMenuAllData = gridOptions.exporterMenuAllData !== undefined ? gridOptions.exporterMenuAllData : true;
17002 * @name exporterMenuCsv
17003 * @propertyOf ui.grid.exporter.api:GridOptions
17004 * @description Add csv export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17006 gridOptions.exporterMenuCsv = gridOptions.exporterMenuCsv !== undefined ? gridOptions.exporterMenuCsv : true;
17010 * @name exporterMenuPdf
17011 * @propertyOf ui.grid.exporter.api:GridOptions
17012 * @description Add pdf export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17014 gridOptions.exporterMenuPdf = gridOptions.exporterMenuPdf !== undefined ? gridOptions.exporterMenuPdf : true;
17018 * @name exporterPdfCustomFormatter
17019 * @propertyOf ui.grid.exporter.api:GridOptions
17020 * @description A custom callback routine that changes the pdf document, adding any
17021 * custom styling or content that is supported by pdfMake. Takes in the complete docDefinition, and
17022 * must return an updated docDefinition ready for pdfMake.
17024 * In this example we add a style to the style array, so that we can use it in our
17025 * footer definition.
17027 * gridOptions.exporterPdfCustomFormatter = function ( docDefinition ) {
17028 * docDefinition.styles.footerStyle = { bold: true, fontSize: 10 };
17029 * return docDefinition;
17032 * gridOptions.exporterPdfFooter = { text: 'My footer', style: 'footerStyle' }
17035 gridOptions.exporterPdfCustomFormatter = ( gridOptions.exporterPdfCustomFormatter && typeof( gridOptions.exporterPdfCustomFormatter ) === 'function' ) ? gridOptions.exporterPdfCustomFormatter : function ( docDef ) { return docDef; };
17039 * @name exporterHeaderFilterUseName
17040 * @propertyOf ui.grid.exporter.api:GridOptions
17041 * @description Defaults to false, which leads to `displayName` being passed into the headerFilter.
17042 * If set to true, then will pass `name` instead.
17047 * gridOptions.exporterHeaderFilterUseName = true;
17050 gridOptions.exporterHeaderFilterUseName = gridOptions.exporterHeaderFilterUseName === true;
17054 * @name exporterHeaderFilter
17055 * @propertyOf ui.grid.exporter.api:GridOptions
17056 * @description A function to apply to the header displayNames before exporting. Useful for internationalisation,
17057 * for example if you were using angular-translate you'd set this to `$translate.instant`. Note that this
17058 * call must be synchronous, it cannot be a call that returns a promise.
17060 * Behaviour can be changed to pass in `name` instead of `displayName` through use of `exporterHeaderFilterUseName: true`.
17064 * gridOptions.exporterHeaderFilter = function( displayName ){ return 'col: ' + name; };
17068 * gridOptions.exporterHeaderFilter = $translate.instant;
17074 * @name exporterFieldCallback
17075 * @propertyOf ui.grid.exporter.api:GridOptions
17076 * @description A function to call for each field before exporting it. Allows
17077 * massaging of raw data into a display format, for example if you have applied
17078 * filters to convert codes into decodes, or you require
17079 * a specific date format in the exported content.
17081 * The method is called once for each field exported, and provides the grid, the
17082 * gridCol and the GridRow for you to use as context in massaging the data.
17084 * @param {Grid} grid provides the grid in case you have need of it
17085 * @param {GridRow} row the row from which the data comes
17086 * @param {GridCol} col the column from which the data comes
17087 * @param {object} value the value for your massaging
17088 * @returns {object} you must return the massaged value ready for exporting
17092 * gridOptions.exporterFieldCallback = function ( grid, row, col, value ){
17093 * if ( col.name === 'status' ){
17094 * value = decodeStatus( value );
17100 gridOptions.exporterFieldCallback = gridOptions.exporterFieldCallback ? gridOptions.exporterFieldCallback : function( grid, row, col, value ) { return value; };
17104 * @name exporterAllDataFn
17105 * @propertyOf ui.grid.exporter.api:GridOptions
17106 * @description This promise is needed when exporting all rows,
17107 * and the data need to be provided by server side. Default is null.
17108 * @returns {Promise} a promise to load all data from server
17112 * gridOptions.exporterAllDataFn = function () {
17113 * return $http.get('/data/100.json')
17117 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataFn ? gridOptions.exporterAllDataFn : null;
17121 * @name exporterAllDataPromise
17122 * @propertyOf ui.grid.exporter.api:GridOptions
17123 * @description DEPRECATED - exporterAllDataFn used to be
17124 * called this, but it wasn't a promise, it was a function that returned
17125 * a promise. Deprecated, but supported for backward compatibility, use
17126 * exporterAllDataFn instead.
17127 * @returns {Promise} a promise to load all data from server
17131 * gridOptions.exporterAllDataFn = function () {
17132 * return $http.get('/data/100.json')
17136 if ( gridOptions.exporterAllDataFn == null && gridOptions.exporterAllDataPromise ) {
17137 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataPromise;
17145 * @methodOf ui.grid.exporter.service:uiGridExporterService
17146 * @description Adds export items to the grid menu,
17147 * allowing the user to select export options
17148 * @param {Grid} grid the grid from which data should be exported
17150 addToMenu: function ( grid ) {
17151 grid.api.core.addToGridMenu( grid, [
17153 title: i18nService.getSafeText('gridMenu.exporterAllAsCsv'),
17154 action: function ($event) {
17155 this.grid.api.exporter.csvExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17157 shown: function() {
17158 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuAllData;
17163 title: i18nService.getSafeText('gridMenu.exporterVisibleAsCsv'),
17164 action: function ($event) {
17165 this.grid.api.exporter.csvExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17167 shown: function() {
17168 return this.grid.options.exporterMenuCsv;
17173 title: i18nService.getSafeText('gridMenu.exporterSelectedAsCsv'),
17174 action: function ($event) {
17175 this.grid.api.exporter.csvExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17177 shown: function() {
17178 return this.grid.options.exporterMenuCsv &&
17179 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17184 title: i18nService.getSafeText('gridMenu.exporterAllAsPdf'),
17185 action: function ($event) {
17186 this.grid.api.exporter.pdfExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17188 shown: function() {
17189 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuAllData;
17194 title: i18nService.getSafeText('gridMenu.exporterVisibleAsPdf'),
17195 action: function ($event) {
17196 this.grid.api.exporter.pdfExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17198 shown: function() {
17199 return this.grid.options.exporterMenuPdf;
17204 title: i18nService.getSafeText('gridMenu.exporterSelectedAsPdf'),
17205 action: function ($event) {
17206 this.grid.api.exporter.pdfExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17208 shown: function() {
17209 return this.grid.options.exporterMenuPdf &&
17210 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17221 * @methodOf ui.grid.exporter.service:uiGridExporterService
17222 * @description Exports rows from the grid in csv format,
17223 * the data exported is selected based on the provided options
17224 * @param {Grid} grid the grid from which data should be exported
17225 * @param {string} rowTypes which rows to export, valid values are
17226 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17227 * uiGridExporterConstants.SELECTED
17228 * @param {string} colTypes which columns to export, valid values are
17229 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17230 * uiGridExporterConstants.SELECTED
17232 csvExport: function (grid, rowTypes, colTypes) {
17234 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function() {
17235 var exportColumnHeaders = grid.options.showHeader ? self.getColumnHeaders(grid, colTypes) : [];
17236 var exportData = self.getData(grid, rowTypes, colTypes);
17237 var csvContent = self.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator);
17239 self.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility);
17245 * @name loadAllDataIfNeeded
17246 * @methodOf ui.grid.exporter.service:uiGridExporterService
17247 * @description When using server side pagination, use exporterAllDataFn to
17248 * load all data before continuing processing.
17249 * When using client side pagination, return a resolved promise so processing
17250 * continues immediately
17251 * @param {Grid} grid the grid from which data should be exported
17252 * @param {string} rowTypes which rows to export, valid values are
17253 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17254 * uiGridExporterConstants.SELECTED
17255 * @param {string} colTypes which columns to export, valid values are
17256 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17257 * uiGridExporterConstants.SELECTED
17259 loadAllDataIfNeeded: function (grid, rowTypes, colTypes) {
17260 if ( rowTypes === uiGridExporterConstants.ALL && grid.rows.length !== grid.options.totalItems && grid.options.exporterAllDataFn) {
17261 return grid.options.exporterAllDataFn()
17263 grid.modifyRows(grid.options.data);
17266 var deferred = $q.defer();
17267 deferred.resolve();
17268 return deferred.promise;
17274 * @propertyOf ui.grid.exporter.api:ColumnDef
17275 * @name exporterSuppressExport
17276 * @description Suppresses export for this column. Used by selection and expandable.
17281 * @name getColumnHeaders
17282 * @methodOf ui.grid.exporter.service:uiGridExporterService
17283 * @description Gets the column headers from the grid to use
17284 * as a title row for the exported file, all headers have
17285 * headerCellFilters applied as appropriate.
17287 * Column headers are an array of objects, each object has
17288 * name, displayName, width and align attributes. Only name is
17289 * used for csv, all attributes are used for pdf.
17291 * @param {Grid} grid the grid from which data should be exported
17292 * @param {string} colTypes which columns to export, valid values are
17293 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17294 * uiGridExporterConstants.SELECTED
17296 getColumnHeaders: function (grid, colTypes) {
17300 if ( colTypes === uiGridExporterConstants.ALL ){
17301 columns = grid.columns;
17303 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17304 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17305 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17307 columns = leftColumns.concat(bodyColumns,rightColumns);
17310 columns.forEach( function( gridCol, index ) {
17311 if ( gridCol.colDef.exporterSuppressExport !== true &&
17312 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17314 name: gridCol.field,
17315 displayName: grid.options.exporterHeaderFilter ? ( grid.options.exporterHeaderFilterUseName ? grid.options.exporterHeaderFilter(gridCol.name) : grid.options.exporterHeaderFilter(gridCol.displayName) ) : gridCol.displayName,
17316 width: gridCol.drawnWidth ? gridCol.drawnWidth : gridCol.width,
17317 align: gridCol.colDef.type === 'number' ? 'right' : 'left'
17328 * @propertyOf ui.grid.exporter.api:ColumnDef
17329 * @name exporterPdfAlign
17330 * @description the alignment you'd like for this specific column when
17331 * exported into a pdf. Can be 'left', 'right', 'center' or any other
17332 * valid pdfMake alignment option.
17338 * @name ui.grid.exporter.api:GridRow
17339 * @description GridRow settings for exporter
17343 * @name exporterEnableExporting
17344 * @propertyOf ui.grid.exporter.api:GridRow
17345 * @description If set to false, then don't export this row, notwithstanding visible or
17347 * <br/>Defaults to true
17353 * @methodOf ui.grid.exporter.service:uiGridExporterService
17354 * @description Gets data from the grid based on the provided options,
17355 * all cells have cellFilters applied as appropriate. Any rows marked
17356 * `exporterEnableExporting: false` will not be exported
17357 * @param {Grid} grid the grid from which data should be exported
17358 * @param {string} rowTypes which rows to export, valid values are
17359 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17360 * uiGridExporterConstants.SELECTED
17361 * @param {string} colTypes which columns to export, valid values are
17362 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17363 * uiGridExporterConstants.SELECTED
17365 getData: function (grid, rowTypes, colTypes) {
17370 switch ( rowTypes ) {
17371 case uiGridExporterConstants.ALL:
17374 case uiGridExporterConstants.VISIBLE:
17375 rows = grid.getVisibleRows();
17377 case uiGridExporterConstants.SELECTED:
17378 if ( grid.api.selection ){
17379 rows = grid.api.selection.getSelectedGridRows();
17381 gridUtil.logError('selection feature must be enabled to allow selected rows to be exported');
17386 if ( colTypes === uiGridExporterConstants.ALL ){
17387 columns = grid.columns;
17389 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17390 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17391 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17393 columns = leftColumns.concat(bodyColumns,rightColumns);
17396 rows.forEach( function( row, index ) {
17398 if (row.exporterEnableExporting !== false) {
17399 var extractedRow = [];
17402 columns.forEach( function( gridCol, index ) {
17403 if ( (gridCol.visible || colTypes === uiGridExporterConstants.ALL ) &&
17404 gridCol.colDef.exporterSuppressExport !== true &&
17405 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17406 var extractedField = { value: grid.options.exporterFieldCallback( grid, row, gridCol, grid.getCellValue( row, gridCol ) ) };
17407 if ( gridCol.colDef.exporterPdfAlign ) {
17408 extractedField.alignment = gridCol.colDef.exporterPdfAlign;
17410 extractedRow.push(extractedField);
17414 data.push(extractedRow);
17424 * @name formatAsCSV
17425 * @methodOf ui.grid.exporter.service:uiGridExporterService
17426 * @description Formats the column headers and data as a CSV,
17427 * and sends that data to the user
17428 * @param {array} exportColumnHeaders an array of column headers,
17429 * where each header is an object with name, width and maybe alignment
17430 * @param {array} exportData an array of rows, where each row is
17431 * an array of column data
17432 * @returns {string} csv the formatted csv as a string
17434 formatAsCsv: function (exportColumnHeaders, exportData, separator) {
17437 var bareHeaders = exportColumnHeaders.map(function(header){return { value: header.displayName };});
17439 var csv = bareHeaders.length > 0 ? (self.formatRowAsCsv(this, separator)(bareHeaders) + '\n') : '';
17441 csv += exportData.map(this.formatRowAsCsv(this, separator)).join('\n');
17448 * @name formatRowAsCsv
17449 * @methodOf ui.grid.exporter.service:uiGridExporterService
17450 * @description Renders a single field as a csv field, including
17451 * quotes around the value
17452 * @param {exporterService} exporter pass in exporter
17453 * @param {array} row the row to be turned into a csv string
17454 * @returns {string} a csv-ified version of the row
17456 formatRowAsCsv: function (exporter, separator) {
17457 return function (row) {
17458 return row.map(exporter.formatFieldAsCsv).join(separator);
17464 * @name formatFieldAsCsv
17465 * @methodOf ui.grid.exporter.service:uiGridExporterService
17466 * @description Renders a single field as a csv field, including
17467 * quotes around the value
17468 * @param {field} field the field to be turned into a csv string,
17469 * may be of any type
17470 * @returns {string} a csv-ified version of the field
17472 formatFieldAsCsv: function (field) {
17473 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
17476 if (typeof(field.value) === 'number') {
17477 return field.value;
17479 if (typeof(field.value) === 'boolean') {
17480 return (field.value ? 'TRUE' : 'FALSE') ;
17482 if (typeof(field.value) === 'string') {
17483 return '"' + field.value.replace(/"/g,'""') + '"';
17486 return JSON.stringify(field.value);
17493 * @methodOf ui.grid.exporter.service:uiGridExporterService
17494 * @description Checks whether current browser is IE and returns it's version if it is
17496 isIE: function () {
17497 var match = navigator.userAgent.match(/(?:MSIE |Trident\/.*; rv:)(\d+)/);
17498 return match ? parseInt(match[1]) : false;
17504 * @name downloadFile
17505 * @methodOf ui.grid.exporter.service:uiGridExporterService
17506 * @description Triggers download of a csv file. Logic provided
17507 * by @cssensei (from his colleagues at https://github.com/ifeelgoods) in issue #2391
17508 * @param {string} fileName the filename we'd like our file to be
17510 * @param {string} csvContent the csv content that we'd like to
17511 * download as a file
17512 * @param {boolean} exporterOlderExcelCompatibility whether or not we put a utf-16 BOM on the from (\uFEFF)
17514 downloadFile: function (fileName, csvContent, exporterOlderExcelCompatibility) {
17516 var a = D.createElement('a');
17517 var strMimeType = 'application/octet-stream;charset=utf-8';
17521 ieVersion = this.isIE();
17522 if (ieVersion && ieVersion < 10) {
17523 var frame = D.createElement('iframe');
17524 document.body.appendChild(frame);
17526 frame.contentWindow.document.open("text/html", "replace");
17527 frame.contentWindow.document.write('sep=,\r\n' + csvContent);
17528 frame.contentWindow.document.close();
17529 frame.contentWindow.focus();
17530 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
17532 document.body.removeChild(frame);
17537 if (navigator.msSaveBlob) {
17538 return navigator.msSaveOrOpenBlob(
17540 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
17541 { type: strMimeType } ),
17546 //html5 A[download]
17547 if ('download' in a) {
17548 var blob = new Blob(
17549 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
17550 { type: strMimeType }
17552 rawFile = URL.createObjectURL(blob);
17553 a.setAttribute('download', fileName);
17555 rawFile = 'data:' + strMimeType + ',' + encodeURIComponent(csvContent);
17556 a.setAttribute('target', '_blank');
17560 a.setAttribute('style', 'display:none;');
17561 D.body.appendChild(a);
17562 setTimeout(function() {
17565 // Workaround for Safari 5
17566 } else if (document.createEvent) {
17567 var eventObj = document.createEvent('MouseEvents');
17568 eventObj.initEvent('click', true, true);
17569 a.dispatchEvent(eventObj);
17571 D.body.removeChild(a);
17579 * @methodOf ui.grid.exporter.service:uiGridExporterService
17580 * @description Exports rows from the grid in pdf format,
17581 * the data exported is selected based on the provided options.
17582 * Note that this function has a dependency on pdfMake, which must
17583 * be installed. The resulting pdf opens in a new
17585 * @param {Grid} grid the grid from which data should be exported
17586 * @param {string} rowTypes which rows to export, valid values are
17587 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17588 * uiGridExporterConstants.SELECTED
17589 * @param {string} colTypes which columns to export, valid values are
17590 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17591 * uiGridExporterConstants.SELECTED
17593 pdfExport: function (grid, rowTypes, colTypes) {
17595 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function () {
17596 var exportColumnHeaders = self.getColumnHeaders(grid, colTypes);
17597 var exportData = self.getData(grid, rowTypes, colTypes);
17598 var docDefinition = self.prepareAsPdf(grid, exportColumnHeaders, exportData);
17601 self.downloadPDF(grid.options.exporterPdfFilename, docDefinition);
17603 pdfMake.createPdf(docDefinition).open();
17611 * @name downloadPdf
17612 * @methodOf ui.grid.exporter.service:uiGridExporterService
17613 * @description Generates and retrieves the pdf as a blob, then downloads
17614 * it as a file. Only used in IE, in all other browsers we use the native
17615 * pdfMake.open function to just open the PDF
17616 * @param {string} fileName the filename to give to the pdf, can be set
17617 * through exporterPdfFilename
17618 * @param {object} docDefinition a pdf docDefinition that we can generate
17619 * and get a blob from
17621 downloadPDF: function (fileName, docDefinition) {
17623 var a = D.createElement('a');
17624 var strMimeType = 'application/octet-stream;charset=utf-8';
17628 ieVersion = this.isIE();
17629 var doc = pdfMake.createPdf(docDefinition);
17632 doc.getBuffer( function (buffer) {
17633 blob = new Blob([buffer]);
17635 if (ieVersion && ieVersion < 10) {
17636 var frame = D.createElement('iframe');
17637 document.body.appendChild(frame);
17639 frame.contentWindow.document.open("text/html", "replace");
17640 frame.contentWindow.document.write(blob);
17641 frame.contentWindow.document.close();
17642 frame.contentWindow.focus();
17643 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
17645 document.body.removeChild(frame);
17650 if (navigator.msSaveBlob) {
17651 return navigator.msSaveBlob(
17661 * @name renderAsPdf
17662 * @methodOf ui.grid.exporter.service:uiGridExporterService
17663 * @description Renders the data into a pdf, and opens that pdf.
17665 * @param {Grid} grid the grid from which data should be exported
17666 * @param {array} exportColumnHeaders an array of column headers,
17667 * where each header is an object with name, width and maybe alignment
17668 * @param {array} exportData an array of rows, where each row is
17669 * an array of column data
17670 * @returns {object} a pdfMake format document definition, ready
17673 prepareAsPdf: function(grid, exportColumnHeaders, exportData) {
17674 var headerWidths = this.calculatePdfHeaderWidths( grid, exportColumnHeaders );
17676 var headerColumns = exportColumnHeaders.map( function( header ) {
17677 return { text: header.displayName, style: 'tableHeader' };
17680 var stringData = exportData.map(this.formatRowAsPdf(this));
17682 var allData = [headerColumns].concat(stringData);
17684 var docDefinition = {
17685 pageOrientation: grid.options.exporterPdfOrientation,
17686 pageSize: grid.options.exporterPdfPageSize,
17688 style: 'tableStyle',
17691 widths: headerWidths,
17696 tableStyle: grid.options.exporterPdfTableStyle,
17697 tableHeader: grid.options.exporterPdfTableHeaderStyle
17699 defaultStyle: grid.options.exporterPdfDefaultStyle
17702 if ( grid.options.exporterPdfLayout ){
17703 docDefinition.layout = grid.options.exporterPdfLayout;
17706 if ( grid.options.exporterPdfHeader ){
17707 docDefinition.header = grid.options.exporterPdfHeader;
17710 if ( grid.options.exporterPdfFooter ){
17711 docDefinition.footer = grid.options.exporterPdfFooter;
17714 if ( grid.options.exporterPdfCustomFormatter ){
17715 docDefinition = grid.options.exporterPdfCustomFormatter( docDefinition );
17717 return docDefinition;
17724 * @name calculatePdfHeaderWidths
17725 * @methodOf ui.grid.exporter.service:uiGridExporterService
17726 * @description Determines the column widths base on the
17727 * widths we got from the grid. If the column is drawn
17728 * then we have a drawnWidth. If the column is not visible
17729 * then we have '*', 'x%' or a width. When columns are
17730 * not visible they don't contribute to the overall gridWidth,
17731 * so we need to adjust to allow for extra columns
17733 * Our basic heuristic is to take the current gridWidth, plus
17734 * numeric columns and call this the base gridwidth.
17736 * To that we add 100 for any '*' column, and x% of the base gridWidth
17737 * for any column that is a %
17739 * @param {Grid} grid the grid from which data should be exported
17740 * @param {array} exportHeaders array of header information
17741 * @returns {object} an array of header widths
17743 calculatePdfHeaderWidths: function ( grid, exportHeaders ) {
17744 var baseGridWidth = 0;
17745 exportHeaders.forEach( function(value){
17746 if (typeof(value.width) === 'number'){
17747 baseGridWidth += value.width;
17751 var extraColumns = 0;
17752 exportHeaders.forEach( function(value){
17753 if (value.width === '*'){
17754 extraColumns += 100;
17756 if (typeof(value.width) === 'string' && value.width.match(/(\d)*%/)) {
17757 var percent = parseInt(value.width.match(/(\d)*%/)[0]);
17759 value.width = baseGridWidth * percent / 100;
17760 extraColumns += value.width;
17764 var gridWidth = baseGridWidth + extraColumns;
17766 return exportHeaders.map(function( header ) {
17767 return header.width === '*' ? header.width : header.width * grid.options.exporterPdfMaxGridWidth / gridWidth;
17774 * @name formatRowAsPdf
17775 * @methodOf ui.grid.exporter.service:uiGridExporterService
17776 * @description Renders a row in a format consumable by PDF,
17777 * mainly meaning casting everything to a string
17778 * @param {exporterService} exporter pass in exporter
17779 * @param {array} row the row to be turned into a csv string
17780 * @returns {string} a csv-ified version of the row
17782 formatRowAsPdf: function ( exporter ) {
17783 return function( row ) {
17784 return row.map(exporter.formatFieldAsPdfString);
17791 * @name formatFieldAsCsv
17792 * @methodOf ui.grid.exporter.service:uiGridExporterService
17793 * @description Renders a single field as a pdf-able field, which
17794 * is different from a csv field only in that strings don't have quotes
17796 * @param {field} field the field to be turned into a pdf string,
17797 * may be of any type
17798 * @returns {string} a string-ified version of the field
17800 formatFieldAsPdfString: function (field) {
17802 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
17804 } else if (typeof(field.value) === 'number') {
17805 returnVal = field.value.toString();
17806 } else if (typeof(field.value) === 'boolean') {
17807 returnVal = (field.value ? 'TRUE' : 'FALSE') ;
17808 } else if (typeof(field.value) === 'string') {
17809 returnVal = field.value.replace(/"/g,'""');
17811 returnVal = JSON.stringify(field.value).replace(/^"/,'').replace(/"$/,'');
17814 if (field.alignment && typeof(field.alignment) === 'string' ){
17815 returnVal = { text: returnVal, alignment: field.alignment };
17829 * @name ui.grid.exporter.directive:uiGridExporter
17833 * @description Adds exporter features to grid
17836 <example module="app">
17837 <file name="app.js">
17838 var app = angular.module('app', ['ui.grid', 'ui.grid.exporter']);
17840 app.controller('MainCtrl', ['$scope', function ($scope) {
17842 { name: 'Bob', title: 'CEO' },
17843 { name: 'Frank', title: 'Lowly Developer' }
17846 $scope.gridOptions = {
17847 enableGridMenu: true,
17848 exporterMenuCsv: false,
17850 {name: 'name', enableCellEdit: true},
17851 {name: 'title', enableCellEdit: true}
17857 <file name="index.html">
17858 <div ng-controller="MainCtrl">
17859 <div ui-grid="gridOptions" ui-grid-exporter></div>
17864 module.directive('uiGridExporter', ['uiGridExporterConstants', 'uiGridExporterService', 'gridUtil', '$compile',
17865 function (uiGridExporterConstants, uiGridExporterService, gridUtil, $compile) {
17869 require: '^uiGrid',
17871 link: function ($scope, $elm, $attrs, uiGridCtrl) {
17872 uiGridExporterService.initializeGrid(uiGridCtrl.grid);
17873 uiGridCtrl.grid.exporter.$scope = $scope;
17885 * @name ui.grid.grouping
17888 * # ui.grid.grouping
17890 * <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>
17892 * This module provides grouping of rows based on the data in them, similar
17893 * in concept to excel grouping. You can group multiple columns, resulting in
17896 * In concept this feature is similar to sorting + grid footer/aggregation, it
17897 * sorts the data based on the grouped columns, then creates group rows that
17898 * reflect a break in the data. Each of those group rows can have aggregations for
17899 * the data within that group.
17901 * This feature leverages treeBase to provide the tree functionality itself,
17902 * the key thing this feature does therefore is to set treeLevels on the rows
17903 * and insert the group headers.
17905 * Design information:
17906 * -------------------
17908 * Each column will get new menu items - group by, and aggregate by. Group by
17909 * will cause this column to be sorted (if not already), and will move this column
17910 * to the front of the sorted columns (i.e. grouped columns take precedence over
17911 * sorted columns). It will respect the sort order already set if there is one,
17912 * and it will allow the sorting logic to change that sort order, it just forces
17913 * the column to the front of the sorting. You can group by multiple columns, the
17914 * logic will add this column to the sorting after any already grouped columns.
17916 * Once a grouping is defined, grouping logic is added to the rowsProcessors. This
17917 * will process the rows, identifying a break in the data value, and inserting a grouping row.
17918 * Grouping rows have specific attributes on them:
17920 * - internalRow = true: tells us that this isn't a real row, so we can ignore it
17921 * from any processing that it looking at core data rows. This is used by the core
17922 * logic (or will be one day), as it's not grouping specific
17923 * - groupHeader = true: tells us this is a groupHeader. This is used by the grouping logic
17924 * to know if this is a groupHeader row or not
17926 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
17927 * row order or filtering or anything like that is changed. In order to avoid the row instantiation
17928 * time, and to preserve state across invocations, we hold a cache of the rows that we created
17929 * last time, and we use them again this time if we can.
17931 * By default rows are collapsed, which means all data rows have their visible property
17932 * set to false, and only level 0 group rows are set to visible.
17937 * <div doc-module-components="ui.grid.grouping"></div>
17940 var module = angular.module('ui.grid.grouping', ['ui.grid', 'ui.grid.treeBase']);
17944 * @name ui.grid.grouping.constant:uiGridGroupingConstants
17946 * @description constants available in grouping module, this includes
17947 * all the constants declared in the treeBase module (these are manually copied
17948 * as there isn't an easy way to include constants in another constants file, and
17949 * we don't want to make users include treeBase)
17952 module.constant('uiGridGroupingConstants', {
17953 featureName: "grouping",
17954 rowHeaderColName: 'treeBaseRowHeaderCol',
17955 EXPANDED: 'expanded',
17956 COLLAPSED: 'collapsed',
17968 * @name ui.grid.grouping.service:uiGridGroupingService
17970 * @description Services for grouping features
17972 module.service('uiGridGroupingService', ['$q', 'uiGridGroupingConstants', 'gridUtil', 'rowSorter', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'uiGridTreeBaseService',
17973 function ($q, uiGridGroupingConstants, gridUtil, rowSorter, GridRow, gridClassFactory, i18nService, uiGridConstants, uiGridTreeBaseService) {
17977 initializeGrid: function (grid, $scope) {
17978 uiGridTreeBaseService.initializeGrid( grid, $scope );
17980 //add feature namespace and any properties to grid for needed
17983 * @name ui.grid.grouping.grid:grouping
17985 * @description Grid properties and functions added for grouping
17987 grid.grouping = {};
17991 * @propertyOf ui.grid.grouping.grid:grouping
17992 * @name groupHeaderCache
17994 * @description Cache that holds the group header rows we created last time, we'll
17995 * reuse these next time, not least because they hold our expanded states.
17997 * We need to take care with these that they don't become a memory leak, we
17998 * create a new cache each time using the values from the old cache. This works
17999 * so long as we're creating group rows for invisible rows as well.
18001 * The cache is a nested hash, indexed on the value we grouped by. So if we
18002 * grouped by gender then age, we'd maybe have something like:
18006 * row: <pointer to the old row>,
18008 * 22: { row: <pointer to the old row> },
18009 * 31: { row: <pointer to the old row> }
18012 * row: <pointer to the old row>,
18014 * 28: { row: <pointer to the old row> },
18015 * 55: { row: <pointer to the old row> }
18020 * We create new rows for any missing rows, this means that they come in as collapsed.
18023 grid.grouping.groupHeaderCache = {};
18025 service.defaultGridOptions(grid.options);
18027 grid.registerRowsProcessor(service.groupRows, 400);
18029 grid.registerColumnBuilder( service.groupingColumnBuilder);
18031 grid.registerColumnsProcessor(service.groupingColumnProcessor, 400);
18035 * @name ui.grid.grouping.api:PublicApi
18037 * @description Public Api for grouping feature
18044 * @eventOf ui.grid.grouping.api:PublicApi
18045 * @name aggregationChanged
18046 * @description raised whenever aggregation is changed, added or removed from a column
18049 * gridApi.grouping.on.aggregationChanged(scope,function(col){})
18051 * @param {gridCol} col the column which on which aggregation changed. The aggregation
18052 * type is available as `col.treeAggregation.type`
18054 aggregationChanged: {},
18057 * @eventOf ui.grid.grouping.api:PublicApi
18058 * @name groupingChanged
18059 * @description raised whenever the grouped columns changes
18062 * gridApi.grouping.on.groupingChanged(scope,function(col){})
18064 * @param {gridCol} col the column which on which grouping changed. The new grouping is
18065 * available as `col.grouping`
18067 groupingChanged: {}
18074 * @name getGrouping
18075 * @methodOf ui.grid.grouping.api:PublicApi
18076 * @description Get the grouping configuration for this grid,
18077 * used by the saveState feature. Adds expandedState to the information
18078 * provided by the internal getGrouping, and removes any aggregations that have a source
18079 * of grouping (i.e. will be automatically reapplied when we regroup the column)
18080 * Returned grouping is an object
18081 * `{ grouping: groupArray, treeAggregations: aggregateArray, expandedState: hash }`
18082 * where grouping contains an array of objects:
18083 * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }`
18084 * and aggregations contains an array of objects:
18085 * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }`
18086 * and expandedState is a hash of the currently expanded nodes
18088 * The groupArray will be sorted by groupPriority.
18090 * @param {boolean} getExpanded whether or not to return the expanded state
18091 * @returns {object} grouping configuration
18093 getGrouping: function ( getExpanded ) {
18094 var grouping = service.getGrouping(grid);
18096 grouping.grouping.forEach( function( group ) {
18097 group.colName = group.col.name;
18101 grouping.aggregations.forEach( function( aggregation ) {
18102 aggregation.colName = aggregation.col.name;
18103 delete aggregation.col;
18106 grouping.aggregations = grouping.aggregations.filter( function( aggregation ){
18107 return !aggregation.aggregation.source || aggregation.aggregation.source !== 'grouping';
18110 if ( getExpanded ){
18111 grouping.rowExpandedStates = service.getRowExpandedStates( grid.grouping.groupingHeaderCache );
18119 * @name setGrouping
18120 * @methodOf ui.grid.grouping.api:PublicApi
18121 * @description Set the grouping configuration for this grid,
18122 * used by the saveState feature, but can also be used by any
18123 * user to specify a combined grouping and aggregation configuration
18124 * @param {object} config the config you want to apply, in the format
18125 * provided out by getGrouping
18127 setGrouping: function ( config ) {
18128 service.setGrouping(grid, config);
18133 * @name groupColumn
18134 * @methodOf ui.grid.grouping.api:PublicApi
18135 * @description Adds this column to the existing grouping, at the end of the priority order.
18136 * If the column doesn't have a sort, adds one, by default ASC
18138 * This column will move to the left of any non-group columns, the
18139 * move is handled in a columnProcessor, so gets called as part of refresh
18141 * @param {string} columnName the name of the column we want to group
18143 groupColumn: function( columnName ) {
18144 var column = grid.getColumn(columnName);
18145 service.groupColumn(grid, column);
18150 * @name ungroupColumn
18151 * @methodOf ui.grid.grouping.api:PublicApi
18152 * @description Removes the groupPriority from this column. If the
18153 * column was previously aggregated the aggregation will come back.
18154 * The sort will remain.
18156 * This column will move to the right of any other group columns, the
18157 * move is handled in a columnProcessor, so gets called as part of refresh
18159 * @param {string} columnName the name of the column we want to ungroup
18161 ungroupColumn: function( columnName ) {
18162 var column = grid.getColumn(columnName);
18163 service.ungroupColumn(grid, column);
18168 * @name clearGrouping
18169 * @methodOf ui.grid.grouping.api:PublicApi
18170 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
18171 * as we don't know whether that sorting was added by grouping or was there beforehand
18174 clearGrouping: function() {
18175 service.clearGrouping(grid);
18180 * @name aggregateColumn
18181 * @methodOf ui.grid.grouping.api:PublicApi
18182 * @description Sets the aggregation type on a column, if the
18183 * column is currently grouped then it removes the grouping first.
18184 * If the aggregationDef is null then will result in the aggregation
18187 * @param {string} columnName the column we want to aggregate
18188 * @param {string} or {function} aggregationDef one of the recognised types
18189 * from uiGridGroupingConstants or a custom aggregation function.
18190 * @param {string} aggregationLabel (optional) The label to use for this aggregation.
18192 aggregateColumn: function( columnName, aggregationDef, aggregationLabel){
18193 var column = grid.getColumn(columnName);
18194 service.aggregateColumn( grid, column, aggregationDef, aggregationLabel);
18201 grid.api.registerEventsFromObject(publicApi.events);
18203 grid.api.registerMethodsFromObject(publicApi.methods);
18205 grid.api.core.on.sortChanged( $scope, service.tidyPriorities);
18209 defaultGridOptions: function (gridOptions) {
18210 //default option to true unless it was explicitly set to false
18213 * @name ui.grid.grouping.api:GridOptions
18215 * @description GridOptions for grouping feature, these are available to be
18216 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
18221 * @name enableGrouping
18222 * @propertyOf ui.grid.grouping.api:GridOptions
18223 * @description Enable row grouping for entire grid.
18224 * <br/>Defaults to true
18226 gridOptions.enableGrouping = gridOptions.enableGrouping !== false;
18230 * @name groupingShowCounts
18231 * @propertyOf ui.grid.grouping.api:GridOptions
18232 * @description shows counts on the groupHeader rows. Not that if you are using a cellFilter or a
18233 * sortingAlgorithm which relies on a specific format or data type, showing counts may cause that
18234 * to break, since the group header rows will always be a string with groupingShowCounts enabled.
18235 * <br/>Defaults to true except on columns of type 'date'
18237 gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false;
18241 * @name groupingNullLabel
18242 * @propertyOf ui.grid.grouping.api:GridOptions
18243 * @description The string to use for the grouping header row label on rows which contain a null or undefined value in the grouped column.
18244 * <br/>Defaults to "Null"
18246 gridOptions.groupingNullLabel = typeof(gridOptions.groupingNullLabel) === 'undefined' ? 'Null' : gridOptions.groupingNullLabel;
18250 * @name enableGroupHeaderSelection
18251 * @propertyOf ui.grid.grouping.api:GridOptions
18252 * @description Allows group header rows to be selected.
18253 * <br/>Defaults to false
18255 gridOptions.enableGroupHeaderSelection = gridOptions.enableGroupHeaderSelection === true;
18261 * @name groupingColumnBuilder
18262 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18263 * @description Sets the grouping defaults based on the columnDefs
18265 * @param {object} colDef columnDef we're basing on
18266 * @param {GridCol} col the column we're to update
18267 * @param {object} gridOptions the options we should use
18268 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
18270 groupingColumnBuilder: function (colDef, col, gridOptions) {
18273 * @name ui.grid.grouping.api:ColumnDef
18275 * @description ColumnDef for grouping feature, these are available to be
18276 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
18281 * @name enableGrouping
18282 * @propertyOf ui.grid.grouping.api:ColumnDef
18283 * @description Enable grouping on this column
18284 * <br/>Defaults to true.
18286 if (colDef.enableGrouping === false){
18293 * @propertyOf ui.grid.grouping.api:ColumnDef
18294 * @description Set the grouping for a column. Format is:
18297 * groupPriority: <number, starts at 0, if less than 0 or undefined then we're aggregating in this column>
18301 * **Note that aggregation used to be included in grouping, but is now separately set on the column via treeAggregation
18302 * setting in treeBase**
18304 * We group in the priority order given, this will also put these columns to the high order of the sort irrespective
18305 * of the sort priority given them. If there is no sort defined then we sort ascending, if there is a sort defined then
18306 * we use that sort.
18308 * If the groupPriority is undefined or less than 0, then we expect to be aggregating, and we look at the
18309 * aggregation types to determine what sort of aggregation we can do. Values are in the constants file, but
18310 * include SUM, COUNT, MAX, MIN
18312 * groupPriorities should generally be sequential, if they're not then the next time getGrouping is called
18313 * we'll renumber them to be sequential.
18314 * <br/>Defaults to undefined.
18317 if ( typeof(col.grouping) === 'undefined' && typeof(colDef.grouping) !== 'undefined') {
18318 col.grouping = angular.copy(colDef.grouping);
18319 if ( typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority > -1 ){
18320 col.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
18321 col.treeAggregationFinalizerFn = service.groupedFinalizerFn;
18323 } else if (typeof(col.grouping) === 'undefined'){
18327 if (typeof(col.grouping) !== 'undefined' && typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority >= 0){
18328 col.suppressRemoveSort = true;
18331 var groupColumn = {
18332 name: 'ui.grid.grouping.group',
18333 title: i18nService.get().grouping.group,
18334 icon: 'ui-grid-icon-indent-right',
18335 shown: function () {
18336 return typeof(this.context.col.grouping) === 'undefined' ||
18337 typeof(this.context.col.grouping.groupPriority) === 'undefined' ||
18338 this.context.col.grouping.groupPriority < 0;
18340 action: function () {
18341 service.groupColumn( this.context.col.grid, this.context.col );
18345 var ungroupColumn = {
18346 name: 'ui.grid.grouping.ungroup',
18347 title: i18nService.get().grouping.ungroup,
18348 icon: 'ui-grid-icon-indent-left',
18349 shown: function () {
18350 return typeof(this.context.col.grouping) !== 'undefined' &&
18351 typeof(this.context.col.grouping.groupPriority) !== 'undefined' &&
18352 this.context.col.grouping.groupPriority >= 0;
18354 action: function () {
18355 service.ungroupColumn( this.context.col.grid, this.context.col );
18359 var aggregateRemove = {
18360 name: 'ui.grid.grouping.aggregateRemove',
18361 title: i18nService.get().grouping.aggregate_remove,
18362 shown: function () {
18363 return typeof(this.context.col.treeAggregationFn) !== 'undefined';
18365 action: function () {
18366 service.aggregateColumn( this.context.col.grid, this.context.col, null);
18370 // generic adder for the aggregation menus, which follow a pattern
18371 var addAggregationMenu = function(type, title){
18372 title = title || i18nService.get().grouping['aggregate_' + type] || type;
18374 name: 'ui.grid.grouping.aggregate' + type,
18376 shown: function () {
18377 return typeof(this.context.col.treeAggregation) === 'undefined' ||
18378 typeof(this.context.col.treeAggregation.type) === 'undefined' ||
18379 this.context.col.treeAggregation.type !== type;
18381 action: function () {
18382 service.aggregateColumn( this.context.col.grid, this.context.col, type);
18386 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregate' + type)) {
18387 col.menuItems.push(menuItem);
18393 * @name groupingShowGroupingMenu
18394 * @propertyOf ui.grid.grouping.api:ColumnDef
18395 * @description Show the grouping (group and ungroup items) menu on this column
18396 * <br/>Defaults to true.
18398 if ( col.colDef.groupingShowGroupingMenu !== false ){
18399 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.group')) {
18400 col.menuItems.push(groupColumn);
18403 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.ungroup')) {
18404 col.menuItems.push(ungroupColumn);
18411 * @name groupingShowAggregationMenu
18412 * @propertyOf ui.grid.grouping.api:ColumnDef
18413 * @description Show the aggregation menu on this column
18414 * <br/>Defaults to true.
18416 if ( col.colDef.groupingShowAggregationMenu !== false ){
18417 angular.forEach(uiGridTreeBaseService.nativeAggregations(), function(aggregationDef, name){
18418 addAggregationMenu(name);
18420 angular.forEach(gridOptions.treeCustomAggregations, function(aggregationDef, name){
18421 addAggregationMenu(name, aggregationDef.menuTitle);
18424 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregateRemove')) {
18425 col.menuItems.push(aggregateRemove);
18435 * @name groupingColumnProcessor
18436 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18437 * @description Moves the columns around based on which are grouped
18439 * @param {array} columns the columns to consider rendering
18440 * @param {array} rows the grid rows, which we don't use but are passed to us
18441 * @returns {array} updated columns array
18443 groupingColumnProcessor: function( columns, rows ) {
18446 columns = service.moveGroupColumns(this, columns, rows);
18452 * @name groupedFinalizerFn
18453 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18454 * @description Used on group columns to display the rendered value and optionally
18455 * display the count of rows.
18457 * @param {aggregation} the aggregation entity for a grouped column
18459 groupedFinalizerFn: function( aggregation ){
18462 if ( typeof(aggregation.groupVal) !== 'undefined') {
18463 aggregation.rendered = aggregation.groupVal;
18464 if ( col.grid.options.groupingShowCounts && col.colDef.type !== 'date' ){
18465 aggregation.rendered += (' (' + aggregation.value + ')');
18468 aggregation.rendered = null;
18474 * @name moveGroupColumns
18475 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18476 * @description Moves the column order so that the grouped columns are lined up
18477 * to the left (well, unless you're RTL, then it's the right). By doing this in
18478 * the columnsProcessor, we make it transient - when the column is ungrouped it'll
18479 * go back to where it was.
18481 * Does nothing if the option `moveGroupColumns` is set to false.
18483 * @param {Grid} grid grid object
18484 * @param {array} columns the columns that we should process/move
18485 * @param {array} rows the grid rows
18486 * @returns {array} updated columns
18488 moveGroupColumns: function( grid, columns, rows ){
18489 if ( grid.options.moveGroupColumns === false){
18493 columns.forEach( function(column, index){
18494 // position used to make stable sort in moveGroupColumns
18495 column.groupingPosition = index;
18498 columns.sort(function(a, b){
18499 var a_group, b_group;
18500 if (a.isRowHeader){
18503 else if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){
18506 a_group = a.grouping.groupPriority;
18509 if (b.isRowHeader){
18512 else if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){
18515 b_group = b.grouping.groupPriority;
18518 // groups get sorted to the top
18519 if ( a_group !== null && b_group === null) { return -1; }
18520 if ( b_group !== null && a_group === null) { return 1; }
18521 if ( a_group !== null && b_group !== null) {return a_group - b_group; }
18523 return a.groupingPosition - b.groupingPosition;
18526 columns.forEach( function(column, index) {
18527 delete column.groupingPosition;
18536 * @name groupColumn
18537 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18538 * @description Adds this column to the existing grouping, at the end of the priority order.
18539 * If the column doesn't have a sort, adds one, by default ASC
18541 * This column will move to the left of any non-group columns, the
18542 * move is handled in a columnProcessor, so gets called as part of refresh
18544 * @param {Grid} grid grid object
18545 * @param {GridCol} column the column we want to group
18547 groupColumn: function( grid, column){
18548 if ( typeof(column.grouping) === 'undefined' ){
18549 column.grouping = {};
18552 // set the group priority to the next number in the hierarchy
18553 var existingGrouping = service.getGrouping( grid );
18554 column.grouping.groupPriority = existingGrouping.grouping.length;
18556 // add sort if not present
18557 if ( !column.sort ){
18558 column.sort = { direction: uiGridConstants.ASC };
18559 } else if ( typeof(column.sort.direction) === 'undefined' || column.sort.direction === null ){
18560 column.sort.direction = uiGridConstants.ASC;
18563 column.treeAggregation = { type: uiGridGroupingConstants.aggregation.COUNT, source: 'grouping' };
18564 column.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
18565 column.treeAggregationFinalizerFn = service.groupedFinalizerFn;
18567 grid.api.grouping.raise.groupingChanged(column);
18568 // This indirectly calls service.tidyPriorities( grid );
18569 grid.api.core.raise.sortChanged(grid, grid.getColumnSorting());
18571 grid.queueGridRefresh();
18577 * @name ungroupColumn
18578 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18579 * @description Removes the groupPriority from this column. If the
18580 * column was previously aggregated the aggregation will come back.
18581 * The sort will remain.
18583 * This column will move to the right of any other group columns, the
18584 * move is handled in a columnProcessor, so gets called as part of refresh
18586 * @param {Grid} grid grid object
18587 * @param {GridCol} column the column we want to ungroup
18589 ungroupColumn: function( grid, column){
18590 if ( typeof(column.grouping) === 'undefined' ){
18594 delete column.grouping.groupPriority;
18595 delete column.treeAggregation;
18596 delete column.customTreeAggregationFinalizer;
18598 service.tidyPriorities( grid );
18600 grid.api.grouping.raise.groupingChanged(column);
18602 grid.queueGridRefresh();
18607 * @name aggregateColumn
18608 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18609 * @description Sets the aggregation type on a column, if the
18610 * column is currently grouped then it removes the grouping first.
18612 * @param {Grid} grid grid object
18613 * @param {GridCol} column the column we want to aggregate
18614 * @param {string} one of the recognised types from uiGridGroupingConstants or one of the custom aggregations from gridOptions
18616 aggregateColumn: function( grid, column, aggregationType){
18618 if (typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
18619 service.ungroupColumn( grid, column );
18622 var aggregationDef = {};
18623 if ( typeof(grid.options.treeCustomAggregations[aggregationType]) !== 'undefined' ){
18624 aggregationDef = grid.options.treeCustomAggregations[aggregationType];
18625 } else if ( typeof(uiGridTreeBaseService.nativeAggregations()[aggregationType]) !== 'undefined' ){
18626 aggregationDef = uiGridTreeBaseService.nativeAggregations()[aggregationType];
18629 column.treeAggregation = { type: aggregationType, label: i18nService.get().aggregation[aggregationDef.label] || aggregationDef.label };
18630 column.treeAggregationFn = aggregationDef.aggregationFn;
18631 column.treeAggregationFinalizerFn = aggregationDef.finalizerFn;
18633 grid.api.grouping.raise.aggregationChanged(column);
18635 grid.queueGridRefresh();
18641 * @name setGrouping
18642 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18643 * @description Set the grouping based on a config object, used by the save state feature
18644 * (more specifically, by the restore function in that feature )
18646 * @param {Grid} grid grid object
18647 * @param {object} config the config we want to set, same format as that returned by getGrouping
18649 setGrouping: function ( grid, config ){
18650 if ( typeof(config) === 'undefined' ){
18654 // first remove any existing grouping
18655 service.clearGrouping(grid);
18657 if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){
18658 config.grouping.forEach( function( group ) {
18659 var col = grid.getColumn(group.colName);
18662 service.groupColumn( grid, col );
18667 if ( config.aggregations && config.aggregations.length ){
18668 config.aggregations.forEach( function( aggregation ) {
18669 var col = grid.getColumn(aggregation.colName);
18672 service.aggregateColumn( grid, col, aggregation.aggregation.type );
18677 if ( config.rowExpandedStates ){
18678 service.applyRowExpandedStates( grid.grouping.groupingHeaderCache, config.rowExpandedStates );
18685 * @name clearGrouping
18686 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18687 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
18688 * as we don't know whether that sorting was added by grouping or was there beforehand
18690 * @param {Grid} grid grid object
18692 clearGrouping: function( grid ) {
18693 var currentGrouping = service.getGrouping(grid);
18695 if ( currentGrouping.grouping.length > 0 ){
18696 currentGrouping.grouping.forEach( function( group ) {
18698 // should have a group.colName if there's no col
18699 group.col = grid.getColumn(group.colName);
18701 service.ungroupColumn(grid, group.col);
18705 if ( currentGrouping.aggregations.length > 0 ){
18706 currentGrouping.aggregations.forEach( function( aggregation ){
18707 if (!aggregation.col){
18708 // should have a group.colName if there's no col
18709 aggregation.col = grid.getColumn(aggregation.colName);
18711 service.aggregateColumn(grid, aggregation.col, null);
18719 * @name tidyPriorities
18720 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18721 * @description Renumbers groupPriority and sortPriority such that
18722 * groupPriority is contiguous, and sortPriority either matches
18723 * groupPriority (for group columns), and otherwise is contiguous and
18724 * higher than groupPriority.
18726 * @param {Grid} grid grid object
18728 tidyPriorities: function( grid ){
18729 // if we're called from sortChanged, grid is in this, not passed as param, the param can be a column or undefined
18730 if ( ( typeof(grid) === 'undefined' || typeof(grid.grid) !== 'undefined' ) && typeof(this.grid) !== 'undefined' ) {
18734 var groupArray = [];
18735 var sortArray = [];
18737 grid.columns.forEach( function(column, index){
18738 if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
18739 groupArray.push(column);
18740 } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0){
18741 sortArray.push(column);
18745 groupArray.sort(function(a, b){ return a.grouping.groupPriority - b.grouping.groupPriority; });
18746 groupArray.forEach( function(column, index){
18747 column.grouping.groupPriority = index;
18748 column.suppressRemoveSort = true;
18749 if ( typeof(column.sort) === 'undefined'){
18752 column.sort.priority = index;
18755 var i = groupArray.length;
18756 sortArray.sort(function(a, b){ return a.sort.priority - b.sort.priority; });
18757 sortArray.forEach( function(column, index){
18758 column.sort.priority = i;
18759 column.suppressRemoveSort = column.colDef.suppressRemoveSort;
18768 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18769 * @description The rowProcessor that creates the groupHeaders (i.e. does
18770 * the actual grouping).
18772 * Assumes it is always called after the sorting processor, guaranteed by the priority setting
18774 * Processes all the rows in order, inserting a groupHeader row whenever there is a change
18775 * in value of a grouped row, based on the sortAlgorithm used for the column. The group header row
18776 * is looked up in the groupHeaderCache, and used from there if there is one. The entity is reset
18777 * to {} if one is found.
18779 * As it processes it maintains a `processingState` array. This records, for each level of grouping we're
18780 * working with, the following information:
18785 * initialised: boolean,
18786 * currentValue: value,
18787 * currentRow: gridRow,
18790 * We look for changes in the currentValue at any of the levels. Where we find a change we:
18792 * - create a new groupHeader row in the array
18794 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
18795 * @returns {array} the updated rows, including our new group rows
18797 groupRows: function( renderableRows ) {
18798 if (renderableRows.length === 0){
18799 return renderableRows;
18803 grid.grouping.oldGroupingHeaderCache = grid.grouping.groupingHeaderCache || {};
18804 grid.grouping.groupingHeaderCache = {};
18806 var processingState = service.initialiseProcessingState( grid );
18808 // processes each of the fields we are grouping by, checks if the value has changed and inserts a groupHeader
18809 // Broken out as shouldn't create functions in a loop.
18810 var updateProcessingState = function( groupFieldState, stateIndex ) {
18811 var fieldValue = grid.getCellValue(row, groupFieldState.col);
18813 // look for change of value - and insert a header
18814 if ( !groupFieldState.initialised || rowSorter.getSortFn(grid, groupFieldState.col, renderableRows)(fieldValue, groupFieldState.currentValue) !== 0 ){
18815 service.insertGroupHeader( grid, renderableRows, i, processingState, stateIndex );
18820 // use a for loop because it's tolerant of the array length changing whilst we go - we can
18821 // manipulate the iterator when we insert groupHeader rows
18822 for (var i = 0; i < renderableRows.length; i++ ){
18823 var row = renderableRows[i];
18825 if ( row.visible ){
18826 processingState.forEach( updateProcessingState );
18830 delete grid.grouping.oldGroupingHeaderCache;
18831 return renderableRows;
18837 * @name initialiseProcessingState
18838 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18839 * @description Creates the processing state array that is used
18842 * @param {Grid} grid grid object
18843 * @returns {array} an array in the format described in the groupRows method,
18844 * initialised with blank values
18846 initialiseProcessingState: function( grid ){
18847 var processingState = [];
18848 var columnSettings = service.getGrouping( grid );
18850 columnSettings.grouping.forEach( function( groupItem, index){
18851 processingState.push({
18852 fieldName: groupItem.field,
18853 col: groupItem.col,
18854 initialised: false,
18855 currentValue: null,
18860 return processingState;
18866 * @name getGrouping
18867 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18868 * @description Get the grouping settings from the columns. As a side effect
18869 * this always renumbers the grouping starting at 0
18870 * @param {Grid} grid grid object
18871 * @returns {array} an array of the group fields, in order of priority
18873 getGrouping: function( grid ){
18874 var groupArray = [];
18875 var aggregateArray = [];
18877 // get all the grouping
18878 grid.columns.forEach( function(column, columnIndex){
18879 if ( column.grouping ){
18880 if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
18881 groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping });
18884 if ( column.treeAggregation && column.treeAggregation.type ){
18885 aggregateArray.push({ field: column.field, col: column, aggregation: column.treeAggregation });
18889 // sort grouping into priority order
18890 groupArray.sort( function(a, b){
18891 return a.groupPriority - b.groupPriority;
18894 // renumber the priority in case it was somewhat messed up, then remove the grouping reference
18895 groupArray.forEach( function( group, index) {
18896 group.grouping.groupPriority = index;
18897 group.groupPriority = index;
18898 delete group.grouping;
18901 return { grouping: groupArray, aggregations: aggregateArray };
18907 * @name insertGroupHeader
18908 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18909 * @description Create a group header row, and link it to the various configuration
18910 * items that we use.
18912 * Look for the row in the oldGroupingHeaderCache, write the row into the new groupingHeaderCache.
18914 * @param {Grid} grid grid object
18915 * @param {array} renderableRows the rows that we are processing
18916 * @param {number} rowIndex the row we were up to processing
18917 * @param {array} processingState the current processing state
18918 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header -
18919 * i.e. the column that we want to create a header for
18921 insertGroupHeader: function( grid, renderableRows, rowIndex, processingState, stateIndex ) {
18922 // set the value that caused the end of a group into the header row and the processing state
18923 var fieldName = processingState[stateIndex].fieldName;
18924 var col = processingState[stateIndex].col;
18926 var newValue = grid.getCellValue(renderableRows[rowIndex], col);
18927 var newDisplayValue = newValue;
18928 if ( typeof(newValue) === 'undefined' || newValue === null ) {
18929 newDisplayValue = grid.options.groupingNullLabel;
18932 var cacheItem = grid.grouping.oldGroupingHeaderCache;
18933 for ( var i = 0; i < stateIndex; i++ ){
18934 if ( cacheItem && cacheItem[processingState[i].currentValue] ){
18935 cacheItem = cacheItem[processingState[i].currentValue].children;
18940 if ( cacheItem && cacheItem[newValue]){
18941 headerRow = cacheItem[newValue].row;
18942 headerRow.entity = {};
18944 headerRow = new GridRow( {}, null, grid );
18945 gridClassFactory.rowTemplateAssigner.call(grid, headerRow);
18948 headerRow.entity['$$' + processingState[stateIndex].col.uid] = { groupVal: newDisplayValue };
18949 headerRow.treeLevel = stateIndex;
18950 headerRow.groupHeader = true;
18951 headerRow.internalRow = true;
18952 headerRow.enableCellEdit = false;
18953 headerRow.enableSelection = grid.options.enableGroupHeaderSelection;
18954 processingState[stateIndex].initialised = true;
18955 processingState[stateIndex].currentValue = newValue;
18956 processingState[stateIndex].currentRow = headerRow;
18958 // set all processing states below this one to not be initialised - change of this state
18959 // means all those need to start again
18960 service.finaliseProcessingState( processingState, stateIndex + 1);
18962 // insert our new header row
18963 renderableRows.splice(rowIndex, 0, headerRow);
18965 // add our new header row to the cache
18966 cacheItem = grid.grouping.groupingHeaderCache;
18967 for ( i = 0; i < stateIndex; i++ ){
18968 cacheItem = cacheItem[processingState[i].currentValue].children;
18970 cacheItem[newValue] = { row: headerRow, children: {} };
18976 * @name finaliseProcessingState
18977 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18978 * @description Set all processing states lower than the one that had a break in value to
18979 * no longer be initialised. Render the counts into the entity ready for display.
18981 * @param {Grid} grid grid object
18982 * @param {array} processingState the current processing state
18983 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header, all
18984 * processing states after this need to be finalised
18986 finaliseProcessingState: function( processingState, stateIndex ){
18987 for ( var i = stateIndex; i < processingState.length; i++){
18988 processingState[i].initialised = false;
18989 processingState[i].currentRow = null;
18990 processingState[i].currentValue = null;
18997 * @name getRowExpandedStates
18998 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18999 * @description Extract the groupHeaderCache hash, pulling out only the states.
19001 * The example below shows a grid that is grouped by gender then age
19006 * state: 'expanded',
19008 * 22: { state: 'expanded' },
19009 * 30: { state: 'collapsed' }
19013 * state: 'expanded',
19015 * 28: { state: 'expanded' },
19016 * 55: { state: 'collapsed' }
19022 * @param {Grid} grid grid object
19023 * @returns {hash} the expanded states as a hash
19025 getRowExpandedStates: function(treeChildren){
19026 if ( typeof(treeChildren) === 'undefined' ){
19030 var newChildren = {};
19032 angular.forEach( treeChildren, function( value, key ){
19033 newChildren[key] = { state: value.row.treeNode.state };
19034 if ( value.children ){
19035 newChildren[key].children = service.getRowExpandedStates( value.children );
19037 newChildren[key].children = {};
19041 return newChildren;
19047 * @name applyRowExpandedStates
19048 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19049 * @description Take a hash in the format as created by getRowExpandedStates,
19050 * and apply it to the grid.grouping.groupHeaderCache.
19052 * Takes a treeSubset, and applies to a treeSubset - so can be called
19055 * @param {object} currentNode can be grid.grouping.groupHeaderCache, or any of
19056 * the children of that hash
19057 * @returns {hash} expandedStates can be the full expanded states, or children
19058 * of that expanded states (which hopefully matches the subset of the groupHeaderCache)
19060 applyRowExpandedStates: function( currentNode, expandedStates ){
19061 if ( typeof(expandedStates) === 'undefined' ){
19065 angular.forEach(expandedStates, function( value, key ) {
19066 if ( currentNode[key] ){
19067 currentNode[key].row.treeNode.state = value.state;
19069 if (value.children && currentNode[key].children){
19070 service.applyRowExpandedStates( currentNode[key].children, value.children );
19086 * @name ui.grid.grouping.directive:uiGridGrouping
19090 * @description Adds grouping features to grid
19093 <example module="app">
19094 <file name="app.js">
19095 var app = angular.module('app', ['ui.grid', 'ui.grid.grouping']);
19097 app.controller('MainCtrl', ['$scope', function ($scope) {
19099 { name: 'Bob', title: 'CEO' },
19100 { name: 'Frank', title: 'Lowly Developer' }
19103 $scope.columnDefs = [
19104 {name: 'name', enableCellEdit: true},
19105 {name: 'title', enableCellEdit: true}
19108 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
19111 <file name="index.html">
19112 <div ng-controller="MainCtrl">
19113 <div ui-grid="gridOptions" ui-grid-grouping></div>
19118 module.directive('uiGridGrouping', ['uiGridGroupingConstants', 'uiGridGroupingService', '$templateCache',
19119 function (uiGridGroupingConstants, uiGridGroupingService, $templateCache) {
19123 require: '^uiGrid',
19125 compile: function () {
19127 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
19128 if (uiGridCtrl.grid.options.enableGrouping !== false){
19129 uiGridGroupingService.initializeGrid(uiGridCtrl.grid, $scope);
19132 post: function ($scope, $elm, $attrs, uiGridCtrl) {
19146 * @name ui.grid.importer
19149 * # ui.grid.importer
19151 * <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>
19153 * This module provides the ability to import data into the grid. It
19154 * uses the column defs to work out which data belongs in which column,
19155 * and creates entities from a configured class (typically a $resource).
19157 * If the rowEdit feature is enabled, it also calls save on those newly
19158 * created objects, and then displays any errors in the imported data.
19160 * Currently the importer imports only CSV and json files, although provision has been
19161 * made to process other file formats, and these can be added over time.
19163 * For json files, the properties within each object in the json must match the column names
19164 * (to put it another way, the importer doesn't process the json, it just copies the objects
19165 * within the json into a new instance of the specified object type)
19167 * For CSV import, the default column identification relies on each column in the
19168 * header row matching a column.name or column.displayName. Optionally, a column identification
19169 * callback can be used. This allows matching using other attributes, which is particularly
19170 * useful if your application has internationalised column headings (i.e. the headings that
19171 * the user sees don't match the column names).
19173 * The importer makes use of the grid menu as the UI for requesting an
19176 * <div ui-grid-importer></div>
19179 var module = angular.module('ui.grid.importer', ['ui.grid']);
19183 * @name ui.grid.importer.constant:uiGridImporterConstants
19185 * @description constants available in importer module
19188 module.constant('uiGridImporterConstants', {
19189 featureName: 'importer'
19194 * @name ui.grid.importer.service:uiGridImporterService
19196 * @description Services for importer feature
19198 module.service('uiGridImporterService', ['$q', 'uiGridConstants', 'uiGridImporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService', '$window',
19199 function ($q, uiGridConstants, uiGridImporterConstants, gridUtil, $compile, $interval, i18nService, $window) {
19203 initializeGrid: function ($scope, grid) {
19205 //add feature namespace and any properties to grid for needed state
19210 this.defaultGridOptions(grid.options);
19214 * @name ui.grid.importer.api:PublicApi
19216 * @description Public Api for importer feature
19228 * @methodOf ui.grid.importer.api:PublicApi
19229 * @description Imports a file into the grid using the file object
19230 * provided. Bypasses the grid menu
19231 * @param {File} fileObject the file we want to import, as a javascript
19234 importFile: function ( fileObject ) {
19235 service.importThisFile( grid, fileObject );
19241 grid.api.registerEventsFromObject(publicApi.events);
19243 grid.api.registerMethodsFromObject(publicApi.methods);
19245 if ( grid.options.enableImporter && grid.options.importerShowMenu ){
19246 if ( grid.api.core.addToGridMenu ){
19247 service.addToMenu( grid );
19249 // order of registration is not guaranteed, register in a little while
19250 $interval( function() {
19251 if (grid.api.core.addToGridMenu){
19252 service.addToMenu( grid );
19260 defaultGridOptions: function (gridOptions) {
19261 //default option to true unless it was explicitly set to false
19264 * @name ui.grid.importer.api:GridOptions
19266 * @description GridOptions for importer feature, these are available to be
19267 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
19272 * @propertyOf ui.grid.importer.api:GridOptions
19273 * @name enableImporter
19274 * @description Whether or not importer is enabled. Automatically set
19275 * to false if the user's browser does not support the required fileApi.
19276 * Otherwise defaults to true.
19279 if (gridOptions.enableImporter || gridOptions.enableImporter === undefined) {
19280 if ( !($window.hasOwnProperty('File') && $window.hasOwnProperty('FileReader') && $window.hasOwnProperty('FileList') && $window.hasOwnProperty('Blob')) ) {
19281 gridUtil.logError('The File APIs are not fully supported in this browser, grid importer cannot be used.');
19282 gridOptions.enableImporter = false;
19284 gridOptions.enableImporter = true;
19287 gridOptions.enableImporter = false;
19292 * @name importerProcessHeaders
19293 * @methodOf ui.grid.importer.api:GridOptions
19294 * @description A callback function that will process headers using custom
19295 * logic. Set this callback function if the headers that your user will provide in their
19296 * import file don't necessarily match the grid header or field names. This might commonly
19297 * occur where your application is internationalised, and therefore the field names
19298 * that the user recognises are in a different language than the field names that
19299 * ui-grid knows about.
19301 * Defaults to the internal `processHeaders` method, which seeks to match using both
19302 * displayName and column.name. Any non-matching columns are discarded.
19304 * Your callback routine should respond by processing the header array, and returning an array
19305 * of matching column names. A null value in any given position means "don't import this column"
19308 * gridOptions.importerProcessHeaders: function( headerArray ) {
19309 * var myHeaderColumns = [];
19311 * headerArray.forEach( function( value, index ) {
19312 * thisCol = mySpecialLookupFunction( value );
19313 * myHeaderColumns.push( thisCol.name );
19316 * return myHeaderCols;
19319 * @param {Grid} grid the grid we're importing into
19320 * @param {array} headerArray an array of the text from the first row of the csv file,
19321 * which you need to match to column.names
19322 * @returns {array} array of matching column names, in the same order as the headerArray
19325 gridOptions.importerProcessHeaders = gridOptions.importerProcessHeaders || service.processHeaders;
19329 * @name importerHeaderFilter
19330 * @methodOf ui.grid.importer.api:GridOptions
19331 * @description A callback function that will filter (usually translate) a single
19332 * header. Used when you want to match the passed in column names to the column
19333 * displayName after the header filter.
19335 * Your callback routine needs to return the filtered header value.
19337 * gridOptions.importerHeaderFilter: function( displayName ) {
19338 * return $translate.instant( displayName );
19344 * gridOptions.importerHeaderFilter: $translate.instant
19346 * @param {string} displayName the displayName that we'd like to translate
19347 * @returns {string} the translated name
19350 gridOptions.importerHeaderFilter = gridOptions.importerHeaderFilter || function( displayName ) { return displayName; };
19354 * @name importerErrorCallback
19355 * @methodOf ui.grid.importer.api:GridOptions
19356 * @description A callback function that provides custom error handling, rather
19357 * than the standard grid behaviour of an alert box and a console message. You
19358 * might use this to internationalise the console log messages, or to write to a
19359 * custom logging routine that returned errors to the server.
19362 * gridOptions.importerErrorCallback: function( grid, errorKey, consoleMessage, context ) {
19363 * myUserDisplayRoutine( errorKey );
19364 * myLoggingRoutine( consoleMessage, context );
19367 * @param {Grid} grid the grid we're importing into, may be useful if you're positioning messages
19369 * @param {string} errorKey one of the i18n keys the importer can return - importer.noHeaders,
19370 * importer.noObjects, importer.invalidCsv, importer.invalidJson, importer.jsonNotArray
19371 * @param {string} consoleMessage the English console message that importer would have written
19372 * @param {object} context the context data that importer would have appended to that console message,
19373 * often the file content itself or the element that is in error
19376 if ( !gridOptions.importerErrorCallback || typeof(gridOptions.importerErrorCallback) !== 'function' ){
19377 delete gridOptions.importerErrorCallback;
19382 * @name importerDataAddCallback
19383 * @methodOf ui.grid.importer.api:GridOptions
19384 * @description A mandatory callback function that adds data to the source data array. The grid
19385 * generally doesn't add rows to the source data array, it is tidier to handle this through a user
19389 * gridOptions.importerDataAddCallback: function( grid, newObjects ) {
19390 * $scope.myData = $scope.myData.concat( newObjects );
19393 * @param {Grid} grid the grid we're importing into, may be useful in some way
19394 * @param {array} newObjects an array of new objects that you should add to your data
19397 if ( gridOptions.enableImporter === true && !gridOptions.importerDataAddCallback ) {
19398 gridUtil.logError("You have not set an importerDataAddCallback, importer is disabled");
19399 gridOptions.enableImporter = false;
19404 * @name importerNewObject
19405 * @propertyOf ui.grid.importer.api:GridOptions
19406 * @description An object on which we call `new` to create each new row before inserting it into
19407 * the data array. Typically this would be a $resource entity, which means that if you're using
19408 * the rowEdit feature, you can directly call save on this entity when the save event is triggered.
19410 * Defaults to a vanilla javascript object
19414 * gridOptions.importerNewObject = MyRes;
19421 * @propertyOf ui.grid.importer.api:GridOptions
19422 * @name importerShowMenu
19423 * @description Whether or not to show an item in the grid menu. Defaults to true.
19426 gridOptions.importerShowMenu = gridOptions.importerShowMenu !== false;
19430 * @methodOf ui.grid.importer.api:GridOptions
19431 * @name importerObjectCallback
19432 * @description A callback that massages the data for each object. For example,
19433 * you might have data stored as a code value, but display the decode. This callback
19434 * can be used to change the decoded value back into a code. Defaults to doing nothing.
19435 * @param {Grid} grid in case you need it
19436 * @param {object} newObject the new object as importer has created it, modify it
19437 * then return the modified version
19438 * @returns {object} the modified object
19441 * gridOptions.importerObjectCallback = function ( grid, newObject ) {
19442 * switch newObject.status {
19444 * newObject.status = 1;
19447 * newObject.status = 2;
19450 * return newObject;
19454 gridOptions.importerObjectCallback = gridOptions.importerObjectCallback || function( grid, newObject ) { return newObject; };
19461 * @methodOf ui.grid.importer.service:uiGridImporterService
19462 * @description Adds import menu item to the grid menu,
19463 * allowing the user to request import of a file
19464 * @param {Grid} grid the grid into which data should be imported
19466 addToMenu: function ( grid ) {
19467 grid.api.core.addToGridMenu( grid, [
19469 title: i18nService.getSafeText('gridMenu.importerTitle'),
19473 templateUrl: 'ui-grid/importerMenuItemContainer',
19474 action: function ($event) {
19475 this.grid.api.importer.importAFile( grid );
19485 * @name importThisFile
19486 * @methodOf ui.grid.importer.service:uiGridImporterService
19487 * @description Imports the provided file into the grid using the file object
19488 * provided. Bypasses the grid menu
19489 * @param {Grid} grid the grid we're importing into
19490 * @param {File} fileObject the file we want to import, as returned from the File
19491 * javascript object
19493 importThisFile: function ( grid, fileObject ) {
19495 gridUtil.logError( 'No file object provided to importThisFile, should be impossible, aborting');
19499 var reader = new FileReader();
19501 switch ( fileObject.type ){
19502 case 'application/json':
19503 reader.onload = service.importJsonClosure( grid );
19506 reader.onload = service.importCsvClosure( grid );
19510 reader.readAsText( fileObject );
19517 * @methodOf ui.grid.importer.service:uiGridImporterService
19518 * @description Creates a function that imports a json file into the grid.
19519 * The json data is imported into new objects of type `gridOptions.importerNewObject`,
19520 * and if the rowEdit feature is enabled the rows are marked as dirty
19521 * @param {Grid} grid the grid we want to import into
19522 * @param {FileObject} importFile the file that we want to import, as
19525 importJsonClosure: function( grid ) {
19526 return function( importFile ){
19527 var newObjects = [];
19530 var importArray = service.parseJson( grid, importFile );
19531 if (importArray === null){
19534 importArray.forEach( function( value, index ) {
19535 newObject = service.newObject( grid );
19536 angular.extend( newObject, value );
19537 newObject = grid.options.importerObjectCallback( grid, newObject );
19538 newObjects.push( newObject );
19541 service.addObjects( grid, newObjects );
19550 * @methodOf ui.grid.importer.service:uiGridImporterService
19551 * @description Parses a json file, returns the parsed data.
19552 * Displays an error if file doesn't parse
19553 * @param {Grid} grid the grid that we want to import into
19554 * @param {FileObject} importFile the file that we want to import, as
19556 * @returns {array} array of objects from the imported json
19558 parseJson: function( grid, importFile ){
19561 loadedObjects = JSON.parse( importFile.target.result );
19563 service.alertError( grid, 'importer.invalidJson', 'File could not be processed, is it valid json? Content was: ', importFile.target.result );
19567 if ( !Array.isArray( loadedObjects ) ){
19568 service.alertError( grid, 'importer.jsonNotarray', 'Import failed, file is not an array, file was: ', importFile.target.result );
19571 return loadedObjects;
19579 * @name importCsvClosure
19580 * @methodOf ui.grid.importer.service:uiGridImporterService
19581 * @description Creates a function that imports a csv file into the grid
19582 * (allowing it to be used in the reader.onload event)
19583 * @param {Grid} grid the grid that we want to import into
19584 * @param {FileObject} importFile the file that we want to import, as
19587 importCsvClosure: function( grid ) {
19588 return function( importFile ){
19589 var importArray = service.parseCsv( importFile );
19590 if ( !importArray || importArray.length < 1 ){
19591 service.alertError( grid, 'importer.invalidCsv', 'File could not be processed, is it valid csv? Content was: ', importFile.target.result );
19595 var newObjects = service.createCsvObjects( grid, importArray );
19596 if ( !newObjects || newObjects.length === 0 ){
19597 service.alertError( grid, 'importer.noObjects', 'Objects were not able to be derived, content was: ', importFile.target.result );
19601 service.addObjects( grid, newObjects );
19609 * @methodOf ui.grid.importer.service:uiGridImporterService
19610 * @description Parses a csv file into an array of arrays, with the first
19611 * array being the headers, and the remaining arrays being the data.
19612 * The logic for this comes from https://github.com/thetalecrafter/excel.js/blob/master/src/csv.js,
19613 * which is noted as being under the MIT license. The code is modified to pass the jscs yoda condition
19615 * @param {FileObject} importFile the file that we want to import, as a
19618 parseCsv: function( importFile ) {
19619 var csv = importFile.target.result;
19621 // use the CSV-JS library to parse
19622 return CSV.parse(csv);
19628 * @name createCsvObjects
19629 * @methodOf ui.grid.importer.service:uiGridImporterService
19630 * @description Converts an array of arrays (representing the csv file)
19631 * into a set of objects. Uses the provided `gridOptions.importerNewObject`
19632 * to create the objects, and maps the header row into the individual columns
19633 * using either `gridOptions.importerProcessHeaders`, or by using a native method
19634 * of matching to either the displayName, column name or column field of
19635 * the columns in the column defs. The resulting objects will have attributes
19636 * that are named based on the column.field or column.name, in that order.
19637 * @param {Grid} grid the grid that we want to import into
19638 * @param {Array} importArray the data that we want to import, as an array
19640 createCsvObjects: function( grid, importArray ){
19641 // pull off header row and turn into headers
19642 var headerMapping = grid.options.importerProcessHeaders( grid, importArray.shift() );
19643 if ( !headerMapping || headerMapping.length === 0 ){
19644 service.alertError( grid, 'importer.noHeaders', 'Column names could not be derived, content was: ', importArray );
19648 var newObjects = [];
19650 importArray.forEach( function( row, index ) {
19651 newObject = service.newObject( grid );
19652 if ( row !== null ){
19653 row.forEach( function( field, index ){
19654 if ( headerMapping[index] !== null ){
19655 newObject[ headerMapping[index] ] = field;
19659 newObject = grid.options.importerObjectCallback( grid, newObject );
19660 newObjects.push( newObject );
19669 * @name processHeaders
19670 * @methodOf ui.grid.importer.service:uiGridImporterService
19671 * @description Determines the columns that the header row from
19672 * a csv (or other) file represents.
19673 * @param {Grid} grid the grid we're importing into
19674 * @param {array} headerRow the header row that we wish to match against
19675 * the column definitions
19676 * @returns {array} an array of the attribute names that should be used
19677 * for that column, based on matching the headers or creating the headers
19680 processHeaders: function( grid, headerRow ) {
19682 if ( !grid.options.columnDefs || grid.options.columnDefs.length === 0 ){
19683 // we are going to create new columnDefs for all these columns, so just remove
19684 // spaces from the names to create fields
19685 headerRow.forEach( function( value, index ) {
19686 headers.push( value.replace( /[^0-9a-zA-Z\-_]/g, '_' ) );
19690 var lookupHash = service.flattenColumnDefs( grid, grid.options.columnDefs );
19691 headerRow.forEach( function( value, index ) {
19692 if ( lookupHash[value] ) {
19693 headers.push( lookupHash[value] );
19694 } else if ( lookupHash[ value.toLowerCase() ] ) {
19695 headers.push( lookupHash[ value.toLowerCase() ] );
19697 headers.push( null );
19706 * @name flattenColumnDefs
19707 * @methodOf ui.grid.importer.service:uiGridImporterService
19708 * @description Runs through the column defs and creates a hash of
19709 * the displayName, name and field, and of each of those values forced to lower case,
19710 * with each pointing to the field or name
19711 * (whichever is present). Used to lookup column headers and decide what
19712 * attribute name to give to the resulting field.
19713 * @param {Grid} grid the grid we're importing into
19714 * @param {array} columnDefs the columnDefs that we should flatten
19715 * @returns {hash} the flattened version of the column def information, allowing
19716 * us to look up a value by `flattenedHash[ headerValue ]`
19718 flattenColumnDefs: function( grid, columnDefs ){
19719 var flattenedHash = {};
19720 columnDefs.forEach( function( columnDef, index) {
19721 if ( columnDef.name ){
19722 flattenedHash[ columnDef.name ] = columnDef.field || columnDef.name;
19723 flattenedHash[ columnDef.name.toLowerCase() ] = columnDef.field || columnDef.name;
19726 if ( columnDef.field ){
19727 flattenedHash[ columnDef.field ] = columnDef.field || columnDef.name;
19728 flattenedHash[ columnDef.field.toLowerCase() ] = columnDef.field || columnDef.name;
19731 if ( columnDef.displayName ){
19732 flattenedHash[ columnDef.displayName ] = columnDef.field || columnDef.name;
19733 flattenedHash[ columnDef.displayName.toLowerCase() ] = columnDef.field || columnDef.name;
19736 if ( columnDef.displayName && grid.options.importerHeaderFilter ){
19737 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName) ] = columnDef.field || columnDef.name;
19738 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName).toLowerCase() ] = columnDef.field || columnDef.name;
19742 return flattenedHash;
19749 * @methodOf ui.grid.importer.service:uiGridImporterService
19750 * @description Inserts our new objects into the grid data, and
19751 * sets the rows to dirty if the rowEdit feature is being used
19753 * Does this by registering a watch on dataChanges, which essentially
19754 * is waiting on the result of the grid data watch, and downstream processing.
19756 * When the callback is called, it deregisters itself - we don't want to run
19757 * again next time data is added.
19759 * If we never get called, we deregister on destroy.
19761 * @param {Grid} grid the grid we're importing into
19762 * @param {array} newObjects the objects we want to insert into the grid data
19763 * @returns {object} the new object
19765 addObjects: function( grid, newObjects, $scope ){
19766 if ( grid.api.rowEdit ){
19767 var dataChangeDereg = grid.registerDataChangeCallback( function() {
19768 grid.api.rowEdit.setRowsDirty( newObjects );
19770 }, [uiGridConstants.dataChange.ROW] );
19772 grid.importer.$scope.$on( '$destroy', dataChangeDereg );
19775 grid.importer.$scope.$apply( grid.options.importerDataAddCallback( grid, newObjects ) );
19783 * @methodOf ui.grid.importer.service:uiGridImporterService
19784 * @description Makes a new object based on `gridOptions.importerNewObject`,
19785 * or based on an empty object if not present
19786 * @param {Grid} grid the grid we're importing into
19787 * @returns {object} the new object
19789 newObject: function( grid ){
19790 if ( typeof(grid.options) !== "undefined" && typeof(grid.options.importerNewObject) !== "undefined" ){
19791 return new grid.options.importerNewObject();
19801 * @methodOf ui.grid.importer.service:uiGridImporterService
19802 * @description Provides an internationalised user alert for the failure,
19803 * and logs a console message including diagnostic content.
19804 * Optionally, if the the `gridOptions.importerErrorCallback` routine
19805 * is defined, then calls that instead, allowing user specified error routines
19806 * @param {Grid} grid the grid we're importing into
19807 * @param {array} headerRow the header row that we wish to match against
19808 * the column definitions
19810 alertError: function( grid, alertI18nToken, consoleMessage, context ){
19811 if ( grid.options.importerErrorCallback ){
19812 grid.options.importerErrorCallback( grid, alertI18nToken, consoleMessage, context );
19814 $window.alert(i18nService.getSafeText( alertI18nToken ));
19815 gridUtil.logError(consoleMessage + context );
19827 * @name ui.grid.importer.directive:uiGridImporter
19831 * @description Adds importer features to grid
19834 module.directive('uiGridImporter', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
19835 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
19839 require: '^uiGrid',
19841 link: function ($scope, $elm, $attrs, uiGridCtrl) {
19842 uiGridImporterService.initializeGrid($scope, uiGridCtrl.grid);
19850 * @name ui.grid.importer.directive:uiGridImporterMenuItem
19854 * @description Handles the processing from the importer menu item - once a file is
19858 module.directive('uiGridImporterMenuItem', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
19859 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
19863 require: '^uiGrid',
19865 templateUrl: 'ui-grid/importerMenuItem',
19866 link: function ($scope, $elm, $attrs, uiGridCtrl) {
19867 var handleFileSelect = function( event ){
19868 var target = event.srcElement || event.target;
19870 if (target && target.files && target.files.length === 1) {
19871 var fileObject = target.files[0];
19872 uiGridImporterService.importThisFile( grid, fileObject );
19873 target.form.reset();
19877 var fileChooser = $elm[0].querySelectorAll('.ui-grid-importer-file-chooser');
19878 var grid = uiGridCtrl.grid;
19880 if ( fileChooser.length !== 1 ){
19881 gridUtil.logError('Found > 1 or < 1 file choosers within the menu item, error, cannot continue');
19883 fileChooser[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
19895 * @name ui.grid.infiniteScroll
19899 * #ui.grid.infiniteScroll
19901 * <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>
19903 * This module provides infinite scroll functionality to ui-grid
19906 var module = angular.module('ui.grid.infiniteScroll', ['ui.grid']);
19909 * @name ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
19911 * @description Service for infinite scroll features
19913 module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', '$q', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent, $q) {
19919 * @name initializeGrid
19920 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
19921 * @description This method register events and methods into grid public API
19924 initializeGrid: function(grid, $scope) {
19925 service.defaultGridOptions(grid.options);
19927 if (!grid.options.enableInfiniteScroll){
19931 grid.infiniteScroll = { dataLoading: false };
19932 service.setScrollDirections( grid, grid.options.infiniteScrollUp, grid.options.infiniteScrollDown );
19933 grid.api.core.on.scrollEnd($scope, service.handleScroll);
19937 * @name ui.grid.infiniteScroll.api:PublicAPI
19939 * @description Public API for infinite scroll feature
19947 * @name needLoadMoreData
19948 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
19949 * @description This event fires when scroll reaches bottom percentage of grid
19950 * and needs to load data
19953 needLoadMoreData: function ($scope, fn) {
19958 * @name needLoadMoreDataTop
19959 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
19960 * @description This event fires when scroll reaches top percentage of grid
19961 * and needs to load data
19964 needLoadMoreDataTop: function ($scope, fn) {
19974 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
19975 * @description Call this function when you have loaded the additional data
19976 * requested. You should set scrollUp and scrollDown to indicate
19977 * whether there are still more pages in each direction.
19979 * If you call dataLoaded without first calling `saveScrollPercentage` then we will
19980 * scroll the user to the start of the newly loaded data, which usually gives a smooth scroll
19981 * experience, but can give a jumpy experience with large `infiniteScrollRowsFromEnd` values, and
19982 * on variable speed internet connections. Using `saveScrollPercentage` as demonstrated in the tutorial
19983 * should give a smoother scrolling experience for users.
19985 * See infinite_scroll tutorial for example of usage
19986 * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire
19987 * any more infinite scroll events upward
19988 * @param {boolean} scrollDown if set to false flags that there are no more pages downwards, so don't
19989 * fire any more infinite scroll events downward
19990 * @returns {promise} a promise that is resolved when the grid scrolling is fully adjusted. If you're
19991 * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning
19993 dataLoaded: function( scrollUp, scrollDown ) {
19994 service.setScrollDirections(grid, scrollUp, scrollDown);
19996 var promise = service.adjustScroll(grid).then(function() {
19997 grid.infiniteScroll.dataLoading = false;
20005 * @name resetScroll
20006 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20007 * @description Call this function when you have taken some action that makes the current
20008 * scroll position invalid. For example, if you're using external sorting and you've resorted
20009 * then you might reset the scroll, or if you've otherwise substantially changed the data, perhaps
20010 * you've reused an existing grid for a new data set
20012 * You must tell us whether there is data upwards or downwards after the reset
20014 * @param {boolean} scrollUp flag that there are pages upwards, fire
20015 * infinite scroll events upward
20016 * @param {boolean} scrollDown flag that there are pages downwards, so
20017 * fire infinite scroll events downward
20018 * @returns {promise} promise that is resolved when the scroll reset is complete
20020 resetScroll: function( scrollUp, scrollDown ) {
20021 service.setScrollDirections( grid, scrollUp, scrollDown);
20023 return service.adjustInfiniteScrollPosition(grid, 0);
20029 * @name saveScrollPercentage
20030 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20031 * @description Saves the scroll percentage and number of visible rows before you adjust the data,
20032 * used if you're subsequently going to call `dataRemovedTop` or `dataRemovedBottom`
20034 saveScrollPercentage: function() {
20035 grid.infiniteScroll.prevScrollTop = grid.renderContainers.body.prevScrollTop;
20036 grid.infiniteScroll.previousVisibleRows = grid.getVisibleRowCount();
20042 * @name dataRemovedTop
20043 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20044 * @description Adjusts the scroll position after you've removed data at the top
20045 * @param {boolean} scrollUp flag that there are pages upwards, fire
20046 * infinite scroll events upward
20047 * @param {boolean} scrollDown flag that there are pages downwards, so
20048 * fire infinite scroll events downward
20050 dataRemovedTop: function( scrollUp, scrollDown ) {
20051 service.dataRemovedTop( grid, scrollUp, scrollDown );
20056 * @name dataRemovedBottom
20057 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20058 * @description Adjusts the scroll position after you've removed data at the bottom
20059 * @param {boolean} scrollUp flag that there are pages upwards, fire
20060 * infinite scroll events upward
20061 * @param {boolean} scrollDown flag that there are pages downwards, so
20062 * fire infinite scroll events downward
20064 dataRemovedBottom: function( scrollUp, scrollDown ) {
20065 service.dataRemovedBottom( grid, scrollUp, scrollDown );
20070 * @name setScrollDirections
20071 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20072 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20073 * and also sets the grid.suppressParentScroll
20074 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20075 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20077 setScrollDirections: function ( scrollUp, scrollDown ) {
20078 service.setScrollDirections( grid, scrollUp, scrollDown );
20084 grid.api.registerEventsFromObject(publicApi.events);
20085 grid.api.registerMethodsFromObject(publicApi.methods);
20089 defaultGridOptions: function (gridOptions) {
20090 //default option to true unless it was explicitly set to false
20093 * @name ui.grid.infiniteScroll.api:GridOptions
20095 * @description GridOptions for infinite scroll feature, these are available to be
20096 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
20101 * @name enableInfiniteScroll
20102 * @propertyOf ui.grid.infiniteScroll.api:GridOptions
20103 * @description Enable infinite scrolling for this grid
20104 * <br/>Defaults to true
20106 gridOptions.enableInfiniteScroll = gridOptions.enableInfiniteScroll !== false;
20110 * @name infiniteScrollRowsFromEnd
20111 * @propertyOf ui.grid.class:GridOptions
20112 * @description This setting controls how close to the end of the dataset a user gets before
20113 * more data is requested by the infinite scroll, whether scrolling up or down. This allows you to
20114 * 'prefetch' rows before the user actually runs out of scrolling.
20116 * Note that if you set this value too high it may give jumpy scrolling behaviour, if you're getting
20117 * this behaviour you could use the `saveScrollPercentageMethod` right before loading your data, and we'll
20118 * preserve that scroll position
20120 * <br> Defaults to 20
20122 gridOptions.infiniteScrollRowsFromEnd = gridOptions.infiniteScrollRowsFromEnd || 20;
20126 * @name infiniteScrollUp
20127 * @propertyOf ui.grid.class:GridOptions
20128 * @description Whether you allow infinite scroll up, implying that the first page of data
20129 * you have displayed is in the middle of your data set. If set to true then we trigger the
20130 * needMoreDataTop event when the user hits the top of the scrollbar.
20131 * <br> Defaults to false
20133 gridOptions.infiniteScrollUp = gridOptions.infiniteScrollUp === true;
20137 * @name infiniteScrollDown
20138 * @propertyOf ui.grid.class:GridOptions
20139 * @description Whether you allow infinite scroll down, implying that the first page of data
20140 * you have displayed is in the middle of your data set. If set to true then we trigger the
20141 * needMoreData event when the user hits the bottom of the scrollbar.
20142 * <br> Defaults to true
20144 gridOptions.infiniteScrollDown = gridOptions.infiniteScrollDown !== false;
20150 * @name setScrollDirections
20151 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20152 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20153 * and also sets the grid.suppressParentScroll
20154 * @param {grid} grid the grid we're operating on
20155 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20156 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20158 setScrollDirections: function ( grid, scrollUp, scrollDown ) {
20159 grid.infiniteScroll.scrollUp = ( scrollUp === true );
20160 grid.suppressParentScrollUp = ( scrollUp === true );
20162 grid.infiniteScroll.scrollDown = ( scrollDown !== false);
20163 grid.suppressParentScrollDown = ( scrollDown !== false);
20169 * @name handleScroll
20170 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20171 * @description Called whenever the grid scrolls, determines whether the scroll should
20172 * trigger an infinite scroll request for more data
20173 * @param {object} args the args from the event
20175 handleScroll: function (args) {
20176 // don't request data if already waiting for data, or if source is coming from ui.grid.adjustInfiniteScrollPosition() function
20177 if ( args.grid.infiniteScroll && args.grid.infiniteScroll.dataLoading || args.source === 'ui.grid.adjustInfiniteScrollPosition' ){
20183 var targetPercentage = args.grid.options.infiniteScrollRowsFromEnd / args.grid.renderContainers.body.visibleRowCache.length;
20184 if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) {
20185 percentage = args.y.percentage;
20186 if (percentage <= targetPercentage){
20187 service.loadData(args.grid);
20189 } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) {
20190 percentage = 1 - args.y.percentage;
20191 if (percentage <= targetPercentage){
20192 service.loadData(args.grid);
20202 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20203 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20204 * and whether there are more pages upwards or downwards. It also stores the number of rows that we had previously,
20205 * and clears out any saved scroll position so that we know whether or not the user calls `saveScrollPercentage`
20206 * @param {Grid} grid the grid we're working on
20208 loadData: function (grid) {
20209 // save number of currently visible rows to calculate new scroll position later - we know that we want
20210 // to be at approximately the row we're currently at
20211 grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20212 grid.infiniteScroll.direction = grid.scrollDirection;
20213 delete grid.infiniteScroll.prevScrollTop;
20215 if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) {
20216 grid.infiniteScroll.dataLoading = true;
20217 grid.api.infiniteScroll.raise.needLoadMoreDataTop();
20218 } else if (grid.scrollDirection === uiGridConstants.scrollDirection.DOWN && grid.infiniteScroll.scrollDown ) {
20219 grid.infiniteScroll.dataLoading = true;
20220 grid.api.infiniteScroll.raise.needLoadMoreData();
20227 * @name adjustScroll
20228 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20229 * @description Once we are informed that data has been loaded, adjust the scroll position to account for that
20230 * addition and to make things look clean.
20232 * If we're scrolling up we scroll to the first row of the old data set -
20233 * so we're assuming that you would have gotten to the top of the grid (from the 20% need more data trigger) by
20234 * 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
20235 * assuming that you would have gotten to the bottom of the grid (from the 80% need more data trigger) by the time
20236 * the data comes back.
20238 * Neither of these are good assumptions, but making this a smoother experience really requires
20239 * 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
20240 * 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
20241 * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for
20242 * now, until someone wants to do better.
20243 * @param {Grid} grid the grid we're working on
20244 * @returns {promise} a promise that is resolved when scrolling has finished
20246 adjustScroll: function(grid){
20247 var promise = $q.defer();
20248 $timeout(function () {
20249 var newPercentage, viewportHeight, rowHeight, newVisibleRows, oldTop, newTop;
20251 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight;
20252 rowHeight = grid.options.rowHeight;
20254 if ( grid.infiniteScroll.direction === undefined ){
20255 // called from initialize, tweak our scroll up a little
20256 service.adjustInfiniteScrollPosition(grid, 0);
20259 newVisibleRows = grid.getVisibleRowCount();
20261 // in case not enough data is loaded to enable scroller - load more data
20262 var canvasHeight = rowHeight * newVisibleRows;
20263 if (grid.infiniteScroll.scrollDown && (viewportHeight > canvasHeight)) {
20264 grid.api.infiniteScroll.raise.needLoadMoreData();
20267 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){
20268 oldTop = grid.infiniteScroll.prevScrollTop || 0;
20269 newTop = oldTop + (newVisibleRows - grid.infiniteScroll.previousVisibleRows)*rowHeight;
20270 service.adjustInfiniteScrollPosition(grid, newTop);
20271 $timeout( function() {
20276 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){
20277 newTop = grid.infiniteScroll.prevScrollTop || (grid.infiniteScroll.previousVisibleRows*rowHeight - viewportHeight);
20278 service.adjustInfiniteScrollPosition(grid, newTop);
20279 $timeout( function() {
20285 return promise.promise;
20291 * @name adjustInfiniteScrollPosition
20292 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20293 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20294 * @param {Grid} grid the grid we're working on
20295 * @param {number} scrollTop the position through the grid that we want to scroll to
20296 * @returns {promise} a promise that is resolved when the scrolling finishes
20298 adjustInfiniteScrollPosition: function (grid, scrollTop) {
20299 var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'),
20300 visibleRows = grid.getVisibleRowCount(),
20301 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight,
20302 rowHeight = grid.options.rowHeight,
20303 scrollHeight = visibleRows*rowHeight-viewportHeight;
20305 //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
20306 if (scrollTop === 0 && grid.infiniteScroll.scrollUp) {
20307 // using pixels results in a relative scroll, hence we have to use percentage
20308 scrollEvent.y = {percentage: 1/scrollHeight};
20311 scrollEvent.y = {percentage: scrollTop/scrollHeight};
20313 grid.scrollContainers('', scrollEvent);
20319 * @name dataRemovedTop
20320 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20321 * @description Adjusts the scroll position after you've removed data at the top. You should
20322 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20323 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20324 * before you start removing data
20325 * @param {Grid} grid the grid we're working on
20326 * @param {boolean} scrollUp flag that there are pages upwards, fire
20327 * infinite scroll events upward
20328 * @param {boolean} scrollDown flag that there are pages downwards, so
20329 * fire infinite scroll events downward
20330 * @returns {promise} a promise that is resolved when the scrolling finishes
20332 dataRemovedTop: function( grid, scrollUp, scrollDown ) {
20333 var newVisibleRows, oldTop, newTop, rowHeight;
20334 service.setScrollDirections( grid, scrollUp, scrollDown );
20336 newVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20337 oldTop = grid.infiniteScroll.prevScrollTop;
20338 rowHeight = grid.options.rowHeight;
20340 // since we removed from the top, our new scroll row will be the old scroll row less the number
20342 newTop = oldTop - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows )*rowHeight;
20344 return service.adjustInfiniteScrollPosition( grid, newTop );
20349 * @name dataRemovedBottom
20350 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20351 * @description Adjusts the scroll position after you've removed data at the bottom. You should
20352 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20353 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20354 * before you start removing data
20355 * @param {Grid} grid the grid we're working on
20356 * @param {boolean} scrollUp flag that there are pages upwards, fire
20357 * infinite scroll events upward
20358 * @param {boolean} scrollDown flag that there are pages downwards, so
20359 * fire infinite scroll events downward
20361 dataRemovedBottom: function( grid, scrollUp, scrollDown ) {
20363 service.setScrollDirections( grid, scrollUp, scrollDown );
20365 newTop = grid.infiniteScroll.prevScrollTop;
20367 return service.adjustInfiniteScrollPosition( grid, newTop );
20374 * @name ui.grid.infiniteScroll.directive:uiGridInfiniteScroll
20378 * @description Adds infinite scroll features to grid
20381 <example module="app">
20382 <file name="app.js">
20383 var app = angular.module('app', ['ui.grid', 'ui.grid.infiniteScroll']);
20385 app.controller('MainCtrl', ['$scope', function ($scope) {
20387 { name: 'Alex', car: 'Toyota' },
20388 { name: 'Sam', car: 'Lexus' }
20391 $scope.columnDefs = [
20397 <file name="index.html">
20398 <div ng-controller="MainCtrl">
20399 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-infinite-scroll="20"></div>
20405 module.directive('uiGridInfiniteScroll', ['uiGridInfiniteScrollService',
20406 function (uiGridInfiniteScrollService) {
20410 require: '^uiGrid',
20411 compile: function($scope, $elm, $attr){
20413 pre: function($scope, $elm, $attr, uiGridCtrl) {
20414 uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid, $scope);
20416 post: function($scope, $elm, $attr) {
20430 * @name ui.grid.moveColumns
20433 * # ui.grid.moveColumns
20435 * <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>
20437 * This module provides column moving capability to ui.grid. It enables to change the position of columns.
20438 * <div doc-module-components="ui.grid.moveColumns"></div>
20440 var module = angular.module('ui.grid.moveColumns', ['ui.grid']);
20444 * @name ui.grid.moveColumns.service:uiGridMoveColumnService
20445 * @description Service for column moving feature.
20447 module.service('uiGridMoveColumnService', ['$q', '$timeout', '$log', 'ScrollEvent', 'uiGridConstants', 'gridUtil', function ($q, $timeout, $log, ScrollEvent, uiGridConstants, gridUtil) {
20450 initializeGrid: function (grid) {
20452 this.registerPublicApi(grid);
20453 this.defaultGridOptions(grid.options);
20454 grid.moveColumns = {orderCache: []}; // Used to cache the order before columns are rebuilt
20455 grid.registerColumnBuilder(self.movableColumnBuilder);
20456 grid.registerDataChangeCallback(self.verifyColumnOrder, [uiGridConstants.dataChange.COLUMN]);
20458 registerPublicApi: function (grid) {
20462 * @name ui.grid.moveColumns.api:PublicApi
20463 * @description Public Api for column moving feature.
20469 * @name columnPositionChanged
20470 * @eventOf ui.grid.moveColumns.api:PublicApi
20471 * @description raised when column is moved
20473 * gridApi.colMovable.on.columnPositionChanged(scope,function(colDef, originalPosition, newPosition){})
20475 * @param {object} colDef the column that was moved
20476 * @param {integer} originalPosition of the column
20477 * @param {integer} finalPosition of the column
20480 columnPositionChanged: function (colDef, originalPosition, newPosition) {
20488 * @methodOf ui.grid.moveColumns.api:PublicApi
20489 * @description Method can be used to change column position.
20491 * gridApi.colMovable.moveColumn(oldPosition, newPosition)
20493 * @param {integer} originalPosition of the column
20494 * @param {integer} finalPosition of the column
20497 moveColumn: function (originalPosition, finalPosition) {
20498 var columns = grid.columns;
20499 if (!angular.isNumber(originalPosition) || !angular.isNumber(finalPosition)) {
20500 gridUtil.logError('MoveColumn: Please provide valid values for originalPosition and finalPosition');
20503 var nonMovableColumns = 0;
20504 for (var i = 0; i < columns.length; i++) {
20505 if ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true) {
20506 nonMovableColumns++;
20509 if (originalPosition >= (columns.length - nonMovableColumns) || finalPosition >= (columns.length - nonMovableColumns)) {
20510 gridUtil.logError('MoveColumn: Invalid values for originalPosition, finalPosition');
20513 var findPositionForRenderIndex = function (index) {
20514 var position = index;
20515 for (var i = 0; i <= position; i++) {
20516 if (angular.isDefined(columns[i]) && ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true)) {
20522 self.redrawColumnAtPosition(grid, findPositionForRenderIndex(originalPosition), findPositionForRenderIndex(finalPosition));
20527 grid.api.registerEventsFromObject(publicApi.events);
20528 grid.api.registerMethodsFromObject(publicApi.methods);
20530 defaultGridOptions: function (gridOptions) {
20533 * @name ui.grid.moveColumns.api:GridOptions
20535 * @description Options for configuring the move column feature, these are available to be
20536 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
20540 * @name enableColumnMoving
20541 * @propertyOf ui.grid.moveColumns.api:GridOptions
20542 * @description If defined, sets the default value for the colMovable flag on each individual colDefs
20543 * if their individual enableColumnMoving configuration is not defined. Defaults to true.
20545 gridOptions.enableColumnMoving = gridOptions.enableColumnMoving !== false;
20547 movableColumnBuilder: function (colDef, col, gridOptions) {
20551 * @name ui.grid.moveColumns.api:ColumnDef
20553 * @description Column Definition for move column feature, these are available to be
20554 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
20558 * @name enableColumnMoving
20559 * @propertyOf ui.grid.moveColumns.api:ColumnDef
20560 * @description Enable column moving for the column.
20562 colDef.enableColumnMoving = colDef.enableColumnMoving === undefined ? gridOptions.enableColumnMoving
20563 : colDef.enableColumnMoving;
20564 return $q.all(promises);
20568 * @name updateColumnCache
20569 * @methodOf ui.grid.moveColumns
20570 * @description Cache the current order of columns, so we can restore them after new columnDefs are defined
20572 updateColumnCache: function(grid){
20573 grid.moveColumns.orderCache = grid.getOnlyDataColumns();
20577 * @name verifyColumnOrder
20578 * @methodOf ui.grid.moveColumns
20579 * @description dataChangeCallback which uses the cached column order to restore the column order
20580 * when it is reset by altering the columnDefs array.
20582 verifyColumnOrder: function(grid){
20583 var headerRowOffset = grid.rowHeaderColumns.length;
20586 angular.forEach(grid.moveColumns.orderCache, function(cacheCol, cacheIndex){
20587 newIndex = grid.columns.indexOf(cacheCol);
20588 if ( newIndex !== -1 && newIndex - headerRowOffset !== cacheIndex ){
20589 var column = grid.columns.splice(newIndex, 1)[0];
20590 grid.columns.splice(cacheIndex + headerRowOffset, 0, column);
20594 redrawColumnAtPosition: function (grid, originalPosition, newPosition) {
20596 var columns = grid.columns;
20598 var originalColumn = columns[originalPosition];
20599 if (originalColumn.colDef.enableColumnMoving) {
20600 if (originalPosition > newPosition) {
20601 for (var i1 = originalPosition; i1 > newPosition; i1--) {
20602 columns[i1] = columns[i1 - 1];
20605 else if (newPosition > originalPosition) {
20606 for (var i2 = originalPosition; i2 < newPosition; i2++) {
20607 columns[i2] = columns[i2 + 1];
20610 columns[newPosition] = originalColumn;
20611 service.updateColumnCache(grid);
20612 grid.queueGridRefresh();
20613 $timeout(function () {
20614 grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
20615 grid.api.colMovable.raise.columnPositionChanged(originalColumn.colDef, originalPosition, newPosition);
20625 * @name ui.grid.moveColumns.directive:uiGridMoveColumns
20628 * @description Adds column moving features to the ui-grid directive.
20630 <example module="app">
20631 <file name="app.js">
20632 var app = angular.module('app', ['ui.grid', 'ui.grid.moveColumns']);
20633 app.controller('MainCtrl', ['$scope', function ($scope) {
20635 { name: 'Bob', title: 'CEO', age: 45 },
20636 { name: 'Frank', title: 'Lowly Developer', age: 25 },
20637 { name: 'Jenny', title: 'Highly Developer', age: 35 }
20639 $scope.columnDefs = [
20646 <file name="main.css">
20652 <file name="index.html">
20653 <div ng-controller="MainCtrl">
20654 <div class="grid" ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-move-columns></div>
20659 module.directive('uiGridMoveColumns', ['uiGridMoveColumnService', function (uiGridMoveColumnService) {
20663 require: '^uiGrid',
20665 compile: function () {
20667 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
20668 uiGridMoveColumnService.initializeGrid(uiGridCtrl.grid);
20670 post: function ($scope, $elm, $attrs, uiGridCtrl) {
20679 * @name ui.grid.moveColumns.directive:uiGridHeaderCell
20683 * @description Stacks on top of ui.grid.uiGridHeaderCell to provide capability to be able to move it to reposition column.
20685 * On receiving mouseDown event headerCell is cloned, now as the mouse moves the cloned header cell also moved in the grid.
20686 * In case the moving cloned header cell reaches the left or right extreme of grid, grid scrolling is triggered (if horizontal scroll exists).
20687 * On mouseUp event column is repositioned at position where mouse is released and cloned header cell is removed.
20689 * Events that invoke cloning of header cell:
20692 * Events that invoke movement of cloned header cell:
20695 * Events that invoke repositioning of column:
20698 module.directive('uiGridHeaderCell', ['$q', 'gridUtil', 'uiGridMoveColumnService', '$document', '$log', 'uiGridConstants', 'ScrollEvent',
20699 function ($q, gridUtil, uiGridMoveColumnService, $document, $log, uiGridConstants, ScrollEvent) {
20702 require: '^uiGrid',
20703 compile: function () {
20705 post: function ($scope, $elm, $attrs, uiGridCtrl) {
20707 if ($scope.col.colDef.enableColumnMoving) {
20710 * Our general approach to column move is that we listen to a touchstart or mousedown
20711 * event over the column header. When we hear one, then we wait for a move of the same type
20712 * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for
20713 * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move,
20714 * and we instead get a mouseup or a touchend, then we just drop out again and do nothing.
20717 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
20720 var previousMouseX;
20721 var totalMouseMovement;
20722 var rightMoveLimit;
20723 var elmCloned = false;
20726 var moveOccurred = false;
20728 var downFn = function( event ){
20729 //Setting some variables required for calculations.
20730 gridLeft = $scope.grid.element[0].getBoundingClientRect().left;
20731 if ( $scope.grid.hasLeftContainer() ){
20732 gridLeft += $scope.grid.renderContainers.left.header[0].getBoundingClientRect().width;
20735 previousMouseX = event.pageX;
20736 totalMouseMovement = 0;
20737 rightMoveLimit = gridLeft + $scope.grid.getViewportWidth();
20739 if ( event.type === 'mousedown' ){
20740 $document.on('mousemove', moveFn);
20741 $document.on('mouseup', upFn);
20742 } else if ( event.type === 'touchstart' ){
20743 $document.on('touchmove', moveFn);
20744 $document.on('touchend', upFn);
20748 var moveFn = function( event ) {
20749 var changeValue = event.pageX - previousMouseX;
20750 if ( changeValue === 0 ){ return; }
20751 //Disable text selection in Chrome during column move
20752 document.onselectstart = function() { return false; };
20754 moveOccurred = true;
20759 else if (elmCloned) {
20760 moveElement(changeValue);
20761 previousMouseX = event.pageX;
20765 var upFn = function( event ){
20766 //Re-enable text selection after column move
20767 document.onselectstart = null;
20769 //Remove the cloned element on mouse up.
20771 movingElm.remove();
20778 if (!moveOccurred){
20782 var columns = $scope.grid.columns;
20783 var columnIndex = 0;
20784 for (var i = 0; i < columns.length; i++) {
20785 if (columns[i].colDef.name !== $scope.col.colDef.name) {
20793 //Case where column should be moved to a position on its left
20794 if (totalMouseMovement < 0) {
20795 var totalColumnsLeftWidth = 0;
20796 for (var il = columnIndex - 1; il >= 0; il--) {
20797 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
20798 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
20799 if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) {
20800 uiGridMoveColumnService.redrawColumnAtPosition
20801 ($scope.grid, columnIndex, il + 1);
20806 //Case where column should be moved to beginning of the grid.
20807 if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) {
20808 uiGridMoveColumnService.redrawColumnAtPosition
20809 ($scope.grid, columnIndex, 0);
20813 //Case where column should be moved to a position on its right
20814 else if (totalMouseMovement > 0) {
20815 var totalColumnsRightWidth = 0;
20816 for (var ir = columnIndex + 1; ir < columns.length; ir++) {
20817 if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) {
20818 totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width;
20819 if (totalColumnsRightWidth > totalMouseMovement) {
20820 uiGridMoveColumnService.redrawColumnAtPosition
20821 ($scope.grid, columnIndex, ir - 1);
20826 //Case where column should be moved to end of the grid.
20827 if (totalColumnsRightWidth < totalMouseMovement) {
20828 uiGridMoveColumnService.redrawColumnAtPosition
20829 ($scope.grid, columnIndex, columns.length - 1);
20834 var onDownEvents = function(){
20835 $contentsElm.on('touchstart', downFn);
20836 $contentsElm.on('mousedown', downFn);
20839 var offAllEvents = function() {
20840 $contentsElm.off('touchstart', downFn);
20841 $contentsElm.off('mousedown', downFn);
20843 $document.off('mousemove', moveFn);
20844 $document.off('touchmove', moveFn);
20846 $document.off('mouseup', upFn);
20847 $document.off('touchend', upFn);
20853 var cloneElement = function () {
20856 //Cloning header cell and appending to current header cell.
20857 movingElm = $elm.clone();
20858 $elm.parent().append(movingElm);
20860 //Left of cloned element should be aligned to original header cell.
20861 movingElm.addClass('movingColumn');
20862 var movingElementStyles = {};
20864 if (gridUtil.detectBrowser() === 'safari') {
20865 //Correction for Safari getBoundingClientRect,
20866 //which does not correctly compute when there is an horizontal scroll
20867 elmLeft = $elm[0].offsetLeft + $elm[0].offsetWidth - $elm[0].getBoundingClientRect().width;
20870 elmLeft = $elm[0].getBoundingClientRect().left;
20872 movingElementStyles.left = (elmLeft - gridLeft) + 'px';
20873 var gridRight = $scope.grid.element[0].getBoundingClientRect().right;
20874 var elmRight = $elm[0].getBoundingClientRect().right;
20875 if (elmRight > gridRight) {
20876 reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight);
20877 movingElementStyles.width = reducedWidth + 'px';
20879 movingElm.css(movingElementStyles);
20882 var moveElement = function (changeValue) {
20883 //Calculate total column width
20884 var columns = $scope.grid.columns;
20885 var totalColumnWidth = 0;
20886 for (var i = 0; i < columns.length; i++) {
20887 if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) {
20888 totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width;
20892 //Calculate new position of left of column
20893 var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1;
20894 var currentElmRight = movingElm[0].getBoundingClientRect().right;
20895 var newElementLeft;
20897 newElementLeft = currentElmLeft - gridLeft + changeValue;
20898 newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit;
20900 //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid
20901 if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) {
20902 movingElm.css({visibility: 'visible', 'left': newElementLeft + 'px'});
20904 else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) {
20906 var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement');
20907 scrollEvent.x = {pixels: changeValue};
20908 scrollEvent.grid.scrollContainers('',scrollEvent);
20911 //Calculate total width of columns on the left of the moving column and the mouse movement
20912 var totalColumnsLeftWidth = 0;
20913 for (var il = 0; il < columns.length; il++) {
20914 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
20915 if (columns[il].colDef.name !== $scope.col.colDef.name) {
20916 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
20923 if ($scope.newScrollLeft === undefined) {
20924 totalMouseMovement += changeValue;
20927 totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth;
20930 //Increase width of moving column, in case the rightmost column was moved and its width was
20931 //decreased because of overflow
20932 if (reducedWidth < $scope.col.drawnWidth) {
20933 reducedWidth += Math.abs(changeValue);
20934 movingElm.css({'width': reducedWidth + 'px'});
20950 * @name ui.grid.pagination
20954 * # ui.grid.pagination
20956 * <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>
20958 * This module provides pagination support to ui-grid
20960 var module = angular.module('ui.grid.pagination', ['ng', 'ui.grid']);
20964 * @name ui.grid.pagination.service:uiGridPaginationService
20966 * @description Service for the pagination feature
20968 module.service('uiGridPaginationService', ['gridUtil',
20969 function (gridUtil) {
20973 * @name initializeGrid
20974 * @methodOf ui.grid.pagination.service:uiGridPaginationService
20975 * @description Attaches the service to a certain grid
20976 * @param {Grid} grid The grid we want to work with
20978 initializeGrid: function (grid) {
20979 service.defaultGridOptions(grid.options);
20983 * @name ui.grid.pagination.api:PublicAPI
20985 * @description Public API for the pagination feature
20992 * @name paginationChanged
20993 * @eventOf ui.grid.pagination.api:PublicAPI
20994 * @description This event fires when the pageSize or currentPage changes
20995 * @param {int} currentPage requested page number
20996 * @param {int} pageSize requested page size
20998 paginationChanged: function (currentPage, pageSize) { }
21006 * @methodOf ui.grid.pagination.api:PublicAPI
21007 * @description Returns the number of the current page
21009 getPage: function () {
21010 return grid.options.enablePagination ? grid.options.paginationCurrentPage : null;
21014 * @name getTotalPages
21015 * @methodOf ui.grid.pagination.api:PublicAPI
21016 * @description Returns the total number of pages
21018 getTotalPages: function () {
21019 if (!grid.options.enablePagination) {
21023 return (grid.options.totalItems === 0) ? 1 : Math.ceil(grid.options.totalItems / grid.options.paginationPageSize);
21028 * @methodOf ui.grid.pagination.api:PublicAPI
21029 * @description Moves to the next page, if possible
21031 nextPage: function () {
21032 if (!grid.options.enablePagination) {
21036 if (grid.options.totalItems > 0) {
21037 grid.options.paginationCurrentPage = Math.min(
21038 grid.options.paginationCurrentPage + 1,
21039 publicApi.methods.pagination.getTotalPages()
21042 grid.options.paginationCurrentPage++;
21047 * @name previousPage
21048 * @methodOf ui.grid.pagination.api:PublicAPI
21049 * @description Moves to the previous page, if we're not on the first page
21051 previousPage: function () {
21052 if (!grid.options.enablePagination) {
21056 grid.options.paginationCurrentPage = Math.max(grid.options.paginationCurrentPage - 1, 1);
21061 * @methodOf ui.grid.pagination.api:PublicAPI
21062 * @description Moves to the requested page
21063 * @param {int} page The number of the page that should be displayed
21065 seek: function (page) {
21066 if (!grid.options.enablePagination) {
21069 if (!angular.isNumber(page) || page < 1) {
21070 throw 'Invalid page number: ' + page;
21073 grid.options.paginationCurrentPage = Math.min(page, publicApi.methods.pagination.getTotalPages());
21079 grid.api.registerEventsFromObject(publicApi.events);
21080 grid.api.registerMethodsFromObject(publicApi.methods);
21082 var processPagination = function( renderableRows ){
21083 if (grid.options.useExternalPagination || !grid.options.enablePagination) {
21084 return renderableRows;
21086 //client side pagination
21087 var pageSize = parseInt(grid.options.paginationPageSize, 10);
21088 var currentPage = parseInt(grid.options.paginationCurrentPage, 10);
21090 var visibleRows = renderableRows.filter(function (row) { return row.visible; });
21091 grid.options.totalItems = visibleRows.length;
21093 var firstRow = (currentPage - 1) * pageSize;
21094 if (firstRow > visibleRows.length) {
21095 currentPage = grid.options.paginationCurrentPage = 1;
21096 firstRow = (currentPage - 1) * pageSize;
21098 return visibleRows.slice(firstRow, firstRow + pageSize);
21101 grid.registerRowsProcessor(processPagination, 900 );
21104 defaultGridOptions: function (gridOptions) {
21107 * @name ui.grid.pagination.api:GridOptions
21109 * @description GridOptions for the pagination feature, these are available to be
21110 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21115 * @name enablePagination
21116 * @propertyOf ui.grid.pagination.api:GridOptions
21117 * @description Enables pagination, defaults to true
21119 gridOptions.enablePagination = gridOptions.enablePagination !== false;
21122 * @name enablePaginationControls
21123 * @propertyOf ui.grid.pagination.api:GridOptions
21124 * @description Enables the paginator at the bottom of the grid. Turn this off, if you want to implement your
21125 * own controls outside the grid.
21127 gridOptions.enablePaginationControls = gridOptions.enablePaginationControls !== false;
21130 * @name useExternalPagination
21131 * @propertyOf ui.grid.pagination.api:GridOptions
21132 * @description Disables client side pagination. When true, handle the paginationChanged event and set data
21133 * and totalItems, defaults to `false`
21135 gridOptions.useExternalPagination = gridOptions.useExternalPagination === true;
21139 * @propertyOf ui.grid.pagination.api:GridOptions
21140 * @description Total number of items, set automatically when client side pagination, needs set by user
21141 * for server side pagination
21143 if (gridUtil.isNullOrUndefined(gridOptions.totalItems)) {
21144 gridOptions.totalItems = 0;
21148 * @name paginationPageSizes
21149 * @propertyOf ui.grid.pagination.api:GridOptions
21150 * @description Array of page sizes, defaults to `[250, 500, 1000]`
21152 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSizes)) {
21153 gridOptions.paginationPageSizes = [250, 500, 1000];
21157 * @name paginationPageSize
21158 * @propertyOf ui.grid.pagination.api:GridOptions
21159 * @description Page size, defaults to the first item in paginationPageSizes, or 0 if paginationPageSizes is empty
21161 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSize)) {
21162 if (gridOptions.paginationPageSizes.length > 0) {
21163 gridOptions.paginationPageSize = gridOptions.paginationPageSizes[0];
21165 gridOptions.paginationPageSize = 0;
21170 * @name paginationCurrentPage
21171 * @propertyOf ui.grid.pagination.api:GridOptions
21172 * @description Current page number, defaults to 1
21174 if (gridUtil.isNullOrUndefined(gridOptions.paginationCurrentPage)) {
21175 gridOptions.paginationCurrentPage = 1;
21180 * @name paginationTemplate
21181 * @propertyOf ui.grid.pagination.api:GridOptions
21182 * @description A custom template for the pager, defaults to `ui-grid/pagination`
21184 if (gridUtil.isNullOrUndefined(gridOptions.paginationTemplate)) {
21185 gridOptions.paginationTemplate = 'ui-grid/pagination';
21190 * @methodOf ui.grid.pagination.service:uiGridPaginationService
21191 * @name uiGridPaginationService
21192 * @description Raises paginationChanged and calls refresh for client side pagination
21193 * @param {Grid} grid the grid for which the pagination changed
21194 * @param {int} currentPage requested page number
21195 * @param {int} pageSize requested page size
21197 onPaginationChanged: function (grid, currentPage, pageSize) {
21198 grid.api.pagination.raise.paginationChanged(currentPage, pageSize);
21199 if (!grid.options.useExternalPagination) {
21200 grid.queueGridRefresh(); //client side pagination
21210 * @name ui.grid.pagination.directive:uiGridPagination
21214 * @description Adds pagination features to grid
21216 <example module="app">
21217 <file name="app.js">
21218 var app = angular.module('app', ['ui.grid', 'ui.grid.pagination']);
21220 app.controller('MainCtrl', ['$scope', function ($scope) {
21222 { name: 'Alex', car: 'Toyota' },
21223 { name: 'Sam', car: 'Lexus' },
21224 { name: 'Joe', car: 'Dodge' },
21225 { name: 'Bob', car: 'Buick' },
21226 { name: 'Cindy', car: 'Ford' },
21227 { name: 'Brian', car: 'Audi' },
21228 { name: 'Malcom', car: 'Mercedes Benz' },
21229 { name: 'Dave', car: 'Ford' },
21230 { name: 'Stacey', car: 'Audi' },
21231 { name: 'Amy', car: 'Acura' },
21232 { name: 'Scott', car: 'Toyota' },
21233 { name: 'Ryan', car: 'BMW' },
21236 $scope.gridOptions = {
21238 paginationPageSizes: [5, 10, 25],
21239 paginationPageSize: 5,
21247 <file name="index.html">
21248 <div ng-controller="MainCtrl">
21249 <div ui-grid="gridOptions" ui-grid-pagination></div>
21254 module.directive('uiGridPagination', ['gridUtil', 'uiGridPaginationService',
21255 function (gridUtil, uiGridPaginationService) {
21261 pre: function ($scope, $elm, $attr, uiGridCtrl) {
21262 uiGridPaginationService.initializeGrid(uiGridCtrl.grid);
21264 gridUtil.getTemplate(uiGridCtrl.grid.options.paginationTemplate)
21265 .then(function (contents) {
21266 var template = angular.element(contents);
21267 $elm.append(template);
21268 uiGridCtrl.innerCompile(template);
21278 * @name ui.grid.pagination.directive:uiGridPager
21281 * @description Panel for handling pagination
21283 module.directive('uiGridPager', ['uiGridPaginationService', 'uiGridConstants', 'gridUtil', 'i18nService',
21284 function (uiGridPaginationService, uiGridConstants, gridUtil, i18nService) {
21288 require: '^uiGrid',
21289 link: function ($scope, $elm, $attr, uiGridCtrl) {
21290 var defaultFocusElementSelector = '.ui-grid-pager-control-input';
21291 $scope.aria = i18nService.getSafeText('pagination.aria'); //Returns an object with all of the aria labels
21293 $scope.paginationApi = uiGridCtrl.grid.api.pagination;
21294 $scope.sizesLabel = i18nService.getSafeText('pagination.sizes');
21295 $scope.totalItemsLabel = i18nService.getSafeText('pagination.totalItems');
21296 $scope.paginationOf = i18nService.getSafeText('pagination.of');
21297 $scope.paginationThrough = i18nService.getSafeText('pagination.through');
21299 var options = uiGridCtrl.grid.options;
21301 uiGridCtrl.grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
21302 adjustment.height = adjustment.height - gridUtil.elementHeight($elm);
21306 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback(function (grid) {
21307 if (!grid.options.useExternalPagination) {
21308 grid.options.totalItems = grid.rows.length;
21310 }, [uiGridConstants.dataChange.ROW]);
21312 $scope.$on('$destroy', dataChangeDereg);
21314 var setShowing = function () {
21315 $scope.showingLow = ((options.paginationCurrentPage - 1) * options.paginationPageSize) + 1;
21316 $scope.showingHigh = Math.min(options.paginationCurrentPage * options.paginationPageSize, options.totalItems);
21319 var deregT = $scope.$watch('grid.options.totalItems + grid.options.paginationPageSize', setShowing);
21321 var deregP = $scope.$watch('grid.options.paginationCurrentPage + grid.options.paginationPageSize', function (newValues, oldValues) {
21322 if (newValues === oldValues || oldValues === undefined) {
21326 if (!angular.isNumber(options.paginationCurrentPage) || options.paginationCurrentPage < 1) {
21327 options.paginationCurrentPage = 1;
21331 if (options.totalItems > 0 && options.paginationCurrentPage > $scope.paginationApi.getTotalPages()) {
21332 options.paginationCurrentPage = $scope.paginationApi.getTotalPages();
21337 uiGridPaginationService.onPaginationChanged($scope.grid, options.paginationCurrentPage, options.paginationPageSize);
21341 $scope.$on('$destroy', function() {
21346 $scope.cantPageForward = function () {
21347 if (options.totalItems > 0) {
21348 return options.paginationCurrentPage >= $scope.paginationApi.getTotalPages();
21350 return options.data.length < 1;
21354 $scope.cantPageToLast = function () {
21355 if (options.totalItems > 0) {
21356 return $scope.cantPageForward();
21362 $scope.cantPageBackward = function () {
21363 return options.paginationCurrentPage <= 1;
21366 var focusToInputIf = function(condition){
21368 gridUtil.focus.bySelector($elm, defaultFocusElementSelector);
21372 //Takes care of setting focus to the middle element when focus is lost
21373 $scope.pageFirstPageClick = function () {
21374 $scope.paginationApi.seek(1);
21375 focusToInputIf($scope.cantPageBackward());
21378 $scope.pagePreviousPageClick = function () {
21379 $scope.paginationApi.previousPage();
21380 focusToInputIf($scope.cantPageBackward());
21383 $scope.pageNextPageClick = function () {
21384 $scope.paginationApi.nextPage();
21385 focusToInputIf($scope.cantPageForward());
21388 $scope.pageLastPageClick = function () {
21389 $scope.paginationApi.seek($scope.paginationApi.getTotalPages());
21390 focusToInputIf($scope.cantPageToLast());
21404 * @name ui.grid.pinning
21407 * # ui.grid.pinning
21409 * <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>
21411 * This module provides column pinning to the end user via menu options in the column header
21413 * <div doc-module-components="ui.grid.pinning"></div>
21416 var module = angular.module('ui.grid.pinning', ['ui.grid']);
21418 module.constant('uiGridPinningConstants', {
21426 module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', 'uiGridPinningConstants', function (gridUtil, GridRenderContainer, i18nService, uiGridPinningConstants) {
21429 initializeGrid: function (grid) {
21430 service.defaultGridOptions(grid.options);
21432 // Register a column builder to add new menu items for pinning left and right
21433 grid.registerColumnBuilder(service.pinningColumnBuilder);
21437 * @name ui.grid.pinning.api:PublicApi
21439 * @description Public Api for pinning feature
21447 * @eventOf ui.grid.pinning.api:PublicApi
21448 * @description raised when column pin state has changed
21450 * gridApi.pinning.on.columnPinned(scope, function(colDef){})
21452 * @param {object} colDef the column that was changed
21453 * @param {string} container the render container the column is in ('left', 'right', '')
21455 columnPinned: function(colDef, container) {
21464 * @methodOf ui.grid.pinning.api:PublicApi
21465 * @description pin column left, right, or none
21467 * gridApi.pinning.pinColumn(col, uiGridPinningConstants.container.LEFT)
21469 * @param {gridColumn} col the column being pinned
21470 * @param {string} container one of the recognised types
21471 * from uiGridPinningConstants
21473 pinColumn: function(col, container) {
21474 service.pinColumn(grid, col, container);
21480 grid.api.registerEventsFromObject(publicApi.events);
21481 grid.api.registerMethodsFromObject(publicApi.methods);
21484 defaultGridOptions: function (gridOptions) {
21485 //default option to true unless it was explicitly set to false
21488 * @name ui.grid.pinning.api:GridOptions
21490 * @description GridOptions for pinning feature, these are available to be
21491 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21496 * @name enablePinning
21497 * @propertyOf ui.grid.pinning.api:GridOptions
21498 * @description Enable pinning for the entire grid.
21499 * <br/>Defaults to true
21501 gridOptions.enablePinning = gridOptions.enablePinning !== false;
21505 pinningColumnBuilder: function (colDef, col, gridOptions) {
21506 //default to true unless gridOptions or colDef is explicitly false
21510 * @name ui.grid.pinning.api:ColumnDef
21512 * @description ColumnDef for pinning feature, these are available to be
21513 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
21518 * @name enablePinning
21519 * @propertyOf ui.grid.pinning.api:ColumnDef
21520 * @description Enable pinning for the individual column.
21521 * <br/>Defaults to true
21523 colDef.enablePinning = colDef.enablePinning === undefined ? gridOptions.enablePinning : colDef.enablePinning;
21529 * @propertyOf ui.grid.pinning.api:ColumnDef
21530 * @description Column is pinned left when grid is rendered
21531 * <br/>Defaults to false
21536 * @name pinnedRight
21537 * @propertyOf ui.grid.pinning.api:ColumnDef
21538 * @description Column is pinned right when grid is rendered
21539 * <br/>Defaults to false
21541 if (colDef.pinnedLeft) {
21542 col.renderContainer = 'left';
21543 col.grid.createLeftContainer();
21545 else if (colDef.pinnedRight) {
21546 col.renderContainer = 'right';
21547 col.grid.createRightContainer();
21550 if (!colDef.enablePinning) {
21554 var pinColumnLeftAction = {
21555 name: 'ui.grid.pinning.pinLeft',
21556 title: i18nService.get().pinning.pinLeft,
21557 icon: 'ui-grid-icon-left-open',
21558 shown: function () {
21559 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'left';
21561 action: function () {
21562 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.LEFT);
21566 var pinColumnRightAction = {
21567 name: 'ui.grid.pinning.pinRight',
21568 title: i18nService.get().pinning.pinRight,
21569 icon: 'ui-grid-icon-right-open',
21570 shown: function () {
21571 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'right';
21573 action: function () {
21574 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.RIGHT);
21578 var removePinAction = {
21579 name: 'ui.grid.pinning.unpin',
21580 title: i18nService.get().pinning.unpin,
21581 icon: 'ui-grid-icon-cancel',
21582 shown: function () {
21583 return typeof(this.context.col.renderContainer) !== 'undefined' && this.context.col.renderContainer !== null && this.context.col.renderContainer !== 'body';
21585 action: function () {
21586 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.UNPIN);
21590 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinLeft')) {
21591 col.menuItems.push(pinColumnLeftAction);
21593 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinRight')) {
21594 col.menuItems.push(pinColumnRightAction);
21596 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.unpin')) {
21597 col.menuItems.push(removePinAction);
21601 pinColumn: function(grid, col, container) {
21602 if (container === uiGridPinningConstants.container.NONE) {
21603 col.renderContainer = null;
21606 col.renderContainer = container;
21607 if (container === uiGridPinningConstants.container.LEFT) {
21608 grid.createLeftContainer();
21610 else if (container === uiGridPinningConstants.container.RIGHT) {
21611 grid.createRightContainer();
21617 grid.api.pinning.raise.columnPinned( col.colDef, container );
21625 module.directive('uiGridPinning', ['gridUtil', 'uiGridPinningService',
21626 function (gridUtil, uiGridPinningService) {
21630 compile: function () {
21632 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
21633 uiGridPinningService.initializeGrid(uiGridCtrl.grid);
21635 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21650 * @name ui.grid.resizeColumns
21653 * # ui.grid.resizeColumns
21655 * <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>
21657 * This module allows columns to be resized.
21659 var module = angular.module('ui.grid.resizeColumns', ['ui.grid']);
21661 module.service('uiGridResizeColumnsService', ['gridUtil', '$q', '$timeout',
21662 function (gridUtil, $q, $timeout) {
21665 defaultGridOptions: function(gridOptions){
21666 //default option to true unless it was explicitly set to false
21669 * @name ui.grid.resizeColumns.api:GridOptions
21671 * @description GridOptions for resizeColumns feature, these are available to be
21672 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21677 * @name enableColumnResizing
21678 * @propertyOf ui.grid.resizeColumns.api:GridOptions
21679 * @description Enable column resizing on the entire grid
21680 * <br/>Defaults to true
21682 gridOptions.enableColumnResizing = gridOptions.enableColumnResizing !== false;
21685 //use old name if it is explicitly false
21686 if (gridOptions.enableColumnResize === false){
21687 gridOptions.enableColumnResizing = false;
21691 colResizerColumnBuilder: function (colDef, col, gridOptions) {
21696 * @name ui.grid.resizeColumns.api:ColumnDef
21698 * @description ColumnDef for resizeColumns feature, these are available to be
21699 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
21704 * @name enableColumnResizing
21705 * @propertyOf ui.grid.resizeColumns.api:ColumnDef
21706 * @description Enable column resizing on an individual column
21707 * <br/>Defaults to GridOptions.enableColumnResizing
21709 //default to true unless gridOptions or colDef is explicitly false
21710 colDef.enableColumnResizing = colDef.enableColumnResizing === undefined ? gridOptions.enableColumnResizing : colDef.enableColumnResizing;
21713 //legacy support of old option name
21714 if (colDef.enableColumnResize === false){
21715 colDef.enableColumnResizing = false;
21718 return $q.all(promises);
21721 registerPublicApi: function (grid) {
21724 * @name ui.grid.resizeColumns.api:PublicApi
21725 * @description Public Api for column resize feature.
21731 * @name columnSizeChanged
21732 * @eventOf ui.grid.resizeColumns.api:PublicApi
21733 * @description raised when column is resized
21735 * gridApi.colResizable.on.columnSizeChanged(scope,function(colDef, deltaChange){})
21737 * @param {object} colDef the column that was resized
21738 * @param {integer} delta of the column size change
21741 columnSizeChanged: function (colDef, deltaChange) {
21746 grid.api.registerEventsFromObject(publicApi.events);
21749 fireColumnSizeChanged: function (grid, colDef, deltaChange) {
21750 $timeout(function () {
21751 if ( grid.api.colResizable ){
21752 grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange);
21754 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.");
21759 // get either this column, or the column next to this column, to resize,
21760 // returns the column we're going to resize
21761 findTargetCol: function(col, position, rtlMultiplier){
21762 var renderContainer = col.getRenderContainer();
21764 if (position === 'left') {
21765 // Get the column to the left of this one
21766 var colIndex = renderContainer.visibleColumnCache.indexOf(col);
21767 return renderContainer.visibleColumnCache[colIndex - 1 * rtlMultiplier];
21782 * @name ui.grid.resizeColumns.directive:uiGridResizeColumns
21786 * 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
21787 * option to false. This prevents resizing for the entire grid, regardless of individual columnDef options.
21790 <doc:example module="app">
21793 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
21795 app.controller('MainCtrl', ['$scope', function ($scope) {
21796 $scope.gridOpts = {
21798 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
21799 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
21800 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
21801 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
21807 <div ng-controller="MainCtrl">
21808 <div class="testGrid" ui-grid="gridOpts" ui-grid-resize-columns ></div>
21816 module.directive('uiGridResizeColumns', ['gridUtil', 'uiGridResizeColumnsService', function (gridUtil, uiGridResizeColumnsService) {
21820 require: '^uiGrid',
21822 compile: function () {
21824 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
21825 uiGridResizeColumnsService.defaultGridOptions(uiGridCtrl.grid.options);
21826 uiGridCtrl.grid.registerColumnBuilder( uiGridResizeColumnsService.colResizerColumnBuilder);
21827 uiGridResizeColumnsService.registerPublicApi(uiGridCtrl.grid);
21829 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21836 // Extend the uiGridHeaderCell directive
21837 module.directive('uiGridHeaderCell', ['gridUtil', '$templateCache', '$compile', '$q', 'uiGridResizeColumnsService', 'uiGridConstants', '$timeout', function (gridUtil, $templateCache, $compile, $q, uiGridResizeColumnsService, uiGridConstants, $timeout) {
21839 // Run after the original uiGridHeaderCell
21841 require: '^uiGrid',
21843 compile: function() {
21845 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21846 var grid = uiGridCtrl.grid;
21848 if (grid.options.enableColumnResizing) {
21849 var columnResizerElm = $templateCache.get('ui-grid/columnResizer');
21851 var rtlMultiplier = 1;
21852 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
21853 if (grid.isRTL()) {
21854 $scope.position = 'left';
21855 rtlMultiplier = -1;
21858 var displayResizers = function(){
21860 // remove any existing resizers.
21861 var resizers = $elm[0].getElementsByClassName('ui-grid-column-resizer');
21862 for ( var i = 0; i < resizers.length; i++ ){
21863 angular.element(resizers[i]).remove();
21866 // get the target column for the left resizer
21867 var otherCol = uiGridResizeColumnsService.findTargetCol($scope.col, 'left', rtlMultiplier);
21868 var renderContainer = $scope.col.getRenderContainer();
21870 // Don't append the left resizer if this is the first column or the column to the left of this one has resizing disabled
21871 if (otherCol && renderContainer.visibleColumnCache.indexOf($scope.col) !== 0 && otherCol.colDef.enableColumnResizing !== false) {
21872 var resizerLeft = angular.element(columnResizerElm).clone();
21873 resizerLeft.attr('position', 'left');
21875 $elm.prepend(resizerLeft);
21876 $compile(resizerLeft)($scope);
21879 // Don't append the right resizer if this column has resizing disabled
21880 if ($scope.col.colDef.enableColumnResizing !== false) {
21881 var resizerRight = angular.element(columnResizerElm).clone();
21882 resizerRight.attr('position', 'right');
21884 $elm.append(resizerRight);
21885 $compile(resizerRight)($scope);
21891 var waitDisplay = function(){
21892 $timeout(displayResizers);
21895 var dataChangeDereg = grid.registerDataChangeCallback( waitDisplay, [uiGridConstants.dataChange.COLUMN] );
21897 $scope.$on( '$destroy', dataChangeDereg );
21909 * @name ui.grid.resizeColumns.directive:uiGridColumnResizer
21914 * Draggable handle that controls column resizing.
21917 <doc:example module="app">
21920 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
21922 app.controller('MainCtrl', ['$scope', function ($scope) {
21923 $scope.gridOpts = {
21924 enableColumnResizing: true,
21926 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
21927 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
21928 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
21929 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
21935 <div ng-controller="MainCtrl">
21936 <div class="testGrid" ui-grid="gridOpts"></div>
21940 // TODO: e2e specs?
21942 // TODO: post-resize a horizontal scroll event should be fired
21946 module.directive('uiGridColumnResizer', ['$document', 'gridUtil', 'uiGridConstants', 'uiGridResizeColumnsService', function ($document, gridUtil, uiGridConstants, uiGridResizeColumnsService) {
21947 var resizeOverlay = angular.element('<div class="ui-grid-resize-overlay"></div>');
21956 require: '?^uiGrid',
21957 link: function ($scope, $elm, $attrs, uiGridCtrl) {
21963 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
21964 if (uiGridCtrl.grid.isRTL()) {
21965 $scope.position = 'left';
21966 rtlMultiplier = -1;
21969 if ($scope.position === 'left') {
21970 $elm.addClass('left');
21972 else if ($scope.position === 'right') {
21973 $elm.addClass('right');
21976 // Refresh the grid canvas
21977 // takes an argument representing the diff along the X-axis that the resize had
21978 function refreshCanvas(xDiff) {
21979 // Then refresh the grid canvas, rebuilding the styles so that the scrollbar updates its size
21980 uiGridCtrl.grid.refreshCanvas(true).then( function() {
21981 uiGridCtrl.grid.queueGridRefresh();
21985 // Check that the requested width isn't wider than the maxWidth, or narrower than the minWidth
21986 // Returns the new recommended with, after constraints applied
21987 function constrainWidth(col, width){
21988 var newWidth = width;
21990 // If the new width would be less than the column's allowably minimum width, don't allow it
21991 if (col.minWidth && newWidth < col.minWidth) {
21992 newWidth = col.minWidth;
21994 else if (col.maxWidth && newWidth > col.maxWidth) {
21995 newWidth = col.maxWidth;
22003 * Our approach to event handling aims to deal with both touch devices and mouse devices
22004 * We register down handlers on both touch and mouse. When a touchstart or mousedown event
22005 * occurs, we register the corresponding touchmove/touchend, or mousemove/mouseend events.
22007 * This way we can listen for both without worrying about the fact many touch devices also emulate
22008 * mouse events - basically whichever one we hear first is what we'll go with.
22010 function moveFunction(event, args) {
22011 if (event.originalEvent) { event = event.originalEvent; }
22012 event.preventDefault();
22014 x = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22016 if (x < 0) { x = 0; }
22017 else if (x > uiGridCtrl.grid.gridWidth) { x = uiGridCtrl.grid.gridWidth; }
22019 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22021 // Don't resize if it's disabled on this column
22022 if (col.colDef.enableColumnResizing === false) {
22026 if (!uiGridCtrl.grid.element.hasClass('column-resizing')) {
22027 uiGridCtrl.grid.element.addClass('column-resizing');
22030 // Get the diff along the X axis
22031 var xDiff = x - startX;
22033 // Get the width that this mouse would give the column
22034 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22036 // check we're not outside the allowable bounds for this column
22037 x = x + ( constrainWidth(col, newWidth) - newWidth ) * rtlMultiplier;
22039 resizeOverlay.css({ left: x + 'px' });
22041 uiGridCtrl.fireEvent(uiGridConstants.events.ITEM_DRAGGING);
22045 function upFunction(event, args) {
22046 if (event.originalEvent) { event = event.originalEvent; }
22047 event.preventDefault();
22049 uiGridCtrl.grid.element.removeClass('column-resizing');
22051 resizeOverlay.remove();
22053 // Resize the column
22054 x = (event.changedTouches ? event.changedTouches[0] : event).clientX - gridLeft;
22055 var xDiff = x - startX;
22058 // no movement, so just reset event handlers, including turning back on both
22059 // down events - we turned one off when this event started
22065 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22067 // Don't resize if it's disabled on this column
22068 if (col.colDef.enableColumnResizing === false) {
22072 // Get the new width
22073 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22075 // check we're not outside the allowable bounds for this column
22076 col.width = constrainWidth(col, newWidth);
22077 col.hasCustomWidth = true;
22079 refreshCanvas(xDiff);
22081 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff);
22083 // stop listening of up and move events - wait for next down
22084 // reset the down events - we will have turned one off when this event started
22090 var downFunction = function(event, args) {
22091 if (event.originalEvent) { event = event.originalEvent; }
22092 event.stopPropagation();
22094 // Get the left offset of the grid
22095 // gridLeft = uiGridCtrl.grid.element[0].offsetLeft;
22096 gridLeft = uiGridCtrl.grid.element[0].getBoundingClientRect().left;
22098 // Get the starting X position, which is the X coordinate of the click minus the grid's offset
22099 startX = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22101 // Append the resizer overlay
22102 uiGridCtrl.grid.element.append(resizeOverlay);
22104 // Place the resizer overlay at the start position
22105 resizeOverlay.css({ left: startX });
22107 // Add handlers for move and up events - if we were mousedown then we listen for mousemove and mouseup, if
22108 // we were touchdown then we listen for touchmove and touchup. Also remove the handler for the equivalent
22109 // down event - so if we're touchdown, then remove the mousedown handler until this event is over, if we're
22110 // mousedown then remove the touchdown handler until this event is over, this avoids processing duplicate events
22111 if ( event.type === 'touchstart' ){
22112 $document.on('touchend', upFunction);
22113 $document.on('touchmove', moveFunction);
22114 $elm.off('mousedown', downFunction);
22116 $document.on('mouseup', upFunction);
22117 $document.on('mousemove', moveFunction);
22118 $elm.off('touchstart', downFunction);
22122 var onDownEvents = function() {
22123 $elm.on('mousedown', downFunction);
22124 $elm.on('touchstart', downFunction);
22127 var offAllEvents = function() {
22128 $document.off('mouseup', upFunction);
22129 $document.off('touchend', upFunction);
22130 $document.off('mousemove', moveFunction);
22131 $document.off('touchmove', moveFunction);
22132 $elm.off('mousedown', downFunction);
22133 $elm.off('touchstart', downFunction);
22139 // On doubleclick, resize to fit all rendered cells
22140 var dblClickFn = function(event, args){
22141 event.stopPropagation();
22143 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22145 // Don't resize if it's disabled on this column
22146 if (col.colDef.enableColumnResizing === false) {
22150 // Go through the rendered rows and find out the max size for the data in this column
22154 // Get the parent render container element
22155 var renderContainerElm = gridUtil.closestElm($elm, '.ui-grid-render-container');
22157 // 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
22158 var cells = renderContainerElm.querySelectorAll('.' + uiGridConstants.COL_CLASS_PREFIX + col.uid + ' .ui-grid-cell-contents');
22159 Array.prototype.forEach.call(cells, function (cell) {
22160 // Get the cell width
22161 // gridUtil.logDebug('width', gridUtil.elementWidth(cell));
22163 // Account for the menu button if it exists
22165 if (angular.element(cell).parent().hasClass('ui-grid-header-cell')) {
22166 menuButton = angular.element(cell).parent()[0].querySelectorAll('.ui-grid-column-menu-button');
22169 gridUtil.fakeElement(cell, {}, function(newElm) {
22170 // Make the element float since it's a div and can expand to fill its container
22171 var e = angular.element(newElm);
22172 e.attr('style', 'float: left');
22174 var width = gridUtil.elementWidth(e);
22177 var menuButtonWidth = gridUtil.elementWidth(menuButton);
22178 width = width + menuButtonWidth;
22181 if (width > maxWidth) {
22183 xDiff = maxWidth - width;
22188 // check we're not outside the allowable bounds for this column
22189 col.width = constrainWidth(col, maxWidth);
22190 col.hasCustomWidth = true;
22192 refreshCanvas(xDiff);
22194 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); };
22195 $elm.on('dblclick', dblClickFn);
22197 $elm.on('$destroy', function() {
22198 $elm.off('dblclick', dblClickFn);
22214 * @name ui.grid.rowEdit
22217 * # ui.grid.rowEdit
22219 * <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>
22221 * This module extends the edit feature to provide tracking and saving of rows
22222 * of data. The tutorial provides more information on how this feature is best
22223 * used {@link tutorial/205_row_editable here}.
22225 * This feature depends on usage of the ui-grid-edit feature, and also benefits
22226 * from use of ui-grid-cellNav to provide the full spreadsheet-like editing
22231 var module = angular.module('ui.grid.rowEdit', ['ui.grid', 'ui.grid.edit', 'ui.grid.cellNav']);
22235 * @name ui.grid.rowEdit.constant:uiGridRowEditConstants
22237 * @description constants available in row edit module
22239 module.constant('uiGridRowEditConstants', {
22244 * @name ui.grid.rowEdit.service:uiGridRowEditService
22246 * @description Services for row editing features
22248 module.service('uiGridRowEditService', ['$interval', '$q', 'uiGridConstants', 'uiGridRowEditConstants', 'gridUtil',
22249 function ($interval, $q, uiGridConstants, uiGridRowEditConstants, gridUtil) {
22253 initializeGrid: function (scope, grid) {
22256 * @name ui.grid.rowEdit.api:PublicApi
22258 * @description Public Api for rowEdit feature
22268 * @eventOf ui.grid.rowEdit.api:PublicApi
22270 * @description raised when a row is ready for saving. Once your
22271 * row has saved you may need to use angular.extend to update the
22272 * data entity with any changed data from your save (for example,
22273 * lock version information if you're using optimistic locking,
22274 * or last update time/user information).
22276 * Your method should call setSavePromise somewhere in the body before
22277 * returning control. The feature will then wait, with the gridRow greyed out
22278 * whilst this promise is being resolved.
22281 * gridApi.rowEdit.on.saveRow(scope,function(rowEntity){})
22283 * and somewhere within the event handler:
22285 * gridApi.rowEdit.setSavePromise( rowEntity, savePromise)
22287 * @param {object} rowEntity the options.data element that was edited
22288 * @returns {promise} Your saveRow method should return a promise, the
22289 * promise should either be resolved (implying successful save), or
22290 * rejected (implying an error).
22292 saveRow: function (rowEntity) {
22300 * @methodOf ui.grid.rowEdit.api:PublicApi
22301 * @name setSavePromise
22302 * @description Sets the promise associated with the row save, mandatory that
22303 * the saveRow event handler calls this method somewhere before returning.
22305 * gridApi.rowEdit.setSavePromise(rowEntity, savePromise)
22307 * @param {object} rowEntity a data row from the grid for which a save has
22309 * @param {promise} savePromise the promise that will be resolved when the
22310 * save is successful, or rejected if the save fails
22313 setSavePromise: function ( rowEntity, savePromise) {
22314 service.setSavePromise(grid, rowEntity, savePromise);
22318 * @methodOf ui.grid.rowEdit.api:PublicApi
22319 * @name getDirtyRows
22320 * @description Returns all currently dirty rows
22322 * gridApi.rowEdit.getDirtyRows(grid)
22324 * @returns {array} An array of gridRows that are currently dirty
22327 getDirtyRows: function () {
22328 return grid.rowEdit.dirtyRows ? grid.rowEdit.dirtyRows : [];
22332 * @methodOf ui.grid.rowEdit.api:PublicApi
22333 * @name getErrorRows
22334 * @description Returns all currently errored rows
22336 * gridApi.rowEdit.getErrorRows(grid)
22338 * @returns {array} An array of gridRows that are currently in error
22341 getErrorRows: function () {
22342 return grid.rowEdit.errorRows ? grid.rowEdit.errorRows : [];
22346 * @methodOf ui.grid.rowEdit.api:PublicApi
22347 * @name flushDirtyRows
22348 * @description Triggers a save event for all currently dirty rows, could
22349 * be used where user presses a save button or navigates away from the page
22351 * gridApi.rowEdit.flushDirtyRows(grid)
22353 * @returns {promise} a promise that represents the aggregate of all
22354 * of the individual save promises - i.e. it will be resolved when all
22355 * the individual save promises have been resolved.
22358 flushDirtyRows: function () {
22359 return service.flushDirtyRows(grid);
22364 * @methodOf ui.grid.rowEdit.api:PublicApi
22365 * @name setRowsDirty
22366 * @description Sets each of the rows passed in dataRows
22367 * to be dirty. note that if you have only just inserted the
22368 * rows into your data you will need to wait for a $digest cycle
22369 * before the gridRows are present - so often you would wrap this
22370 * call in a $interval or $timeout
22372 * $interval( function() {
22373 * gridApi.rowEdit.setRowsDirty(myDataRows);
22376 * @param {array} dataRows the data entities for which the gridRows
22377 * should be set dirty.
22380 setRowsDirty: function ( dataRows) {
22381 service.setRowsDirty(grid, dataRows);
22386 * @methodOf ui.grid.rowEdit.api:PublicApi
22387 * @name setRowsClean
22388 * @description Sets each of the rows passed in dataRows
22389 * to be clean, removing them from the dirty cache and the error cache,
22390 * and clearing the error flag and the dirty flag
22392 * var gridRows = $scope.gridApi.rowEdit.getDirtyRows();
22393 * var dataRows = gridRows.map( function( gridRow ) { return gridRow.entity; });
22394 * $scope.gridApi.rowEdit.setRowsClean( dataRows );
22396 * @param {array} dataRows the data entities for which the gridRows
22397 * should be set clean.
22400 setRowsClean: function ( dataRows) {
22401 service.setRowsClean(grid, dataRows);
22407 grid.api.registerEventsFromObject(publicApi.events);
22408 grid.api.registerMethodsFromObject(publicApi.methods);
22410 grid.api.core.on.renderingComplete( scope, function ( gridApi ) {
22411 grid.api.edit.on.afterCellEdit( scope, service.endEditCell );
22412 grid.api.edit.on.beginCellEdit( scope, service.beginEditCell );
22413 grid.api.edit.on.cancelCellEdit( scope, service.cancelEditCell );
22415 if ( grid.api.cellNav ) {
22416 grid.api.cellNav.on.navigate( scope, service.navigate );
22422 defaultGridOptions: function (gridOptions) {
22426 * @name ui.grid.rowEdit.api:GridOptions
22428 * @description Options for configuring the rowEdit feature, these are available to be
22429 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
22437 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22439 * @description Returns a function that saves the specified row from the grid,
22440 * and returns a promise
22441 * @param {object} grid the grid for which dirty rows should be flushed
22442 * @param {GridRow} gridRow the row that should be saved
22443 * @returns {function} the saveRow function returns a function. That function
22444 * in turn, when called, returns a promise relating to the save callback
22446 saveRow: function ( grid, gridRow ) {
22449 return function() {
22450 gridRow.isSaving = true;
22452 if ( gridRow.rowEditSavePromise ){
22453 // don't save the row again if it's already saving - that causes stale object exceptions
22454 return gridRow.rowEditSavePromise;
22457 var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity );
22459 if ( gridRow.rowEditSavePromise ){
22460 gridRow.rowEditSavePromise.then( self.processSuccessPromise( grid, gridRow ), self.processErrorPromise( grid, gridRow ));
22462 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' );
22471 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22472 * @name setSavePromise
22473 * @description Sets the promise associated with the row save, mandatory that
22474 * the saveRow event handler calls this method somewhere before returning.
22476 * gridApi.rowEdit.setSavePromise(grid, rowEntity)
22478 * @param {object} grid the grid for which dirty rows should be returned
22479 * @param {object} rowEntity a data row from the grid for which a save has
22481 * @param {promise} savePromise the promise that will be resolved when the
22482 * save is successful, or rejected if the save fails
22485 setSavePromise: function (grid, rowEntity, savePromise) {
22486 var gridRow = grid.getRow( rowEntity );
22487 gridRow.rowEditSavePromise = savePromise;
22493 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22494 * @name processSuccessPromise
22495 * @description Returns a function that processes the successful
22496 * resolution of a save promise
22497 * @param {object} grid the grid for which the promise should be processed
22498 * @param {GridRow} gridRow the row that has been saved
22499 * @returns {function} the success handling function
22501 processSuccessPromise: function ( grid, gridRow ) {
22504 return function() {
22505 delete gridRow.isSaving;
22506 delete gridRow.isDirty;
22507 delete gridRow.isError;
22508 delete gridRow.rowEditSaveTimer;
22509 delete gridRow.rowEditSavePromise;
22510 self.removeRow( grid.rowEdit.errorRows, gridRow );
22511 self.removeRow( grid.rowEdit.dirtyRows, gridRow );
22518 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22519 * @name processErrorPromise
22520 * @description Returns a function that processes the failed
22521 * resolution of a save promise
22522 * @param {object} grid the grid for which the promise should be processed
22523 * @param {GridRow} gridRow the row that is now in error
22524 * @returns {function} the error handling function
22526 processErrorPromise: function ( grid, gridRow ) {
22527 return function() {
22528 delete gridRow.isSaving;
22529 delete gridRow.rowEditSaveTimer;
22530 delete gridRow.rowEditSavePromise;
22532 gridRow.isError = true;
22534 if (!grid.rowEdit.errorRows){
22535 grid.rowEdit.errorRows = [];
22537 if (!service.isRowPresent( grid.rowEdit.errorRows, gridRow ) ){
22538 grid.rowEdit.errorRows.push( gridRow );
22546 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22548 * @description Removes a row from a cache of rows - either
22549 * grid.rowEdit.errorRows or grid.rowEdit.dirtyRows. If the row
22550 * is not present silently does nothing.
22551 * @param {array} rowArray the array from which to remove the row
22552 * @param {GridRow} gridRow the row that should be removed
22554 removeRow: function( rowArray, removeGridRow ){
22555 if (typeof(rowArray) === 'undefined' || rowArray === null){
22559 rowArray.forEach( function( gridRow, index ){
22560 if ( gridRow.uid === removeGridRow.uid ){
22561 rowArray.splice( index, 1);
22569 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22570 * @name isRowPresent
22571 * @description Checks whether a row is already present
22572 * in the given array
22573 * @param {array} rowArray the array in which to look for the row
22574 * @param {GridRow} gridRow the row that should be looked for
22576 isRowPresent: function( rowArray, removeGridRow ){
22577 var present = false;
22578 rowArray.forEach( function( gridRow, index ){
22579 if ( gridRow.uid === removeGridRow.uid ){
22589 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22590 * @name flushDirtyRows
22591 * @description Triggers a save event for all currently dirty rows, could
22592 * be used where user presses a save button or navigates away from the page
22594 * gridApi.rowEdit.flushDirtyRows(grid)
22596 * @param {object} grid the grid for which dirty rows should be flushed
22597 * @returns {promise} a promise that represents the aggregate of all
22598 * of the individual save promises - i.e. it will be resolved when all
22599 * the individual save promises have been resolved.
22602 flushDirtyRows: function(grid){
22604 grid.api.rowEdit.getDirtyRows().forEach( function( gridRow ){
22605 service.saveRow( grid, gridRow )();
22606 promises.push( gridRow.rowEditSavePromise );
22609 return $q.all( promises );
22615 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22616 * @name endEditCell
22617 * @description Receives an afterCellEdit event from the edit function,
22618 * and sets flags as appropriate. Only the rowEntity parameter
22619 * is processed, although other params are available. Grid
22620 * is automatically provided by the gridApi.
22621 * @param {object} rowEntity the data entity for which the cell
22624 endEditCell: function( rowEntity, colDef, newValue, previousValue ){
22625 var grid = this.grid;
22626 var gridRow = grid.getRow( rowEntity );
22627 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, dirty flag cannot be set' ); return; }
22629 if ( newValue !== previousValue || gridRow.isDirty ){
22630 if ( !grid.rowEdit.dirtyRows ){
22631 grid.rowEdit.dirtyRows = [];
22634 if ( !gridRow.isDirty ){
22635 gridRow.isDirty = true;
22636 grid.rowEdit.dirtyRows.push( gridRow );
22639 delete gridRow.isError;
22641 service.considerSetTimer( grid, gridRow );
22648 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22649 * @name beginEditCell
22650 * @description Receives a beginCellEdit event from the edit function,
22651 * and cancels any rowEditSaveTimers if present, as the user is still editing
22652 * this row. Only the rowEntity parameter
22653 * is processed, although other params are available. Grid
22654 * is automatically provided by the gridApi.
22655 * @param {object} rowEntity the data entity for which the cell
22656 * editing has commenced
22658 beginEditCell: function( rowEntity, colDef ){
22659 var grid = this.grid;
22660 var gridRow = grid.getRow( rowEntity );
22661 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be cancelled' ); return; }
22663 service.cancelTimer( grid, gridRow );
22669 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22670 * @name cancelEditCell
22671 * @description Receives a cancelCellEdit event from the edit function,
22672 * and if the row was already dirty, restarts the save timer. If the row
22673 * was not already dirty, then it's not dirty now either and does nothing.
22675 * Only the rowEntity parameter
22676 * is processed, although other params are available. Grid
22677 * is automatically provided by the gridApi.
22679 * @param {object} rowEntity the data entity for which the cell
22680 * editing was cancelled
22682 cancelEditCell: function( rowEntity, colDef ){
22683 var grid = this.grid;
22684 var gridRow = grid.getRow( rowEntity );
22685 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be set' ); return; }
22687 service.considerSetTimer( grid, gridRow );
22693 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22695 * @description cellNav tells us that the selected cell has changed. If
22696 * the new row had a timer running, then stop it similar to in a beginCellEdit
22697 * call. If the old row is dirty and not the same as the new row, then
22698 * start a timer on it.
22699 * @param {object} newRowCol the row and column that were selected
22700 * @param {object} oldRowCol the row and column that was left
22703 navigate: function( newRowCol, oldRowCol ){
22704 var grid = this.grid;
22705 if ( newRowCol.row.rowEditSaveTimer ){
22706 service.cancelTimer( grid, newRowCol.row );
22709 if ( oldRowCol && oldRowCol.row && oldRowCol.row !== newRowCol.row ){
22710 service.considerSetTimer( grid, oldRowCol.row );
22717 * @propertyOf ui.grid.rowEdit.api:GridOptions
22718 * @name rowEditWaitInterval
22719 * @description How long the grid should wait for another change on this row
22720 * before triggering a save (in milliseconds). If set to -1, then saves are
22721 * never triggered by timer (implying that the user will call flushDirtyRows()
22725 * Setting the wait interval to 4 seconds
22727 * $scope.gridOptions = { rowEditWaitInterval: 4000 }
22733 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22734 * @name considerSetTimer
22735 * @description Consider setting a timer on this row (if it is dirty). if there is a timer running
22736 * on the row and the row isn't currently saving, cancel it, using cancelTimer, then if the row is
22737 * dirty and not currently saving then set a new timer
22738 * @param {object} grid the grid for which we are processing
22739 * @param {GridRow} gridRow the row for which the timer should be adjusted
22742 considerSetTimer: function( grid, gridRow ){
22743 service.cancelTimer( grid, gridRow );
22745 if ( gridRow.isDirty && !gridRow.isSaving ){
22746 if ( grid.options.rowEditWaitInterval !== -1 ){
22747 var waitTime = grid.options.rowEditWaitInterval ? grid.options.rowEditWaitInterval : 2000;
22748 gridRow.rowEditSaveTimer = $interval( service.saveRow( grid, gridRow ), waitTime, 1);
22756 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22757 * @name cancelTimer
22758 * @description cancel the $interval for any timer running on this row
22759 * then delete the timer itself
22760 * @param {object} grid the grid for which we are processing
22761 * @param {GridRow} gridRow the row for which the timer should be adjusted
22764 cancelTimer: function( grid, gridRow ){
22765 if ( gridRow.rowEditSaveTimer && !gridRow.isSaving ){
22766 $interval.cancel(gridRow.rowEditSaveTimer);
22767 delete gridRow.rowEditSaveTimer;
22774 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22775 * @name setRowsDirty
22776 * @description Sets each of the rows passed in dataRows
22777 * to be dirty. note that if you have only just inserted the
22778 * rows into your data you will need to wait for a $digest cycle
22779 * before the gridRows are present - so often you would wrap this
22780 * call in a $interval or $timeout
22782 * $interval( function() {
22783 * gridApi.rowEdit.setRowsDirty( myDataRows);
22786 * @param {object} grid the grid for which rows should be set dirty
22787 * @param {array} dataRows the data entities for which the gridRows
22788 * should be set dirty.
22791 setRowsDirty: function( grid, myDataRows ) {
22793 myDataRows.forEach( function( value, index ){
22794 gridRow = grid.getRow( value );
22796 if ( !grid.rowEdit.dirtyRows ){
22797 grid.rowEdit.dirtyRows = [];
22800 if ( !gridRow.isDirty ){
22801 gridRow.isDirty = true;
22802 grid.rowEdit.dirtyRows.push( gridRow );
22805 delete gridRow.isError;
22807 service.considerSetTimer( grid, gridRow );
22809 gridUtil.logError( "requested row not found in rowEdit.setRowsDirty, row was: " + value );
22817 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22818 * @name setRowsClean
22819 * @description Sets each of the rows passed in dataRows
22820 * to be clean, clearing the dirty flag and the error flag, and removing
22821 * the rows from the dirty and error caches.
22822 * @param {object} grid the grid for which rows should be set clean
22823 * @param {array} dataRows the data entities for which the gridRows
22824 * should be set clean.
22827 setRowsClean: function( grid, myDataRows ) {
22830 myDataRows.forEach( function( value, index ){
22831 gridRow = grid.getRow( value );
22833 delete gridRow.isDirty;
22834 service.removeRow( grid.rowEdit.dirtyRows, gridRow );
22835 service.cancelTimer( grid, gridRow );
22837 delete gridRow.isError;
22838 service.removeRow( grid.rowEdit.errorRows, gridRow );
22840 gridUtil.logError( "requested row not found in rowEdit.setRowsClean, row was: " + value );
22853 * @name ui.grid.rowEdit.directive:uiGridEdit
22857 * @description Adds row editing features to the ui-grid-edit directive.
22860 module.directive('uiGridRowEdit', ['gridUtil', 'uiGridRowEditService', 'uiGridEditConstants',
22861 function (gridUtil, uiGridRowEditService, uiGridEditConstants) {
22865 require: '^uiGrid',
22867 compile: function () {
22869 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
22870 uiGridRowEditService.initializeGrid($scope, uiGridCtrl.grid);
22872 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22882 * @name ui.grid.rowEdit.directive:uiGridViewport
22885 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
22886 * for the grid row to allow coloring of saving and error rows
22888 module.directive('uiGridViewport',
22889 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
22890 function ($compile, uiGridConstants, gridUtil, $parse) {
22892 priority: -200, // run after default directive
22894 compile: function ($elm, $attrs) {
22895 var rowRepeatDiv = angular.element($elm.children().children()[0]);
22897 var existingNgClass = rowRepeatDiv.attr("ng-class");
22898 var newNgClass = '';
22899 if ( existingNgClass ) {
22900 newNgClass = existingNgClass.slice(0, -1) + ", 'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
22902 newNgClass = "{'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
22904 rowRepeatDiv.attr("ng-class", newNgClass);
22907 pre: function ($scope, $elm, $attrs, controllers) {
22910 post: function ($scope, $elm, $attrs, controllers) {
22924 * @name ui.grid.saveState
22927 * # ui.grid.saveState
22929 * <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>
22931 * This module provides the ability to save the grid state, and restore
22932 * it when the user returns to the page.
22934 * No UI is provided, the caller should provide their own UI/buttons
22935 * as appropriate. Usually the navigate events would be used to save
22936 * the grid state and restore it.
22941 * <div doc-module-components="ui.grid.save-state"></div>
22944 var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning', 'ui.grid.treeView']);
22948 * @name ui.grid.saveState.constant:uiGridSaveStateConstants
22950 * @description constants available in save state module
22953 module.constant('uiGridSaveStateConstants', {
22954 featureName: 'saveState'
22959 * @name ui.grid.saveState.service:uiGridSaveStateService
22961 * @description Services for saveState feature
22963 module.service('uiGridSaveStateService', ['$q', 'uiGridSaveStateConstants', 'gridUtil', '$compile', '$interval', 'uiGridConstants',
22964 function ($q, uiGridSaveStateConstants, gridUtil, $compile, $interval, uiGridConstants ) {
22968 initializeGrid: function (grid) {
22970 //add feature namespace and any properties to grid for needed state
22971 grid.saveState = {};
22972 this.defaultGridOptions(grid.options);
22976 * @name ui.grid.saveState.api:PublicApi
22978 * @description Public Api for saveState feature
22990 * @methodOf ui.grid.saveState.api:PublicApi
22991 * @description Packages the current state of the grid into
22992 * an object, and provides it to the user for saving
22993 * @returns {object} the state as a javascript object that can be saved
22995 save: function () {
22996 return service.save(grid);
23001 * @methodOf ui.grid.saveState.api:PublicApi
23002 * @description Restores the provided state into the grid
23003 * @param {scope} $scope a scope that we can broadcast on
23004 * @param {object} state the state that should be restored into the grid
23006 restore: function ( $scope, state) {
23007 service.restore(grid, $scope, state);
23013 grid.api.registerEventsFromObject(publicApi.events);
23015 grid.api.registerMethodsFromObject(publicApi.methods);
23019 defaultGridOptions: function (gridOptions) {
23020 //default option to true unless it was explicitly set to false
23023 * @name ui.grid.saveState.api:GridOptions
23025 * @description GridOptions for saveState feature, these are available to be
23026 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
23031 * @propertyOf ui.grid.saveState.api:GridOptions
23032 * @description Save the current column widths. Note that unless
23033 * you've provided the user with some way to resize their columns (say
23034 * the resize columns feature), then this makes little sense.
23035 * <br/>Defaults to true
23037 gridOptions.saveWidths = gridOptions.saveWidths !== false;
23041 * @propertyOf ui.grid.saveState.api:GridOptions
23042 * @description Restore the current column order. Note that unless
23043 * you've provided the user with some way to reorder their columns (for
23044 * example the move columns feature), this makes little sense.
23045 * <br/>Defaults to true
23047 gridOptions.saveOrder = gridOptions.saveOrder !== false;
23051 * @propertyOf ui.grid.saveState.api:GridOptions
23052 * @description Save the current scroll position. Note that this
23053 * is saved as the percentage of the grid scrolled - so if your
23054 * user returns to a grid with a significantly different number of
23055 * rows (perhaps some data has been deleted) then the scroll won't
23056 * actually show the same rows as before. If you want to scroll to
23057 * a specific row then you should instead use the saveFocus option, which
23060 * Note that this element will only be saved if the cellNav feature is
23062 * <br/>Defaults to false
23064 gridOptions.saveScroll = gridOptions.saveScroll === true;
23068 * @propertyOf ui.grid.saveState.api:GridOptions
23069 * @description Save the current focused cell. On returning
23070 * to this focused cell we'll also scroll. This option is
23071 * preferred to the saveScroll option, so is set to true by
23072 * default. If saveScroll is set to true then this option will
23075 * By default this option saves the current row number and column
23076 * number, and returns to that row and column. However, if you define
23077 * a saveRowIdentity function, then it will return you to the currently
23078 * selected column within that row (in a business sense - so if some
23079 * rows have been deleted, it will still find the same data, presuming it
23080 * still exists in the list. If it isn't in the list then it will instead
23081 * return to the same row number - i.e. scroll percentage)
23083 * Note that this option will do nothing if the cellNav
23084 * feature is not enabled.
23086 * <br/>Defaults to true (unless saveScroll is true)
23088 gridOptions.saveFocus = gridOptions.saveScroll !== true && gridOptions.saveFocus !== false;
23091 * @name saveRowIdentity
23092 * @propertyOf ui.grid.saveState.api:GridOptions
23093 * @description A function that can be called, passing in a rowEntity,
23094 * and that will return a unique id for that row. This might simply
23095 * return the `id` field from that row (if you have one), or it might
23096 * concatenate some fields within the row to make a unique value.
23098 * This value will be used to find the same row again and set the focus
23099 * to it, if it exists when we return.
23101 * <br/>Defaults to undefined
23105 * @name saveVisible
23106 * @propertyOf ui.grid.saveState.api:GridOptions
23107 * @description Save whether or not columns are visible.
23109 * <br/>Defaults to true
23111 gridOptions.saveVisible = gridOptions.saveVisible !== false;
23115 * @propertyOf ui.grid.saveState.api:GridOptions
23116 * @description Save the current sort state for each column
23118 * <br/>Defaults to true
23120 gridOptions.saveSort = gridOptions.saveSort !== false;
23124 * @propertyOf ui.grid.saveState.api:GridOptions
23125 * @description Save the current filter state for each column
23127 * <br/>Defaults to true
23129 gridOptions.saveFilter = gridOptions.saveFilter !== false;
23132 * @name saveSelection
23133 * @propertyOf ui.grid.saveState.api:GridOptions
23134 * @description Save the currently selected rows. If the `saveRowIdentity` callback
23135 * is defined, then it will save the id of the row and select that. If not, then
23136 * it will attempt to select the rows by row number, which will give the wrong results
23137 * if the data set has changed in the mean-time.
23139 * Note that this option only does anything
23140 * if the selection feature is enabled.
23142 * <br/>Defaults to true
23144 gridOptions.saveSelection = gridOptions.saveSelection !== false;
23147 * @name saveGrouping
23148 * @propertyOf ui.grid.saveState.api:GridOptions
23149 * @description Save the grouping configuration. If set to true and the
23150 * grouping feature is not enabled then does nothing.
23152 * <br/>Defaults to true
23154 gridOptions.saveGrouping = gridOptions.saveGrouping !== false;
23157 * @name saveGroupingExpandedStates
23158 * @propertyOf ui.grid.saveState.api:GridOptions
23159 * @description Save the grouping row expanded states. If set to true and the
23160 * grouping feature is not enabled then does nothing.
23162 * This can be quite a bit of data, in many cases you wouldn't want to save this
23165 * <br/>Defaults to false
23167 gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true;
23170 * @name savePinning
23171 * @propertyOf ui.grid.saveState.api:GridOptions
23172 * @description Save pinning state for columns.
23174 * <br/>Defaults to true
23176 gridOptions.savePinning = gridOptions.savePinning !== false;
23179 * @name saveTreeView
23180 * @propertyOf ui.grid.saveState.api:GridOptions
23181 * @description Save the treeView configuration. If set to true and the
23182 * treeView feature is not enabled then does nothing.
23184 * <br/>Defaults to true
23186 gridOptions.saveTreeView = gridOptions.saveTreeView !== false;
23194 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23195 * @description Saves the current grid state into an object, and
23196 * passes that object back to the caller
23197 * @param {Grid} grid the grid whose state we'd like to save
23198 * @returns {object} the state ready to be saved
23200 save: function (grid) {
23201 var savedState = {};
23203 savedState.columns = service.saveColumns( grid );
23204 savedState.scrollFocus = service.saveScrollFocus( grid );
23205 savedState.selection = service.saveSelection( grid );
23206 savedState.grouping = service.saveGrouping( grid );
23207 savedState.treeView = service.saveTreeView( grid );
23216 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23217 * @description Applies the provided state to the grid
23219 * @param {Grid} grid the grid whose state we'd like to restore
23220 * @param {scope} $scope a scope that we can broadcast on
23221 * @param {object} state the state we'd like to restore
23223 restore: function( grid, $scope, state ){
23224 if ( state.columns ) {
23225 service.restoreColumns( grid, state.columns );
23228 if ( state.scrollFocus ){
23229 service.restoreScrollFocus( grid, $scope, state.scrollFocus );
23232 if ( state.selection ){
23233 service.restoreSelection( grid, state.selection );
23236 if ( state.grouping ){
23237 service.restoreGrouping( grid, state.grouping );
23240 if ( state.treeView ){
23241 service.restoreTreeView( grid, state.treeView );
23250 * @name saveColumns
23251 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23252 * @description Saves the column setup, including sort, filters, ordering,
23253 * pinning and column widths.
23255 * Works through the current columns, storing them in order. Stores the
23256 * column name, then the visible flag, width, sort and filters for each column.
23258 * @param {Grid} grid the grid whose state we'd like to save
23259 * @returns {array} the columns state ready to be saved
23261 saveColumns: function( grid ) {
23263 grid.getOnlyDataColumns().forEach( function( column ) {
23264 var savedColumn = {};
23265 savedColumn.name = column.name;
23267 if ( grid.options.saveVisible ){
23268 savedColumn.visible = column.visible;
23271 if ( grid.options.saveWidths ){
23272 savedColumn.width = column.width;
23275 // these two must be copied, not just pointed too - otherwise our saved state is pointing to the same object as current state
23276 if ( grid.options.saveSort ){
23277 savedColumn.sort = angular.copy( column.sort );
23280 if ( grid.options.saveFilter ){
23281 savedColumn.filters = [];
23282 column.filters.forEach( function( filter ){
23283 var copiedFilter = {};
23284 angular.forEach( filter, function( value, key) {
23285 if ( key !== 'condition' && key !== '$$hashKey' && key !== 'placeholder'){
23286 copiedFilter[key] = value;
23289 savedColumn.filters.push(copiedFilter);
23293 if ( !!grid.api.pinning && grid.options.savePinning ){
23294 savedColumn.pinned = column.renderContainer ? column.renderContainer : '';
23297 columns.push( savedColumn );
23306 * @name saveScrollFocus
23307 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23308 * @description Saves the currently scroll or focus.
23310 * If cellNav isn't present then does nothing - we can't return
23311 * to the scroll position without cellNav anyway.
23313 * If the cellNav module is present, and saveFocus is true, then
23314 * it saves the currently focused cell. If rowIdentity is present
23315 * then saves using rowIdentity, otherwise saves visibleRowNum.
23317 * If the cellNav module is not present, and saveScroll is true, then
23318 * it approximates the current scroll row and column, and saves that.
23320 * @param {Grid} grid the grid whose state we'd like to save
23321 * @returns {object} the selection state ready to be saved
23323 saveScrollFocus: function( grid ){
23324 if ( !grid.api.cellNav ){
23328 var scrollFocus = {};
23329 if ( grid.options.saveFocus ){
23330 scrollFocus.focus = true;
23331 var rowCol = grid.api.cellNav.getFocusedCell();
23332 if ( rowCol !== null ) {
23333 if ( rowCol.col !== null ){
23334 scrollFocus.colName = rowCol.col.colDef.name;
23336 if ( rowCol.row !== null ){
23337 scrollFocus.rowVal = service.getRowVal( grid, rowCol.row );
23342 if ( grid.options.saveScroll || grid.options.saveFocus && !scrollFocus.colName && !scrollFocus.rowVal ) {
23343 scrollFocus.focus = false;
23344 if ( grid.renderContainers.body.prevRowScrollIndex ){
23345 scrollFocus.rowVal = service.getRowVal( grid, grid.renderContainers.body.visibleRowCache[ grid.renderContainers.body.prevRowScrollIndex ]);
23348 if ( grid.renderContainers.body.prevColScrollIndex ){
23349 scrollFocus.colName = grid.renderContainers.body.visibleColumnCache[ grid.renderContainers.body.prevColScrollIndex ].name;
23353 return scrollFocus;
23359 * @name saveSelection
23360 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23361 * @description Saves the currently selected rows, if the selection feature is enabled
23362 * @param {Grid} grid the grid whose state we'd like to save
23363 * @returns {array} the selection state ready to be saved
23365 saveSelection: function( grid ){
23366 if ( !grid.api.selection || !grid.options.saveSelection ){
23370 var selection = grid.api.selection.getSelectedGridRows().map( function( gridRow ) {
23371 return service.getRowVal( grid, gridRow );
23380 * @name saveGrouping
23381 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23382 * @description Saves the grouping state, if the grouping feature is enabled
23383 * @param {Grid} grid the grid whose state we'd like to save
23384 * @returns {object} the grouping state ready to be saved
23386 saveGrouping: function( grid ){
23387 if ( !grid.api.grouping || !grid.options.saveGrouping ){
23391 return grid.api.grouping.getGrouping( grid.options.saveGroupingExpandedStates );
23397 * @name saveTreeView
23398 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23399 * @description Saves the tree view state, if the tree feature is enabled
23400 * @param {Grid} grid the grid whose state we'd like to save
23401 * @returns {object} the tree view state ready to be saved
23403 saveTreeView: function( grid ){
23404 if ( !grid.api.treeView || !grid.options.saveTreeView ){
23408 return grid.api.treeView.getTreeView();
23415 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23416 * @description Helper function that gets either the rowNum or
23417 * the saveRowIdentity, given a gridRow
23418 * @param {Grid} grid the grid the row is in
23419 * @param {GridRow} gridRow the row we want the rowNum for
23420 * @returns {object} an object containing { identity: true/false, row: rowNumber/rowIdentity }
23423 getRowVal: function( grid, gridRow ){
23429 if ( grid.options.saveRowIdentity ){
23430 rowVal.identity = true;
23431 rowVal.row = grid.options.saveRowIdentity( gridRow.entity );
23433 rowVal.identity = false;
23434 rowVal.row = grid.renderContainers.body.visibleRowCache.indexOf( gridRow );
23442 * @name restoreColumns
23443 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23444 * @description Restores the columns, including order, visible, width,
23445 * pinning, sort and filters.
23447 * @param {Grid} grid the grid whose state we'd like to restore
23448 * @param {object} columnsState the list of columns we had before, with their state
23450 restoreColumns: function( grid, columnsState ){
23451 var isSortChanged = false;
23453 columnsState.forEach( function( columnState, index ) {
23454 var currentCol = grid.getColumn( columnState.name );
23456 if ( currentCol && !grid.isRowHeaderColumn(currentCol) ){
23457 if ( grid.options.saveVisible &&
23458 ( currentCol.visible !== columnState.visible ||
23459 currentCol.colDef.visible !== columnState.visible ) ){
23460 currentCol.visible = columnState.visible;
23461 currentCol.colDef.visible = columnState.visible;
23462 grid.api.core.raise.columnVisibilityChanged(currentCol);
23465 if ( grid.options.saveWidths ){
23466 currentCol.width = columnState.width;
23469 if ( grid.options.saveSort &&
23470 !angular.equals(currentCol.sort, columnState.sort) &&
23471 !( currentCol.sort === undefined && angular.isEmpty(columnState.sort) ) ){
23472 currentCol.sort = angular.copy( columnState.sort );
23473 isSortChanged = true;
23476 if ( grid.options.saveFilter &&
23477 !angular.equals(currentCol.filters, columnState.filters ) ){
23478 columnState.filters.forEach( function( filter, index ){
23479 angular.extend( currentCol.filters[index], filter );
23480 if ( typeof(filter.term) === 'undefined' || filter.term === null ){
23481 delete currentCol.filters[index].term;
23484 grid.api.core.raise.filterChanged();
23487 if ( !!grid.api.pinning && grid.options.savePinning && currentCol.renderContainer !== columnState.pinned ){
23488 grid.api.pinning.pinColumn(currentCol, columnState.pinned);
23491 var currentIndex = grid.getOnlyDataColumns().indexOf( currentCol );
23492 if (currentIndex !== -1) {
23493 if (grid.options.saveOrder && currentIndex !== index) {
23494 var column = grid.columns.splice(currentIndex + grid.rowHeaderColumns.length, 1)[0];
23495 grid.columns.splice(index + grid.rowHeaderColumns.length, 0, column);
23501 if ( isSortChanged ) {
23502 grid.api.core.raise.sortChanged( grid, grid.getColumnSorting() );
23509 * @name restoreScrollFocus
23510 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23511 * @description Scrolls to the position that was saved. If focus is true, then
23512 * sets focus to the specified row/col. If focus is false, then scrolls to the
23513 * specified row/col.
23515 * @param {Grid} grid the grid whose state we'd like to restore
23516 * @param {scope} $scope a scope that we can broadcast on
23517 * @param {object} scrollFocusState the scroll/focus state ready to be restored
23519 restoreScrollFocus: function( grid, $scope, scrollFocusState ){
23520 if ( !grid.api.cellNav ){
23525 if ( scrollFocusState.colName ){
23526 var colDefs = grid.options.columnDefs.filter( function( colDef ) { return colDef.name === scrollFocusState.colName; });
23527 if ( colDefs.length > 0 ){
23528 colDef = colDefs[0];
23532 if ( scrollFocusState.rowVal && scrollFocusState.rowVal.row ){
23533 if ( scrollFocusState.rowVal.identity ){
23534 row = service.findRowByIdentity( grid, scrollFocusState.rowVal );
23536 row = grid.renderContainers.body.visibleRowCache[ scrollFocusState.rowVal.row ];
23540 var entity = row && row.entity ? row.entity : null ;
23542 if ( colDef || entity ) {
23543 if (scrollFocusState.focus ){
23544 grid.api.cellNav.scrollToFocus( entity, colDef );
23546 grid.scrollTo( entity, colDef );
23554 * @name restoreSelection
23555 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23556 * @description Selects the rows that are provided in the selection
23557 * state. If you are using `saveRowIdentity` and more than one row matches the identity
23558 * function then only the first is selected.
23559 * @param {Grid} grid the grid whose state we'd like to restore
23560 * @param {object} selectionState the selection state ready to be restored
23562 restoreSelection: function( grid, selectionState ){
23563 if ( !grid.api.selection ){
23567 grid.api.selection.clearSelectedRows();
23569 selectionState.forEach( function( rowVal ) {
23570 if ( rowVal.identity ){
23571 var foundRow = service.findRowByIdentity( grid, rowVal );
23574 grid.api.selection.selectRow( foundRow.entity );
23578 grid.api.selection.selectRowByVisibleIndex( rowVal.row );
23586 * @name restoreGrouping
23587 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23588 * @description Restores the grouping configuration, if the grouping feature
23590 * @param {Grid} grid the grid whose state we'd like to restore
23591 * @param {object} groupingState the grouping state ready to be restored
23593 restoreGrouping: function( grid, groupingState ){
23594 if ( !grid.api.grouping || typeof(groupingState) === 'undefined' || groupingState === null || angular.equals(groupingState, {}) ){
23598 grid.api.grouping.setGrouping( groupingState );
23603 * @name restoreTreeView
23604 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23605 * @description Restores the tree view configuration, if the tree view feature
23607 * @param {Grid} grid the grid whose state we'd like to restore
23608 * @param {object} treeViewState the tree view state ready to be restored
23610 restoreTreeView: function( grid, treeViewState ){
23611 if ( !grid.api.treeView || typeof(treeViewState) === 'undefined' || treeViewState === null || angular.equals(treeViewState, {}) ){
23615 grid.api.treeView.setTreeView( treeViewState );
23620 * @name findRowByIdentity
23621 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23622 * @description Finds a row given it's identity value, returns the first found row
23623 * if any are found, otherwise returns null if no rows are found.
23624 * @param {Grid} grid the grid whose state we'd like to restore
23625 * @param {object} rowVal the row we'd like to find
23626 * @returns {gridRow} the found row, or null if none found
23628 findRowByIdentity: function( grid, rowVal ){
23629 if ( !grid.options.saveRowIdentity ){
23633 var filteredRows = grid.rows.filter( function( gridRow ) {
23634 if ( grid.options.saveRowIdentity( gridRow.entity ) === rowVal.row ){
23641 if ( filteredRows.length > 0 ){
23642 return filteredRows[0];
23656 * @name ui.grid.saveState.directive:uiGridSaveState
23660 * @description Adds saveState features to grid
23663 <example module="app">
23664 <file name="app.js">
23665 var app = angular.module('app', ['ui.grid', 'ui.grid.saveState']);
23667 app.controller('MainCtrl', ['$scope', function ($scope) {
23669 { name: 'Bob', title: 'CEO' },
23670 { name: 'Frank', title: 'Lowly Developer' }
23673 $scope.gridOptions = {
23676 {name: 'title', enableCellEdit: true}
23682 <file name="index.html">
23683 <div ng-controller="MainCtrl">
23684 <div ui-grid="gridOptions" ui-grid-save-state></div>
23689 module.directive('uiGridSaveState', ['uiGridSaveStateConstants', 'uiGridSaveStateService', 'gridUtil', '$compile',
23690 function (uiGridSaveStateConstants, uiGridSaveStateService, gridUtil, $compile) {
23694 require: '^uiGrid',
23696 link: function ($scope, $elm, $attrs, uiGridCtrl) {
23697 uiGridSaveStateService.initializeGrid(uiGridCtrl.grid);
23709 * @name ui.grid.selection
23712 * # ui.grid.selection
23713 * This module provides row selection
23715 * <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>
23717 * <div doc-module-components="ui.grid.selection"></div>
23720 var module = angular.module('ui.grid.selection', ['ui.grid']);
23724 * @name ui.grid.selection.constant:uiGridSelectionConstants
23726 * @description constants available in selection module
23728 module.constant('uiGridSelectionConstants', {
23729 featureName: "selection",
23730 selectionRowHeaderColName: 'selectionRowHeaderCol'
23733 //add methods to GridRow
23734 angular.module('ui.grid').config(['$provide', function($provide) {
23735 $provide.decorator('GridRow', ['$delegate', function($delegate) {
23739 * @name ui.grid.selection.api:GridRow
23741 * @description GridRow prototype functions added for selection
23746 * @name enableSelection
23747 * @propertyOf ui.grid.selection.api:GridRow
23748 * @description Enable row selection for this row, only settable by internal code.
23750 * The grouping feature, for example, might set group header rows to not be selectable.
23751 * <br/>Defaults to true
23757 * @propertyOf ui.grid.selection.api:GridRow
23758 * @description Selected state of row. Should be readonly. Make any changes to selected state using setSelected().
23759 * <br/>Defaults to false
23765 * @name setSelected
23766 * @methodOf ui.grid.selection.api:GridRow
23767 * @description Sets the isSelected property and updates the selectedCount
23768 * Changes to isSelected state should only be made via this function
23769 * @param {bool} selected value to set
23771 $delegate.prototype.setSelected = function(selected) {
23772 this.isSelected = selected;
23774 this.grid.selection.selectedCount++;
23777 this.grid.selection.selectedCount--;
23787 * @name ui.grid.selection.service:uiGridSelectionService
23789 * @description Services for selection features
23791 module.service('uiGridSelectionService', ['$q', '$templateCache', 'uiGridSelectionConstants', 'gridUtil',
23792 function ($q, $templateCache, uiGridSelectionConstants, gridUtil) {
23796 initializeGrid: function (grid) {
23798 //add feature namespace and any properties to grid for needed
23801 * @name ui.grid.selection.grid:selection
23803 * @description Grid properties and functions added for selection
23805 grid.selection = {};
23806 grid.selection.lastSelectedRow = null;
23807 grid.selection.selectAll = false;
23812 * @name selectedCount
23813 * @propertyOf ui.grid.selection.grid:selection
23814 * @description Current count of selected rows
23816 * var count = grid.selection.selectedCount
23818 grid.selection.selectedCount = 0;
23820 service.defaultGridOptions(grid.options);
23824 * @name ui.grid.selection.api:PublicApi
23826 * @description Public Api for selection feature
23833 * @name rowSelectionChanged
23834 * @eventOf ui.grid.selection.api:PublicApi
23835 * @description is raised after the row.isSelected state is changed
23836 * @param {GridRow} row the row that was selected/deselected
23837 * @param {Event} event object if raised from an event
23839 rowSelectionChanged: function (scope, row, evt) {
23843 * @name rowSelectionChangedBatch
23844 * @eventOf ui.grid.selection.api:PublicApi
23845 * @description is raised after the row.isSelected state is changed
23846 * in bulk, if the `enableSelectionBatchEvent` option is set to true
23847 * (which it is by default). This allows more efficient processing
23849 * @param {array} rows the rows that were selected/deselected
23850 * @param {Event} event object if raised from an event
23852 rowSelectionChangedBatch: function (scope, rows, evt) {
23860 * @name toggleRowSelection
23861 * @methodOf ui.grid.selection.api:PublicApi
23862 * @description Toggles data row as selected or unselected
23863 * @param {object} rowEntity gridOptions.data[] array instance
23864 * @param {Event} event object if raised from an event
23866 toggleRowSelection: function (rowEntity, evt) {
23867 var row = grid.getRow(rowEntity);
23868 if (row !== null) {
23869 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23875 * @methodOf ui.grid.selection.api:PublicApi
23876 * @description Select the data row
23877 * @param {object} rowEntity gridOptions.data[] array instance
23878 * @param {Event} event object if raised from an event
23880 selectRow: function (rowEntity, evt) {
23881 var row = grid.getRow(rowEntity);
23882 if (row !== null && !row.isSelected) {
23883 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23888 * @name selectRowByVisibleIndex
23889 * @methodOf ui.grid.selection.api:PublicApi
23890 * @description Select the specified row by visible index (i.e. if you
23891 * specify row 0 you'll get the first visible row selected). In this context
23892 * visible means of those rows that are theoretically visible (i.e. not filtered),
23893 * rather than rows currently rendered on the screen.
23894 * @param {number} index index within the rowsVisible array
23895 * @param {Event} event object if raised from an event
23897 selectRowByVisibleIndex: function ( rowNum, evt ) {
23898 var row = grid.renderContainers.body.visibleRowCache[rowNum];
23899 if (row !== null && typeof(row) !== 'undefined' && !row.isSelected) {
23900 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23905 * @name unSelectRow
23906 * @methodOf ui.grid.selection.api:PublicApi
23907 * @description UnSelect the data row
23908 * @param {object} rowEntity gridOptions.data[] array instance
23909 * @param {Event} event object if raised from an event
23911 unSelectRow: function (rowEntity, evt) {
23912 var row = grid.getRow(rowEntity);
23913 if (row !== null && row.isSelected) {
23914 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23919 * @name selectAllRows
23920 * @methodOf ui.grid.selection.api:PublicApi
23921 * @description Selects all rows. Does nothing if multiSelect = false
23922 * @param {Event} event object if raised from an event
23924 selectAllRows: function (evt) {
23925 if (grid.options.multiSelect === false) {
23929 var changedRows = [];
23930 grid.rows.forEach(function (row) {
23931 if ( !row.isSelected && row.enableSelection !== false ){
23932 row.setSelected(true);
23933 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
23936 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
23937 grid.selection.selectAll = true;
23941 * @name selectAllVisibleRows
23942 * @methodOf ui.grid.selection.api:PublicApi
23943 * @description Selects all visible rows. Does nothing if multiSelect = false
23944 * @param {Event} event object if raised from an event
23946 selectAllVisibleRows: function (evt) {
23947 if (grid.options.multiSelect === false) {
23951 var changedRows = [];
23952 grid.rows.forEach(function (row) {
23954 if (!row.isSelected && row.enableSelection !== false){
23955 row.setSelected(true);
23956 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
23959 if (row.isSelected){
23960 row.setSelected(false);
23961 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
23965 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
23966 grid.selection.selectAll = true;
23970 * @name clearSelectedRows
23971 * @methodOf ui.grid.selection.api:PublicApi
23972 * @description Unselects all rows
23973 * @param {Event} event object if raised from an event
23975 clearSelectedRows: function (evt) {
23976 service.clearSelectedRows(grid, evt);
23980 * @name getSelectedRows
23981 * @methodOf ui.grid.selection.api:PublicApi
23982 * @description returns all selectedRow's entity references
23984 getSelectedRows: function () {
23985 return service.getSelectedRows(grid).map(function (gridRow) {
23986 return gridRow.entity;
23991 * @name getSelectedGridRows
23992 * @methodOf ui.grid.selection.api:PublicApi
23993 * @description returns all selectedRow's as gridRows
23995 getSelectedGridRows: function () {
23996 return service.getSelectedRows(grid);
24000 * @name setMultiSelect
24001 * @methodOf ui.grid.selection.api:PublicApi
24002 * @description Sets the current gridOption.multiSelect to true or false
24003 * @param {bool} multiSelect true to allow multiple rows
24005 setMultiSelect: function (multiSelect) {
24006 grid.options.multiSelect = multiSelect;
24010 * @name setModifierKeysToMultiSelect
24011 * @methodOf ui.grid.selection.api:PublicApi
24012 * @description Sets the current gridOption.modifierKeysToMultiSelect to true or false
24013 * @param {bool} modifierKeysToMultiSelect true to only allow multiple rows when using ctrlKey or shiftKey is used
24015 setModifierKeysToMultiSelect: function (modifierKeysToMultiSelect) {
24016 grid.options.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
24020 * @name getSelectAllState
24021 * @methodOf ui.grid.selection.api:PublicApi
24022 * @description Returns whether or not the selectAll checkbox is currently ticked. The
24023 * grid doesn't automatically select rows when you add extra data - so when you add data
24024 * you need to explicitly check whether the selectAll is set, and then call setVisible rows
24027 getSelectAllState: function () {
24028 return grid.selection.selectAll;
24035 grid.api.registerEventsFromObject(publicApi.events);
24037 grid.api.registerMethodsFromObject(publicApi.methods);
24041 defaultGridOptions: function (gridOptions) {
24042 //default option to true unless it was explicitly set to false
24045 * @name ui.grid.selection.api:GridOptions
24047 * @description GridOptions for selection feature, these are available to be
24048 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
24053 * @name enableRowSelection
24054 * @propertyOf ui.grid.selection.api:GridOptions
24055 * @description Enable row selection for entire grid.
24056 * <br/>Defaults to true
24058 gridOptions.enableRowSelection = gridOptions.enableRowSelection !== false;
24061 * @name multiSelect
24062 * @propertyOf ui.grid.selection.api:GridOptions
24063 * @description Enable multiple row selection for entire grid
24064 * <br/>Defaults to true
24066 gridOptions.multiSelect = gridOptions.multiSelect !== false;
24070 * @propertyOf ui.grid.selection.api:GridOptions
24071 * @description Prevent a row from being unselected. Works in conjunction
24072 * with `multiselect = false` and `gridApi.selection.selectRow()` to allow
24073 * you to create a single selection only grid - a row is always selected, you
24074 * can only select different rows, you can't unselect the row.
24075 * <br/>Defaults to false
24077 gridOptions.noUnselect = gridOptions.noUnselect === true;
24080 * @name modifierKeysToMultiSelect
24081 * @propertyOf ui.grid.selection.api:GridOptions
24082 * @description Enable multiple row selection only when using the ctrlKey or shiftKey. Requires multiSelect to be true.
24083 * <br/>Defaults to false
24085 gridOptions.modifierKeysToMultiSelect = gridOptions.modifierKeysToMultiSelect === true;
24088 * @name enableRowHeaderSelection
24089 * @propertyOf ui.grid.selection.api:GridOptions
24090 * @description Enable a row header to be used for selection
24091 * <br/>Defaults to true
24093 gridOptions.enableRowHeaderSelection = gridOptions.enableRowHeaderSelection !== false;
24096 * @name enableFullRowSelection
24097 * @propertyOf ui.grid.selection.api:GridOptions
24098 * @description Enable selection by clicking anywhere on the row. Defaults to
24099 * false if `enableRowHeaderSelection` is true, otherwise defaults to false.
24101 if ( typeof(gridOptions.enableFullRowSelection) === 'undefined' ){
24102 gridOptions.enableFullRowSelection = !gridOptions.enableRowHeaderSelection;
24106 * @name enableSelectAll
24107 * @propertyOf ui.grid.selection.api:GridOptions
24108 * @description Enable the select all checkbox at the top of the selectionRowHeader
24109 * <br/>Defaults to true
24111 gridOptions.enableSelectAll = gridOptions.enableSelectAll !== false;
24114 * @name enableSelectionBatchEvent
24115 * @propertyOf ui.grid.selection.api:GridOptions
24116 * @description If selected rows are changed in bulk, either via the API or
24117 * via the selectAll checkbox, then a separate event is fired. Setting this
24118 * option to false will cause the rowSelectionChanged event to be called multiple times
24120 * <br/>Defaults to true
24122 gridOptions.enableSelectionBatchEvent = gridOptions.enableSelectionBatchEvent !== false;
24125 * @name selectionRowHeaderWidth
24126 * @propertyOf ui.grid.selection.api:GridOptions
24127 * @description can be used to set a custom width for the row header selection column
24128 * <br/>Defaults to 30px
24130 gridOptions.selectionRowHeaderWidth = angular.isDefined(gridOptions.selectionRowHeaderWidth) ? gridOptions.selectionRowHeaderWidth : 30;
24134 * @name enableFooterTotalSelected
24135 * @propertyOf ui.grid.selection.api:GridOptions
24136 * @description Shows the total number of selected items in footer if true.
24137 * <br/>Defaults to true.
24138 * <br/>GridOptions.showGridFooter must also be set to true.
24140 gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false;
24144 * @name isRowSelectable
24145 * @propertyOf ui.grid.selection.api:GridOptions
24146 * @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property.
24149 gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop;
24154 * @name toggleRowSelection
24155 * @methodOf ui.grid.selection.service:uiGridSelectionService
24156 * @description Toggles row as selected or unselected
24157 * @param {Grid} grid grid object
24158 * @param {GridRow} row row to select or deselect
24159 * @param {Event} event object if resulting from event
24160 * @param {bool} multiSelect if false, only one row at time can be selected
24161 * @param {bool} noUnselect if true then rows cannot be unselected
24163 toggleRowSelection: function (grid, row, evt, multiSelect, noUnselect) {
24164 var selected = row.isSelected;
24166 if ( row.enableSelection === false && !selected ){
24171 if (!multiSelect && !selected) {
24172 service.clearSelectedRows(grid, evt);
24173 } else if (!multiSelect && selected) {
24174 selectedRows = service.getSelectedRows(grid);
24175 if (selectedRows.length > 1) {
24176 selected = false; // Enable reselect of the row
24177 service.clearSelectedRows(grid, evt);
24181 if (selected && noUnselect){
24182 // don't deselect the row
24184 row.setSelected(!selected);
24185 if (row.isSelected === true) {
24186 grid.selection.lastSelectedRow = row;
24189 selectedRows = service.getSelectedRows(grid);
24190 grid.selection.selectAll = grid.rows.length === selectedRows.length;
24192 grid.api.selection.raise.rowSelectionChanged(row, evt);
24197 * @name shiftSelect
24198 * @methodOf ui.grid.selection.service:uiGridSelectionService
24199 * @description selects a group of rows from the last selected row using the shift key
24200 * @param {Grid} grid grid object
24201 * @param {GridRow} clicked row
24202 * @param {Event} event object if raised from an event
24203 * @param {bool} multiSelect if false, does nothing this is for multiSelect only
24205 shiftSelect: function (grid, row, evt, multiSelect) {
24206 if (!multiSelect) {
24209 var selectedRows = service.getSelectedRows(grid);
24210 var fromRow = selectedRows.length > 0 ? grid.renderContainers.body.visibleRowCache.indexOf(grid.selection.lastSelectedRow) : 0;
24211 var toRow = grid.renderContainers.body.visibleRowCache.indexOf(row);
24212 //reverse select direction
24213 if (fromRow > toRow) {
24219 var changedRows = [];
24220 for (var i = fromRow; i <= toRow; i++) {
24221 var rowToSelect = grid.renderContainers.body.visibleRowCache[i];
24223 if ( !rowToSelect.isSelected && rowToSelect.enableSelection !== false ){
24224 rowToSelect.setSelected(true);
24225 grid.selection.lastSelectedRow = rowToSelect;
24226 service.decideRaiseSelectionEvent( grid, rowToSelect, changedRows, evt );
24230 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24234 * @name getSelectedRows
24235 * @methodOf ui.grid.selection.service:uiGridSelectionService
24236 * @description Returns all the selected rows
24237 * @param {Grid} grid grid object
24239 getSelectedRows: function (grid) {
24240 return grid.rows.filter(function (row) {
24241 return row.isSelected;
24247 * @name clearSelectedRows
24248 * @methodOf ui.grid.selection.service:uiGridSelectionService
24249 * @description Clears all selected rows
24250 * @param {Grid} grid grid object
24251 * @param {Event} event object if raised from an event
24253 clearSelectedRows: function (grid, evt) {
24254 var changedRows = [];
24255 service.getSelectedRows(grid).forEach(function (row) {
24256 if ( row.isSelected ){
24257 row.setSelected(false);
24258 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24261 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24262 grid.selection.selectAll = false;
24263 grid.selection.selectedCount = 0;
24268 * @name decideRaiseSelectionEvent
24269 * @methodOf ui.grid.selection.service:uiGridSelectionService
24270 * @description Decides whether to raise a single event or a batch event
24271 * @param {Grid} grid grid object
24272 * @param {GridRow} row row that has changed
24273 * @param {array} changedRows an array to which we can append the changed
24274 * @param {Event} event object if raised from an event
24275 * row if we're doing batch events
24277 decideRaiseSelectionEvent: function( grid, row, changedRows, evt ){
24278 if ( !grid.options.enableSelectionBatchEvent ){
24279 grid.api.selection.raise.rowSelectionChanged(row, evt);
24281 changedRows.push(row);
24287 * @name raiseSelectionEvent
24288 * @methodOf ui.grid.selection.service:uiGridSelectionService
24289 * @description Decides whether we need to raise a batch event, and
24290 * raises it if we do.
24291 * @param {Grid} grid grid object
24292 * @param {array} changedRows an array of changed rows, only populated
24293 * @param {Event} event object if raised from an event
24294 * if we're doing batch events
24296 decideRaiseSelectionBatchEvent: function( grid, changedRows, evt ){
24297 if ( changedRows.length > 0 ){
24298 grid.api.selection.raise.rowSelectionChangedBatch(changedRows, evt);
24309 * @name ui.grid.selection.directive:uiGridSelection
24313 * @description Adds selection features to grid
24316 <example module="app">
24317 <file name="app.js">
24318 var app = angular.module('app', ['ui.grid', 'ui.grid.selection']);
24320 app.controller('MainCtrl', ['$scope', function ($scope) {
24322 { name: 'Bob', title: 'CEO' },
24323 { name: 'Frank', title: 'Lowly Developer' }
24326 $scope.columnDefs = [
24327 {name: 'name', enableCellEdit: true},
24328 {name: 'title', enableCellEdit: true}
24332 <file name="index.html">
24333 <div ng-controller="MainCtrl">
24334 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-selection></div>
24339 module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', 'uiGridConstants',
24340 function (uiGridSelectionConstants, uiGridSelectionService, $templateCache, uiGridConstants) {
24344 require: '^uiGrid',
24346 compile: function () {
24348 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
24349 uiGridSelectionService.initializeGrid(uiGridCtrl.grid);
24350 if (uiGridCtrl.grid.options.enableRowHeaderSelection) {
24351 var selectionRowHeaderDef = {
24352 name: uiGridSelectionConstants.selectionRowHeaderColName,
24354 width: uiGridCtrl.grid.options.selectionRowHeaderWidth,
24356 cellTemplate: 'ui-grid/selectionRowHeader',
24357 headerCellTemplate: 'ui-grid/selectionHeaderCell',
24358 enableColumnResizing: false,
24359 enableColumnMenu: false,
24360 exporterSuppressExport: true,
24361 allowCellFocus: true
24364 uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef);
24367 var processorSet = false;
24369 var processSelectableRows = function( rows ){
24370 rows.forEach(function(row){
24371 row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row);
24376 var updateOptions = function(){
24377 if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) {
24378 uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500);
24379 processorSet = true;
24385 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback( updateOptions, [uiGridConstants.dataChange.OPTIONS] );
24387 $scope.$on( '$destroy', dataChangeDereg);
24389 post: function ($scope, $elm, $attrs, uiGridCtrl) {
24397 module.directive('uiGridSelectionRowHeaderButtons', ['$templateCache', 'uiGridSelectionService', 'gridUtil',
24398 function ($templateCache, uiGridSelectionService, gridUtil) {
24402 template: $templateCache.get('ui-grid/selectionRowHeaderButtons'),
24404 require: '^uiGrid',
24405 link: function($scope, $elm, $attrs, uiGridCtrl) {
24406 var self = uiGridCtrl.grid;
24407 $scope.selectButtonClick = selectButtonClick;
24409 // On IE, prevent mousedowns on the select button from starting a selection.
24410 // If this is not done and you shift+click on another row, the browser will select a big chunk of text
24411 if (gridUtil.detectBrowser() === 'ie') {
24412 $elm.on('mousedown', selectButtonMouseDown);
24416 function selectButtonClick(row, evt) {
24417 evt.stopPropagation();
24419 if (evt.shiftKey) {
24420 uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
24422 else if (evt.ctrlKey || evt.metaKey) {
24423 uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect);
24426 uiGridSelectionService.toggleRowSelection(self, row, evt, (self.options.multiSelect && !self.options.modifierKeysToMultiSelect), self.options.noUnselect);
24430 function selectButtonMouseDown(evt) {
24431 if (evt.ctrlKey || evt.shiftKey) {
24432 evt.target.onselectstart = function () { return false; };
24433 window.setTimeout(function () { evt.target.onselectstart = null; }, 0);
24440 module.directive('uiGridSelectionSelectAllButtons', ['$templateCache', 'uiGridSelectionService',
24441 function ($templateCache, uiGridSelectionService) {
24445 template: $templateCache.get('ui-grid/selectionSelectAllButtons'),
24447 link: function($scope, $elm, $attrs, uiGridCtrl) {
24448 var self = $scope.col.grid;
24450 $scope.headerButtonClick = function(row, evt) {
24451 if ( self.selection.selectAll ){
24452 uiGridSelectionService.clearSelectedRows(self, evt);
24453 if ( self.options.noUnselect ){
24454 self.api.selection.selectRowByVisibleIndex(0, evt);
24456 self.selection.selectAll = false;
24458 if ( self.options.multiSelect ){
24459 self.api.selection.selectAllVisibleRows(evt);
24460 self.selection.selectAll = true;
24470 * @name ui.grid.selection.directive:uiGridViewport
24473 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
24476 module.directive('uiGridViewport',
24477 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService',
24478 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService) {
24480 priority: -200, // run after default directive
24482 compile: function ($elm, $attrs) {
24483 var rowRepeatDiv = angular.element($elm.children().children()[0]);
24485 var existingNgClass = rowRepeatDiv.attr("ng-class");
24486 var newNgClass = '';
24487 if ( existingNgClass ) {
24488 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-row-selected': row.isSelected}";
24490 newNgClass = "{'ui-grid-row-selected': row.isSelected}";
24492 rowRepeatDiv.attr("ng-class", newNgClass);
24495 pre: function ($scope, $elm, $attrs, controllers) {
24498 post: function ($scope, $elm, $attrs, controllers) {
24507 * @name ui.grid.selection.directive:uiGridCell
24511 * @description Stacks on top of ui.grid.uiGridCell to provide selection feature
24513 module.directive('uiGridCell',
24514 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', '$timeout',
24515 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService, $timeout) {
24517 priority: -200, // run after default uiGridCell directive
24519 require: '?^uiGrid',
24521 link: function ($scope, $elm, $attrs, uiGridCtrl) {
24523 var touchStartTime = 0;
24524 var touchTimeout = 300;
24526 // Bind to keydown events in the render container
24527 if (uiGridCtrl.grid.api.cellNav) {
24529 uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
24530 if (rowCol === null ||
24531 rowCol.row !== $scope.row ||
24532 rowCol.col !== $scope.col) {
24536 if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
24537 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
24541 // uiGridCellNavService.scrollToIfNecessary(uiGridCtrl.grid, rowCol.row, rowCol.col);
24545 //$elm.bind('keydown', function (evt) {
24546 // if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
24547 // uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
24548 // $scope.$apply();
24552 var selectCells = function(evt){
24553 // if we get a click, then stop listening for touchend
24554 $elm.off('touchend', touchEnd);
24556 if (evt.shiftKey) {
24557 uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect);
24559 else if (evt.ctrlKey || evt.metaKey) {
24560 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect, $scope.grid.options.noUnselect);
24563 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
24567 // don't re-enable the touchend handler for a little while - some devices generate both, and it will
24568 // take a little while to move your hand from the mouse to the screen if you have both modes of input
24569 $timeout(function() {
24570 $elm.on('touchend', touchEnd);
24574 var touchStart = function(evt){
24575 touchStartTime = (new Date()).getTime();
24577 // if we get a touch event, then stop listening for click
24578 $elm.off('click', selectCells);
24581 var touchEnd = function(evt) {
24582 var touchEndTime = (new Date()).getTime();
24583 var touchTime = touchEndTime - touchStartTime;
24585 if (touchTime < touchTimeout ) {
24590 // don't re-enable the click handler for a little while - some devices generate both, and it will
24591 // take a little while to move your hand from the screen to the mouse if you have both modes of input
24592 $timeout(function() {
24593 $elm.on('click', selectCells);
24597 function registerRowSelectionEvents() {
24598 if ($scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection) {
24599 $elm.addClass('ui-grid-disable-selection');
24600 $elm.on('touchstart', touchStart);
24601 $elm.on('touchend', touchEnd);
24602 $elm.on('click', selectCells);
24604 $scope.registered = true;
24608 function deregisterRowSelectionEvents() {
24609 if ($scope.registered){
24610 $elm.removeClass('ui-grid-disable-selection');
24612 $elm.off('touchstart', touchStart);
24613 $elm.off('touchend', touchEnd);
24614 $elm.off('click', selectCells);
24616 $scope.registered = false;
24620 registerRowSelectionEvents();
24621 // register a dataChange callback so that we can change the selection configuration dynamically
24622 // if the user changes the options
24623 var dataChangeDereg = $scope.grid.registerDataChangeCallback( function() {
24624 if ( $scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection &&
24625 !$scope.registered ){
24626 registerRowSelectionEvents();
24627 } else if ( ( !$scope.grid.options.enableRowSelection || !$scope.grid.options.enableFullRowSelection ) &&
24628 $scope.registered ){
24629 deregisterRowSelectionEvents();
24631 }, [uiGridConstants.dataChange.OPTIONS] );
24633 $elm.on( '$destroy', dataChangeDereg);
24638 module.directive('uiGridGridFooter', ['$compile', 'uiGridConstants', 'gridUtil', function ($compile, uiGridConstants, gridUtil) {
24643 require: '^uiGrid',
24645 compile: function ($elm, $attrs) {
24647 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
24649 if (!uiGridCtrl.grid.options.showGridFooter) {
24654 gridUtil.getTemplate('ui-grid/gridFooterSelectedItems')
24655 .then(function (contents) {
24656 var template = angular.element(contents);
24658 var newElm = $compile(template)($scope);
24660 angular.element($elm[0].getElementsByClassName('ui-grid-grid-footer')[0]).append(newElm);
24664 post: function ($scope, $elm, $attrs, controllers) {
24679 * @name ui.grid.treeBase
24682 * # ui.grid.treeBase
24684 * <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>
24686 * This module provides base tree handling functions that are shared by other features, notably grouping
24687 * and treeView. It provides a tree view of the data, with nodes in that
24690 * Design information:
24691 * -------------------
24693 * The raw data that is provided must come with a $$treeLevel on any non-leaf node. Grouping will create
24694 * these on all the group header rows, treeView will expect these to be set in the raw data by the user.
24695 * TreeBase will run a rowsProcessor that:
24696 * - builds `treeBase.tree` out of the provided rows
24697 * - permits a recursive sort of the tree
24698 * - maintains the expand/collapse state of each node
24699 * - provides the expand/collapse all button and the expand/collapse buttons
24700 * - maintains the count of children for each node
24702 * Each row is updated with a link to the tree node that represents it. Refer {@link ui.grid.treeBase.grid:treeBase.tree tree documentation}
24705 * TreeBase adds information to the rows
24706 * - treeLevel: if present and > -1 tells us the level (level 0 is the top level)
24707 * - treeNode: pointer to the node in the grid.treeBase.tree that refers
24708 * to this row, allowing us to manipulate the state
24710 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
24711 * row order or filtering or anything like that is changed. We recall the expanded state
24712 * across invocations of the rowsProcessors by the reference to the treeNode on the individual
24713 * rows. We rebuild the tree itself quite frequently, when we do this we use the saved treeNodes to
24714 * get the state, but we overwrite the other data in that treeNode.
24716 * By default rows are collapsed, which means all data rows have their visible property
24717 * set to false, and only level 0 group rows are set to visible.
24719 * We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into
24720 * grid.treeBase.tree, then call refresh. This is because we can't easily change the visible
24721 * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as
24722 * well use it all the time.
24724 * Tree base provides sorting (on non-grouped columns).
24726 * Sorting works in two passes. The standard sorting is performed for any columns that are important to building
24727 * the tree (for example, any grouped columns). Then after the tree is built, a recursive tree sort is performed
24728 * for the remaining sort columns (including the original sort) - these columns are sorted within each tree level
24729 * (so all the level 1 nodes are sorted, then all the level 2 nodes within each level 1 node etc).
24731 * To achieve this we make use of the `ignoreSort` property on the sort configuration. The parent feature (treeView or grouping)
24732 * must provide a rowsProcessor that runs with very low priority (typically in the 60-65 range), and that sets
24733 * the `ignoreSort`on any sort that it wants to run on the tree. TreeBase will clear the ignoreSort on all sorts - so it
24734 * will turn on any sorts that haven't run. It will then call a recursive sort on the tree.
24736 * Tree base provides treeAggregation. It checks the treeAggregation configuration on each column, and aggregates based on
24737 * the logic provided as it builds the tree. Footer aggregation from the uiGrid core should not be used with treeBase aggregation,
24738 * since it operates on all visible rows, as opposed to to leaf nodes only. Setting `showColumnFooter: true` will show the
24739 * treeAggregations in the column footer. Aggregation information will be collected in the format:
24745 * label: 'count: ',
24746 * rendered: 'count: 4'
24750 * A callback is provided to format the value once it is finalised (aka a valueFilter).
24755 * <div doc-module-components="ui.grid.treeBase"></div>
24758 var module = angular.module('ui.grid.treeBase', ['ui.grid']);
24762 * @name ui.grid.treeBase.constant:uiGridTreeBaseConstants
24764 * @description constants available in treeBase module.
24766 * These constants are manually copied into grouping and treeView,
24767 * as I haven't found a way to simply include them, and it's not worth
24768 * investing time in for something that changes very infrequently.
24771 module.constant('uiGridTreeBaseConstants', {
24772 featureName: "treeBase",
24773 rowHeaderColName: 'treeBaseRowHeaderCol',
24774 EXPANDED: 'expanded',
24775 COLLAPSED: 'collapsed',
24787 * @name ui.grid.treeBase.service:uiGridTreeBaseService
24789 * @description Services for treeBase feature
24793 * @name ui.grid.treeBase.api:ColumnDef
24795 * @description ColumnDef for tree feature, these are available to be
24796 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
24799 module.service('uiGridTreeBaseService', ['$q', 'uiGridTreeBaseConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'rowSorter',
24800 function ($q, uiGridTreeBaseConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants, rowSorter) {
24804 initializeGrid: function (grid, $scope) {
24806 //add feature namespace and any properties to grid for needed
24809 * @name ui.grid.treeBase.grid:treeBase
24811 * @description Grid properties and functions added for treeBase
24813 grid.treeBase = {};
24817 * @propertyOf ui.grid.treeBase.grid:treeBase
24818 * @name numberLevels
24820 * @description Total number of tree levels currently used, calculated by the rowsProcessor by
24821 * retaining the highest tree level it sees
24823 grid.treeBase.numberLevels = 0;
24827 * @propertyOf ui.grid.treeBase.grid:treeBase
24830 * @description Whether or not the expandAll box is selected
24832 grid.treeBase.expandAll = false;
24836 * @propertyOf ui.grid.treeBase.grid:treeBase
24839 * @description Tree represented as a nested array that holds the state of each node, along with a
24840 * pointer to the row. The array order is material - we will display the children in the order
24841 * they are stored in the array
24843 * Each node stores:
24845 * - the state of this node
24846 * - an array of children of this node
24847 * - a pointer to the parent of this node (reverse pointer, allowing us to walk up the tree)
24848 * - the number of children of this node
24849 * - aggregation information calculated from the nodes
24853 * state: 'expanded',
24854 * row: <reference to row>,
24860 * label: 'count: ',
24861 * rendered: 'count: 2'
24865 * state: 'expanded',
24866 * row: <reference to row>,
24867 * parentRow: <reference to row>,
24872 * label: 'count: ',
24873 * rendered: 'count: 4'
24876 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
24877 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
24878 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
24879 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> }
24883 * state: 'collapsed',
24884 * row: <reference to row>,
24885 * parentRow: <reference to row>,
24890 * label: 'count: ',
24891 * rendered: 'count: 3'
24894 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
24895 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
24896 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> }
24900 * }, {<another level 0 node maybe>} ]
24902 * Missing state values are false - meaning they aren't expanded.
24904 * This is used because the rowProcessors run every time the grid is refreshed, so
24905 * we'd lose the expanded state every time the grid was refreshed. This instead gives
24906 * us a reliable lookup that persists across rowProcessors.
24908 * This tree is rebuilt every time we run the rowsProcessors. Since each row holds a pointer
24909 * to it's tree node we can persist expand/collapse state across calls to rowsProcessor, we discard
24910 * all transient information on the tree (children, childCount) and recalculate it
24913 grid.treeBase.tree = {};
24915 service.defaultGridOptions(grid.options);
24917 grid.registerRowsProcessor(service.treeRows, 410);
24919 grid.registerColumnBuilder( service.treeBaseColumnBuilder );
24921 service.createRowHeader( grid );
24925 * @name ui.grid.treeBase.api:PublicApi
24927 * @description Public Api for treeBase feature
24934 * @eventOf ui.grid.treeBase.api:PublicApi
24935 * @name rowExpanded
24936 * @description raised whenever a row is expanded. If you are dynamically
24937 * rendering your tree you can listen to this event, and then retrieve
24938 * the children of this row and load them into the grid data.
24940 * When the data is loaded the grid will automatically refresh to show these new rows
24943 * gridApi.treeBase.on.rowExpanded(scope,function(row){})
24945 * @param {gridRow} row the row that was expanded. You can also
24946 * retrieve the grid from this row with row.grid
24952 * @eventOf ui.grid.treeBase.api:PublicApi
24953 * @name rowCollapsed
24954 * @description raised whenever a row is collapsed. Doesn't really have
24955 * a purpose at the moment, included for symmetry
24958 * gridApi.treeBase.on.rowCollapsed(scope,function(row){})
24960 * @param {gridRow} row the row that was collapsed. You can also
24961 * retrieve the grid from this row with row.grid
24971 * @name expandAllRows
24972 * @methodOf ui.grid.treeBase.api:PublicApi
24973 * @description Expands all tree rows
24975 expandAllRows: function () {
24976 service.expandAllRows(grid);
24981 * @name collapseAllRows
24982 * @methodOf ui.grid.treeBase.api:PublicApi
24983 * @description collapse all tree rows
24985 collapseAllRows: function () {
24986 service.collapseAllRows(grid);
24991 * @name toggleRowTreeState
24992 * @methodOf ui.grid.treeBase.api:PublicApi
24993 * @description call expand if the row is collapsed, collapse if it is expanded
24994 * @param {gridRow} row the row you wish to toggle
24996 toggleRowTreeState: function (row) {
24997 service.toggleRowTreeState(grid, row);
25003 * @methodOf ui.grid.treeBase.api:PublicApi
25004 * @description expand the immediate children of the specified row
25005 * @param {gridRow} row the row you wish to expand
25007 expandRow: function (row) {
25008 service.expandRow(grid, row);
25013 * @name expandRowChildren
25014 * @methodOf ui.grid.treeBase.api:PublicApi
25015 * @description expand all children of the specified row
25016 * @param {gridRow} row the row you wish to expand
25018 expandRowChildren: function (row) {
25019 service.expandRowChildren(grid, row);
25024 * @name collapseRow
25025 * @methodOf ui.grid.treeBase.api:PublicApi
25026 * @description collapse the specified row. When
25027 * you expand the row again, all grandchildren will retain their state
25028 * @param {gridRow} row the row you wish to collapse
25030 collapseRow: function ( row ) {
25031 service.collapseRow(grid, row);
25036 * @name collapseRowChildren
25037 * @methodOf ui.grid.treeBase.api:PublicApi
25038 * @description collapse all children of the specified row. When
25039 * you expand the row again, all grandchildren will be collapsed
25040 * @param {gridRow} row the row you wish to collapse children for
25042 collapseRowChildren: function ( row ) {
25043 service.collapseRowChildren(grid, row);
25048 * @name getTreeState
25049 * @methodOf ui.grid.treeBase.api:PublicApi
25050 * @description Get the tree state for this grid,
25051 * used by the saveState feature
25052 * Returned treeState as an object
25053 * `{ expandedState: { uid: 'expanded', uid: 'collapsed' } }`
25054 * where expandedState is a hash of row uid and the current expanded state
25056 * @returns {object} tree state
25058 * TODO - this needs work - we need an identifier that persists across instantiations,
25059 * not uid. This really means we need a row identity defined, but that won't work for
25060 * grouping. Perhaps this needs to be moved up to treeView and grouping, rather than
25063 getTreeExpandedState: function () {
25064 return { expandedState: service.getTreeState(grid) };
25069 * @name setTreeState
25070 * @methodOf ui.grid.treeBase.api:PublicApi
25071 * @description Set the expanded states of the tree
25072 * @param {object} config the config you want to apply, in the format
25073 * provided by getTreeState
25075 setTreeState: function ( config ) {
25076 service.setTreeState( grid, config );
25081 * @name getRowChildren
25082 * @methodOf ui.grid.treeBase.api:PublicApi
25083 * @description Get the children of the specified row
25084 * @param {GridRow} row the row you want the children of
25085 * @returns {Array} array of children of this row, the children
25088 getRowChildren: function ( row ){
25089 return row.treeNode.children.map( function( childNode ){
25090 return childNode.row;
25097 grid.api.registerEventsFromObject(publicApi.events);
25099 grid.api.registerMethodsFromObject(publicApi.methods);
25103 defaultGridOptions: function (gridOptions) {
25104 //default option to true unless it was explicitly set to false
25107 * @name ui.grid.treeBase.api:GridOptions
25109 * @description GridOptions for treeBase feature, these are available to be
25110 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
25115 * @name treeRowHeaderBaseWidth
25116 * @propertyOf ui.grid.treeBase.api:GridOptions
25117 * @description Base width of the tree header, provides for a single level of tree. This
25118 * is incremented by `treeIndent` for each extra level
25119 * <br/>Defaults to 30
25121 gridOptions.treeRowHeaderBaseWidth = gridOptions.treeRowHeaderBaseWidth || 30;
25126 * @propertyOf ui.grid.treeBase.api:GridOptions
25127 * @description Number of pixels of indent for the icon at each tree level, wider indents are visually more pleasing,
25128 * but will make the tree row header wider
25129 * <br/>Defaults to 10
25131 gridOptions.treeIndent = gridOptions.treeIndent || 10;
25135 * @name showTreeRowHeader
25136 * @propertyOf ui.grid.treeBase.api:GridOptions
25137 * @description If set to false, don't create the row header. Youll need to programatically control the expand
25139 * <br/>Defaults to true
25141 gridOptions.showTreeRowHeader = gridOptions.showTreeRowHeader !== false;
25145 * @name showTreeExpandNoChildren
25146 * @propertyOf ui.grid.treeBase.api:GridOptions
25147 * @description If set to true, show the expand/collapse button even if there are no
25148 * children of a node. You'd use this if you're planning to dynamically load the children
25150 * <br/>Defaults to true, grouping overrides to false
25152 gridOptions.showTreeExpandNoChildren = gridOptions.showTreeExpandNoChildren !== false;
25156 * @name treeRowHeaderAlwaysVisible
25157 * @propertyOf ui.grid.treeBase.api:GridOptions
25158 * @description If set to true, row header even if there are no tree nodes
25160 * <br/>Defaults to true
25162 gridOptions.treeRowHeaderAlwaysVisible = gridOptions.treeRowHeaderAlwaysVisible !== false;
25166 * @name treeCustomAggregations
25167 * @propertyOf ui.grid.treeBase.api:GridOptions
25168 * @description Define custom aggregation functions. The properties of this object will be
25169 * aggregation types available for use on columnDef with {@link ui.grid.treeBase.api:ColumnDef treeAggregationType} or through the column menu.
25170 * If a function defined here uses the same name as one of the native aggregations, this one will take precedence.
25171 * The object format is:
25175 * aggregationName: {
25176 * label: (optional) string,
25177 * aggregationFn: function( aggregation, fieldValue, numValue, row ){...},
25178 * finalizerFn: (optional) function( aggregation ){...}
25182 * aggregationFn: function( aggregation, fieldValue, numValue ){
25183 * aggregation.count = (aggregation.count || 1) + 1;
25184 * aggregation.sum = (aggregation.sum || 0) + numValue;
25186 * finalizerFn: function( aggregation ){
25187 * aggregation.value = aggregation.sum / aggregation.count
25193 * <br/>The `finalizerFn` may be used to manipulate the value before rendering, or to
25194 * apply a custom rendered value. If `aggregation.rendered` is left undefined, the value will be
25195 * rendered. Note that the native aggregation functions use an `finalizerFn` to concatenate
25196 * the label and the value.
25198 * <br/>Defaults to {}
25200 gridOptions.treeCustomAggregations = gridOptions.treeCustomAggregations || {};
25206 * @name treeBaseColumnBuilder
25207 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25208 * @description Sets the tree defaults based on the columnDefs
25210 * @param {object} colDef columnDef we're basing on
25211 * @param {GridCol} col the column we're to update
25212 * @param {object} gridOptions the options we should use
25213 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
25215 treeBaseColumnBuilder: function (colDef, col, gridOptions) {
25220 * @name customTreeAggregationFn
25221 * @propertyOf ui.grid.treeBase.api:ColumnDef
25222 * @description A custom function that aggregates rows into some form of
25223 * total. Aggregations run row-by-row, the function needs to be capable of
25224 * creating a running total.
25226 * The function will be provided the aggregation item (in which you can store running
25227 * totals), the row value that is to be aggregated, and that same row value converted to
25228 * a number (most aggregations work on numbers)
25231 * customTreeAggregationFn = function ( aggregation, fieldValue, numValue, row ){
25232 * // calculates the average of the squares of the values
25233 * if ( typeof(aggregation.count) === 'undefined' ){
25234 * aggregation.count = 0;
25236 * aggregation.count++;
25238 * if ( !isNaN(numValue) ){
25239 * if ( typeof(aggregation.total) === 'undefined' ){
25240 * aggregation.total = 0;
25242 * aggregation.total = aggregation.total + numValue * numValue;
25245 * aggregation.value = aggregation.total / aggregation.count;
25248 * <br/>Defaults to undefined. May be overwritten by treeAggregationType, the two options should not be used together.
25250 if ( typeof(colDef.customTreeAggregationFn) !== 'undefined' ){
25251 col.treeAggregationFn = colDef.customTreeAggregationFn;
25256 * @name treeAggregationType
25257 * @propertyOf ui.grid.treeBase.api:ColumnDef
25258 * @description Use one of the native or grid-level aggregation methods for calculating aggregations on this column.
25259 * Native method are in the constants file and include: SUM, COUNT, MIN, MAX, AVG. This may also be the property the
25260 * name of an aggregation function defined with {@link ui.grid.treeBase.api:GridOptions treeCustomAggregations}.
25263 * treeAggregationType = uiGridTreeBaseConstants.aggregation.SUM,
25267 * If you are using aggregations you should either:
25269 * - also use grouping, in which case the aggregations are displayed in the group header, OR
25270 * - use treeView, in which case you can set `treeAggregationUpdateEntity: true` in the colDef, and
25271 * treeBase will store the aggregation information in the entity, or you can set `treeAggregationUpdateEntity: false`
25272 * in the colDef, and you need to manual retrieve the calculated aggregations from the row.treeNode.aggregations
25274 * <br/>Takes precendence over a treeAggregationFn, the two options should not be used together.
25275 * <br/>Defaults to undefined.
25277 if ( typeof(colDef.treeAggregationType) !== 'undefined' ){
25278 col.treeAggregation = { type: colDef.treeAggregationType };
25279 if ( typeof(gridOptions.treeCustomAggregations[colDef.treeAggregationType]) !== 'undefined' ){
25280 col.treeAggregationFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].aggregationFn;
25281 col.treeAggregationFinalizerFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].finalizerFn;
25282 col.treeAggregation.label = gridOptions.treeCustomAggregations[colDef.treeAggregationType].label;
25283 } else if ( typeof(service.nativeAggregations()[colDef.treeAggregationType]) !== 'undefined' ){
25284 col.treeAggregationFn = service.nativeAggregations()[colDef.treeAggregationType].aggregationFn;
25285 col.treeAggregation.label = service.nativeAggregations()[colDef.treeAggregationType].label;
25291 * @name treeAggregationLabel
25292 * @propertyOf ui.grid.treeBase.api:ColumnDef
25293 * @description A custom label to use for this aggregation. If provided we don't use native i18n.
25295 if ( typeof(colDef.treeAggregationLabel) !== 'undefined' ){
25296 if (typeof(col.treeAggregation) === 'undefined' ){
25297 col.treeAggregation = {};
25299 col.treeAggregation.label = colDef.treeAggregationLabel;
25304 * @name treeAggregationUpdateEntity
25305 * @propertyOf ui.grid.treeBase.api:ColumnDef
25306 * @description Store calculated aggregations into the entity, allowing them
25307 * to be displayed in the grid using a standard cellTemplate. This defaults to true,
25308 * if you are using grouping then you shouldn't set it to false, as then the aggregations won't
25311 * If you are using treeView in most cases you'll want to set this to true. This will result in
25312 * getCellValue returning the aggregation rather than whatever was stored in the cell attribute on
25313 * the entity. If you want to render the underlying entity value (and do something else with the aggregation)
25314 * then you could use a custom cellTemplate to display `row.entity.myAttribute`, rather than using getCellValue.
25316 * <br/>Defaults to true
25320 * gridOptions.columns = [{
25322 * treeAggregation: { type: uiGridTreeBaseConstants.aggregation.SUM },
25323 * treeAggregationUpdateEntity: true
25324 * cellTemplate: '<div>{{row.entity.myCol + " " + row.treeNode.aggregations[0].rendered}}</div>'
25328 col.treeAggregationUpdateEntity = colDef.treeAggregationUpdateEntity !== false;
25332 * @name customTreeAggregationFinalizerFn
25333 * @propertyOf ui.grid.treeBase.api:ColumnDef
25334 * @description A custom function that populates aggregation.rendered, this is called when
25335 * a particular aggregation has been fully calculated, and we want to render the value.
25337 * With the native aggregation options we just concatenate `aggregation.label` and
25338 * `aggregation.value`, but if you wanted to apply a filter or otherwise manipulate the label
25339 * or the value, you can do so with this function. This function will be called after the
25340 * the default `finalizerFn`.
25344 * customTreeAggregationFinalizerFn = function ( aggregation ){
25345 * aggregation.rendered = aggregation.label + aggregation.value / 100 + '%';
25348 * <br/>Defaults to undefined.
25350 if ( typeof(col.customTreeAggregationFinalizerFn) === 'undefined' ){
25351 col.customTreeAggregationFinalizerFn = colDef.customTreeAggregationFinalizerFn;
25359 * @name createRowHeader
25360 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25361 * @description Create the rowHeader. If treeRowHeaderAlwaysVisible then
25362 * set it to visible, otherwise set it to invisible
25364 * @param {Grid} grid grid object
25366 createRowHeader: function( grid ){
25367 var rowHeaderColumnDef = {
25368 name: uiGridTreeBaseConstants.rowHeaderColName,
25370 width: grid.options.treeRowHeaderBaseWidth,
25372 cellTemplate: 'ui-grid/treeBaseRowHeader',
25373 headerCellTemplate: 'ui-grid/treeBaseHeaderCell',
25374 enableColumnResizing: false,
25375 enableColumnMenu: false,
25376 exporterSuppressExport: true,
25377 allowCellFocus: true
25380 rowHeaderColumnDef.visible = grid.options.treeRowHeaderAlwaysVisible;
25381 grid.addRowHeaderColumn( rowHeaderColumnDef );
25387 * @name expandAllRows
25388 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25389 * @description Expands all nodes in the tree
25391 * @param {Grid} grid grid object
25393 expandAllRows: function (grid) {
25394 grid.treeBase.tree.forEach( function( node ) {
25395 service.setAllNodes( grid, node, uiGridTreeBaseConstants.EXPANDED);
25397 grid.treeBase.expandAll = true;
25398 grid.queueGridRefresh();
25404 * @name collapseAllRows
25405 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25406 * @description Collapses all nodes in the tree
25408 * @param {Grid} grid grid object
25410 collapseAllRows: function (grid) {
25411 grid.treeBase.tree.forEach( function( node ) {
25412 service.setAllNodes( grid, node, uiGridTreeBaseConstants.COLLAPSED);
25414 grid.treeBase.expandAll = false;
25415 grid.queueGridRefresh();
25421 * @name setAllNodes
25422 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25423 * @description Works through a subset of grid.treeBase.rowExpandedStates, setting
25424 * all child nodes (and their descendents) of the provided node to the given state.
25426 * Calls itself recursively on all nodes so as to achieve this.
25428 * @param {Grid} grid the grid we're operating on (so we can raise events)
25429 * @param {object} treeNode a node in the tree that we want to update
25430 * @param {string} targetState the state we want to set it to
25432 setAllNodes: function (grid, treeNode, targetState) {
25433 if ( typeof(treeNode.state) !== 'undefined' && treeNode.state !== targetState ){
25434 treeNode.state = targetState;
25436 if ( targetState === uiGridTreeBaseConstants.EXPANDED ){
25437 grid.api.treeBase.raise.rowExpanded(treeNode.row);
25439 grid.api.treeBase.raise.rowCollapsed(treeNode.row);
25443 // set all child nodes
25444 if ( treeNode.children ){
25445 treeNode.children.forEach(function( childNode ){
25446 service.setAllNodes(grid, childNode, targetState);
25454 * @name toggleRowTreeState
25455 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25456 * @description Toggles the expand or collapse state of this grouped row, if
25457 * it's a parent row
25459 * @param {Grid} grid grid object
25460 * @param {GridRow} row the row we want to toggle
25462 toggleRowTreeState: function ( grid, row ){
25463 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25467 if (row.treeNode.state === uiGridTreeBaseConstants.EXPANDED){
25468 service.collapseRow(grid, row);
25470 service.expandRow(grid, row);
25473 grid.queueGridRefresh();
25480 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25481 * @description Expands this specific row, showing only immediate children.
25483 * @param {Grid} grid grid object
25484 * @param {GridRow} row the row we want to expand
25486 expandRow: function ( grid, row ){
25487 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25491 if ( row.treeNode.state !== uiGridTreeBaseConstants.EXPANDED ){
25492 row.treeNode.state = uiGridTreeBaseConstants.EXPANDED;
25493 grid.api.treeBase.raise.rowExpanded(row);
25494 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
25495 grid.queueGridRefresh();
25502 * @name expandRowChildren
25503 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25504 * @description Expands this specific row, showing all children.
25506 * @param {Grid} grid grid object
25507 * @param {GridRow} row the row we want to expand
25509 expandRowChildren: function ( grid, row ){
25510 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25514 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.EXPANDED);
25515 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
25516 grid.queueGridRefresh();
25522 * @name collapseRow
25523 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25524 * @description Collapses this specific row
25526 * @param {Grid} grid grid object
25527 * @param {GridRow} row the row we want to collapse
25529 collapseRow: function( grid, row ){
25530 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25534 if ( row.treeNode.state !== uiGridTreeBaseConstants.COLLAPSED ){
25535 row.treeNode.state = uiGridTreeBaseConstants.COLLAPSED;
25536 grid.treeBase.expandAll = false;
25537 grid.api.treeBase.raise.rowCollapsed(row);
25538 grid.queueGridRefresh();
25545 * @name collapseRowChildren
25546 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25547 * @description Collapses this specific row and all children
25549 * @param {Grid} grid grid object
25550 * @param {GridRow} row the row we want to collapse
25552 collapseRowChildren: function( grid, row ){
25553 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25557 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.COLLAPSED);
25558 grid.treeBase.expandAll = false;
25559 grid.queueGridRefresh();
25565 * @name allExpanded
25566 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25567 * @description Returns true if all rows are expanded, false
25568 * if they're not. Walks the tree to determine this. Used
25569 * to set the expandAll state.
25571 * If the node has no children, then return true (it's immaterial
25572 * whether it is expanded). If the node has children, then return
25573 * false if this node is collapsed, or if any child node is not all expanded
25575 * @param {object} tree the grid to check
25576 * @returns {boolean} whether or not the tree is all expanded
25578 allExpanded: function( tree ){
25579 var allExpanded = true;
25580 tree.forEach( function( node ){
25581 if ( !service.allExpandedInternal( node ) ){
25582 allExpanded = false;
25585 return allExpanded;
25588 allExpandedInternal: function( treeNode ){
25589 if ( treeNode.children && treeNode.children.length > 0 ){
25590 if ( treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
25593 var allExpanded = true;
25594 treeNode.children.forEach( function( node ){
25595 if ( !service.allExpandedInternal( node ) ){
25596 allExpanded = false;
25599 return allExpanded;
25609 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25610 * @description The rowProcessor that adds the nodes to the tree, and sets the visible
25611 * state of each row based on it's parent state
25613 * Assumes it is always called after the sorting processor, and the grouping processor if there is one.
25614 * Performs any tree sorts itself after having built the tree
25616 * Processes all the rows in order, setting the group level based on the $$treeLevel in the associated
25617 * entity, and setting the visible state based on the parent's state.
25619 * Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly
25622 * Aggregates if necessary along the way.
25624 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
25625 * @returns {array} the updated rows
25627 treeRows: function( renderableRows ) {
25628 if (renderableRows.length === 0){
25629 return renderableRows;
25633 var currentLevel = 0;
25634 var currentState = uiGridTreeBaseConstants.EXPANDED;
25637 grid.treeBase.tree = service.createTree( grid, renderableRows );
25638 service.updateRowHeaderWidth( grid );
25640 service.sortTree( grid );
25641 service.fixFilter( grid );
25643 return service.renderTree( grid.treeBase.tree );
25649 * @name createOrUpdateRowHeaderWidth
25650 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25651 * @description Calculates the rowHeader width.
25653 * If rowHeader is always present, updates the width.
25655 * If rowHeader is only sometimes present (`treeRowHeaderAlwaysVisible: false`), determines whether there
25656 * should be one, then creates or removes it as appropriate, with the created rowHeader having the
25659 * If there's never a rowHeader then never creates one: `showTreeRowHeader: false`
25661 * @param {Grid} grid the grid we want to set the row header on
25663 updateRowHeaderWidth: function( grid ){
25664 var rowHeader = grid.getColumn(uiGridTreeBaseConstants.rowHeaderColName);
25666 var newWidth = grid.options.treeRowHeaderBaseWidth + grid.options.treeIndent * Math.max(grid.treeBase.numberLevels - 1, 0);
25667 if ( rowHeader && newWidth !== rowHeader.width ){
25668 rowHeader.width = newWidth;
25669 grid.queueRefresh();
25672 var newVisibility = true;
25673 if ( grid.options.showTreeRowHeader === false ){
25674 newVisibility = false;
25676 if ( grid.options.treeRowHeaderAlwaysVisible === false && grid.treeBase.numberLevels <= 0 ){
25677 newVisibility = false;
25679 if ( rowHeader.visible !== newVisibility ) {
25680 rowHeader.visible = newVisibility;
25681 rowHeader.colDef.visible = newVisibility;
25682 grid.queueGridRefresh();
25690 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25691 * @description Creates an array of rows based on the tree, exporting only
25692 * the visible nodes and leaves
25694 * @param {array} nodeList the list of nodes - can be grid.treeBase.tree, or can be node.children when
25695 * we're calling recursively
25696 * @returns {array} renderable rows
25698 renderTree: function( nodeList ){
25699 var renderableRows = [];
25701 nodeList.forEach( function ( node ){
25702 if ( node.row.visible ){
25703 renderableRows.push( node.row );
25705 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
25706 renderableRows = renderableRows.concat( service.renderTree( node.children ) );
25709 return renderableRows;
25716 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25717 * @description Creates a tree from the renderableRows
25719 * @param {Grid} grid the grid
25720 * @param {array} renderableRows the rows we want to create a tree from
25721 * @returns {object} the tree we've build
25723 createTree: function( grid, renderableRows ) {
25724 var currentLevel = -1;
25727 grid.treeBase.tree = [];
25728 grid.treeBase.numberLevels = 0;
25729 var aggregations = service.getAggregations( grid );
25731 var createNode = function( row ){
25732 if ( typeof(row.entity.$$treeLevel) !== 'undefined' && row.treeLevel !== row.entity.$$treeLevel ){
25733 row.treeLevel = row.entity.$$treeLevel;
25736 if ( row.treeLevel <= currentLevel ){
25737 // pop any levels that aren't parents of this level, formatting the aggregation at the same time
25738 while ( row.treeLevel <= currentLevel ){
25739 var lastParent = parents.pop();
25740 service.finaliseAggregations( lastParent );
25744 // reset our current state based on the new parent, set to expanded if this is a level 0 node
25745 if ( parents.length > 0 ){
25746 currentState = service.setCurrentState(parents);
25748 currentState = uiGridTreeBaseConstants.EXPANDED;
25752 // aggregate if this is a leaf node
25753 if ( ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) && row.visible ){
25754 service.aggregate( grid, row, parents );
25757 // add this node to the tree
25758 service.addOrUseNode(grid, row, parents, aggregations);
25760 if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel !== null && row.treeLevel >= 0 ){
25763 currentState = service.setCurrentState(parents);
25766 // update the tree number of levels, so we can set header width if we need to
25767 if ( grid.treeBase.numberLevels < row.treeLevel + 1){
25768 grid.treeBase.numberLevels = row.treeLevel + 1;
25772 renderableRows.forEach( createNode );
25774 // finalise remaining aggregations
25775 while ( parents.length > 0 ){
25776 var lastParent = parents.pop();
25777 service.finaliseAggregations( lastParent );
25780 return grid.treeBase.tree;
25786 * @name addOrUseNode
25787 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25788 * @description Creates a tree node for this row. If this row already has a treeNode
25789 * recorded against it, preserves the state, but otherwise overwrites the data.
25791 * @param {grid} grid the grid we're operating on
25792 * @param {gridRow} row the row we want to set
25793 * @param {array} parents an array of the parents this row should have
25794 * @param {array} aggregationBase empty aggregation information
25795 * @returns {undefined} updates the parents array, updates the row to have a treeNode, and updates the
25796 * grid.treeBase.tree
25798 addOrUseNode: function( grid, row, parents, aggregationBase ){
25799 var newAggregations = [];
25800 aggregationBase.forEach( function(aggregation){
25801 newAggregations.push(service.buildAggregationObject(aggregation.col));
25804 var newNode = { state: uiGridTreeBaseConstants.COLLAPSED, row: row, parentRow: null, aggregations: newAggregations, children: [] };
25805 if ( row.treeNode ){
25806 newNode.state = row.treeNode.state;
25808 if ( parents.length > 0 ){
25809 newNode.parentRow = parents[parents.length - 1];
25811 row.treeNode = newNode;
25813 if ( parents.length === 0 ){
25814 grid.treeBase.tree.push( newNode );
25816 parents[parents.length - 1].treeNode.children.push( newNode );
25823 * @name setCurrentState
25824 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25825 * @description Looks at the parents array to determine our current state.
25826 * If any node in the hierarchy is collapsed, then return collapsed, otherwise return
25829 * @param {array} parents an array of the parents this row should have
25830 * @returns {string} the state we should be setting to any nodes we see
25832 setCurrentState: function( parents ){
25833 var currentState = uiGridTreeBaseConstants.EXPANDED;
25834 parents.forEach( function(parent){
25835 if ( parent.treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
25836 currentState = uiGridTreeBaseConstants.COLLAPSED;
25839 return currentState;
25846 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25847 * @description Performs a recursive sort on the tree nodes, sorting the
25848 * children of each node and putting them back into the children array.
25850 * Before doing this it turns back on all the sortIgnore - things that were previously
25851 * ignored we process now. Since we're sorting within the nodes, presumably anything
25852 * that was already sorted is how we derived the nodes, we can keep those sorts too.
25854 * We only sort tree nodes that are expanded - no point in wasting effort sorting collapsed
25857 * @param {Grid} grid the grid to get the aggregation information from
25858 * @returns {array} the aggregation information
25860 sortTree: function( grid ){
25861 grid.columns.forEach( function( column ) {
25862 if ( column.sort && column.sort.ignoreSort ){
25863 delete column.sort.ignoreSort;
25867 grid.treeBase.tree = service.sortInternal( grid, grid.treeBase.tree );
25870 sortInternal: function( grid, treeList ){
25871 var rows = treeList.map( function( node ){
25875 rows = rowSorter.sort( grid, rows, grid.columns );
25877 var treeNodes = rows.map( function( row ){
25878 return row.treeNode;
25881 treeNodes.forEach( function( node ){
25882 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
25883 node.children = service.sortInternal( grid, node.children );
25893 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25894 * @description After filtering has run, we need to go back through the tree
25895 * and make sure the parent rows are always visible if any of the child rows
25896 * are visible (filtering may make a child visible, but the parent may not
25897 * match the filter criteria)
25899 * This has a risk of being computationally expensive, we do it by walking
25900 * the tree and remembering whether there are any invisible nodes on the
25903 * @param {Grid} grid the grid to fix filters on
25905 fixFilter: function( grid ){
25906 var parentsVisible;
25908 grid.treeBase.tree.forEach( function( node ){
25909 if ( node.children && node.children.length > 0 ){
25910 parentsVisible = node.row.visible;
25911 service.fixFilterInternal( node.children, parentsVisible );
25916 fixFilterInternal: function( nodes, parentsVisible) {
25917 nodes.forEach( function( node ){
25918 if ( node.row.visible && !parentsVisible ){
25919 service.setParentsVisible( node );
25920 parentsVisible = true;
25923 if ( node.children && node.children.length > 0 ){
25924 if ( service.fixFilterInternal( node.children, ( parentsVisible && node.row.visible ) ) ) {
25925 parentsVisible = true;
25930 return parentsVisible;
25933 setParentsVisible: function( node ){
25934 while ( node.parentRow ){
25935 node.parentRow.visible = true;
25936 node = node.parentRow.treeNode;
25942 * @name buildAggregationObject
25943 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25944 * @description Build the object which is stored on the column for holding meta-data about the aggregation.
25945 * This method should only be called with columns which have an aggregation.
25947 * @param {Column} the column which this object relates to
25948 * @returns {object} {col: Column object, label: string, type: string (optional)}
25950 buildAggregationObject: function( column ){
25951 var newAggregation = { col: column };
25953 if ( column.treeAggregation && column.treeAggregation.type ){
25954 newAggregation.type = column.treeAggregation.type;
25957 if ( column.treeAggregation && column.treeAggregation.label ){
25958 newAggregation.label = column.treeAggregation.label;
25961 return newAggregation;
25966 * @name getAggregations
25967 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25968 * @description Looks through the grid columns to find those with aggregations,
25969 * and collates the aggregation information into an array, returns that array
25971 * @param {Grid} grid the grid to get the aggregation information from
25972 * @returns {array} the aggregation information
25974 getAggregations: function( grid ){
25975 var aggregateArray = [];
25977 grid.columns.forEach( function(column){
25978 if ( typeof(column.treeAggregationFn) !== 'undefined' ){
25979 aggregateArray.push( service.buildAggregationObject(column) );
25981 if ( grid.options.showColumnFooter && typeof(column.colDef.aggregationType) === 'undefined' && column.treeAggregation ){
25982 // Add aggregation object for footer
25983 column.treeFooterAggregation = service.buildAggregationObject(column);
25984 column.aggregationType = service.treeFooterAggregationType;
25988 return aggregateArray;
25995 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25996 * @description Accumulate the data from this row onto the aggregations for each parent
25998 * Iterate over the parents, then iterate over the aggregations for each of those parents,
25999 * and perform the aggregation for each individual aggregation
26001 * @param {Grid} grid grid object
26002 * @param {GridRow} row the row we want to set grouping visibility on
26003 * @param {array} parents the parents that we would want to aggregate onto
26005 aggregate: function( grid, row, parents ){
26006 if ( parents.length === 0 && row.treeNode && row.treeNode.aggregations ){
26007 row.treeNode.aggregations.forEach(function(aggregation){
26008 // Calculate aggregations for footer even if there are no grouped rows
26009 if ( typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ) {
26010 var fieldValue = grid.getCellValue(row, aggregation.col);
26011 var numValue = Number(fieldValue);
26012 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26017 parents.forEach( function( parent, index ){
26018 if ( parent.treeNode.aggregations ){
26019 parent.treeNode.aggregations.forEach( function( aggregation ){
26020 var fieldValue = grid.getCellValue(row, aggregation.col);
26021 var numValue = Number(fieldValue);
26022 aggregation.col.treeAggregationFn(aggregation, fieldValue, numValue, row);
26024 if ( index === 0 && typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ){
26025 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26033 // Aggregation routines - no doco needed as self evident
26034 nativeAggregations: function() {
26035 var nativeAggregations = {
26037 label: i18nService.get().aggregation.count,
26038 menuTitle: i18nService.get().grouping.aggregate_count,
26039 aggregationFn: function (aggregation, fieldValue, numValue) {
26040 if (typeof(aggregation.value) === 'undefined') {
26041 aggregation.value = 1;
26043 aggregation.value++;
26049 label: i18nService.get().aggregation.sum,
26050 menuTitle: i18nService.get().grouping.aggregate_sum,
26051 aggregationFn: function( aggregation, fieldValue, numValue ) {
26052 if (!isNaN(numValue)) {
26053 if (typeof(aggregation.value) === 'undefined') {
26054 aggregation.value = numValue;
26056 aggregation.value += numValue;
26063 label: i18nService.get().aggregation.min,
26064 menuTitle: i18nService.get().grouping.aggregate_min,
26065 aggregationFn: function( aggregation, fieldValue, numValue ) {
26066 if (typeof(aggregation.value) === 'undefined') {
26067 aggregation.value = fieldValue;
26069 if (typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)) {
26070 aggregation.value = fieldValue;
26077 label: i18nService.get().aggregation.max,
26078 menuTitle: i18nService.get().grouping.aggregate_max,
26079 aggregationFn: function( aggregation, fieldValue, numValue ){
26080 if ( typeof(aggregation.value) === 'undefined' ){
26081 aggregation.value = fieldValue;
26083 if ( typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue > aggregation.value || aggregation.value === null)){
26084 aggregation.value = fieldValue;
26091 label: i18nService.get().aggregation.avg,
26092 menuTitle: i18nService.get().grouping.aggregate_avg,
26093 aggregationFn: function( aggregation, fieldValue, numValue ){
26094 if ( typeof(aggregation.count) === 'undefined' ){
26095 aggregation.count = 1;
26097 aggregation.count++;
26100 if ( isNaN(numValue) ){
26104 if ( typeof(aggregation.value) === 'undefined' || typeof(aggregation.sum) === 'undefined' ){
26105 aggregation.value = numValue;
26106 aggregation.sum = numValue;
26108 aggregation.sum += numValue;
26109 aggregation.value = aggregation.sum / aggregation.count;
26114 return nativeAggregations;
26119 * @name finaliseAggregation
26120 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26121 * @description Helper function used to finalize aggregation nodes and footer cells
26123 * @param {gridRow} row the parent we're finalising
26124 * @param {aggregation} the aggregation object manipulated by the aggregationFn
26126 finaliseAggregation: function(row, aggregation){
26127 if ( aggregation.col.treeAggregationUpdateEntity && typeof(row) !== 'undefined' && typeof(row.entity[ '$$' + aggregation.col.uid ]) !== 'undefined' ){
26128 angular.extend( aggregation, row.entity[ '$$' + aggregation.col.uid ]);
26131 if ( typeof(aggregation.col.treeAggregationFinalizerFn) === 'function' ){
26132 aggregation.col.treeAggregationFinalizerFn( aggregation );
26134 if ( typeof(aggregation.col.customTreeAggregationFinalizerFn) === 'function' ){
26135 aggregation.col.customTreeAggregationFinalizerFn( aggregation );
26137 if ( typeof(aggregation.rendered) === 'undefined' ){
26138 aggregation.rendered = aggregation.label ? aggregation.label + aggregation.value : aggregation.value;
26144 * @name finaliseAggregations
26145 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26146 * @description Format the data from the aggregation into the rendered text
26147 * e.g. if we had label: 'sum: ' and value: 25, we'd create 'sum: 25'.
26149 * As part of this we call any formatting callback routines we've been provided.
26151 * We write our aggregation out to the row.entity if treeAggregationUpdateEntity is
26152 * set on the column - we don't overwrite any information that's already there, we append
26153 * to it so that grouping can have set the groupVal beforehand without us overwriting it.
26155 * We need to copy the data from the row.entity first before we finalise the aggregation,
26156 * we need that information for the finaliserFn
26158 * @param {gridRow} row the parent we're finalising
26160 finaliseAggregations: function( row ){
26161 if ( typeof(row.treeNode.aggregations) === 'undefined' ){
26165 row.treeNode.aggregations.forEach( function( aggregation ) {
26166 service.finaliseAggregation(row, aggregation);
26168 if ( aggregation.col.treeAggregationUpdateEntity ){
26169 var aggregationCopy = {};
26170 angular.forEach( aggregation, function( value, key ){
26171 if ( aggregation.hasOwnProperty(key) && key !== 'col' ){
26172 aggregationCopy[key] = value;
26176 row.entity[ '$$' + aggregation.col.uid ] = aggregationCopy;
26183 * @name treeFooterAggregationType
26184 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26185 * @description Uses the tree aggregation functions and finalizers to set the
26186 * column footer aggregations.
26188 * @param {rows} visible rows. not used, but accepted to match signature of GridColumn.aggregationType
26189 * @param {gridColumn} the column we are finalizing
26191 treeFooterAggregationType: function( rows, column ) {
26192 service.finaliseAggregation(undefined, column.treeFooterAggregation);
26193 if ( typeof(column.treeFooterAggregation.value) === 'undefined' || column.treeFooterAggregation.rendered === null ){
26194 // The was apparently no aggregation performed (perhaps this is a grouped column
26197 return column.treeFooterAggregation.rendered;
26208 * @name ui.grid.treeBase.directive:uiGridTreeRowHeaderButtons
26211 * @description Provides the expand/collapse button on rows
26213 module.directive('uiGridTreeBaseRowHeaderButtons', ['$templateCache', 'uiGridTreeBaseService',
26214 function ($templateCache, uiGridTreeBaseService) {
26218 template: $templateCache.get('ui-grid/treeBaseRowHeaderButtons'),
26220 require: '^uiGrid',
26221 link: function($scope, $elm, $attrs, uiGridCtrl) {
26222 var self = uiGridCtrl.grid;
26223 $scope.treeButtonClick = function(row, evt) {
26224 uiGridTreeBaseService.toggleRowTreeState(self, row, evt);
26233 * @name ui.grid.treeBase.directive:uiGridTreeBaseExpandAllButtons
26236 * @description Provides the expand/collapse all button
26238 module.directive('uiGridTreeBaseExpandAllButtons', ['$templateCache', 'uiGridTreeBaseService',
26239 function ($templateCache, uiGridTreeBaseService) {
26243 template: $templateCache.get('ui-grid/treeBaseExpandAllButtons'),
26245 link: function($scope, $elm, $attrs, uiGridCtrl) {
26246 var self = $scope.col.grid;
26248 $scope.headerButtonClick = function(row, evt) {
26249 if ( self.treeBase.expandAll ){
26250 uiGridTreeBaseService.collapseAllRows(self, evt);
26252 uiGridTreeBaseService.expandAllRows(self, evt);
26262 * @name ui.grid.treeBase.directive:uiGridViewport
26265 * @description Stacks on top of ui.grid.uiGridViewport to set formatting on a tree header row
26267 module.directive('uiGridViewport',
26268 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
26269 function ($compile, uiGridConstants, gridUtil, $parse) {
26271 priority: -200, // run after default directive
26273 compile: function ($elm, $attrs) {
26274 var rowRepeatDiv = angular.element($elm.children().children()[0]);
26276 var existingNgClass = rowRepeatDiv.attr("ng-class");
26277 var newNgClass = '';
26278 if ( existingNgClass ) {
26279 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-tree-header-row': row.treeLevel > -1}";
26281 newNgClass = "{'ui-grid-tree-header-row': row.treeLevel > -1}";
26283 rowRepeatDiv.attr("ng-class", newNgClass);
26286 pre: function ($scope, $elm, $attrs, controllers) {
26289 post: function ($scope, $elm, $attrs, controllers) {
26302 * @name ui.grid.treeView
26305 * # ui.grid.treeView
26307 * <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>
26309 * This module provides a tree view of the data that it is provided, with nodes in that
26310 * tree and leaves. Unlike grouping, the tree is an inherent property of the data and must
26311 * be provided with your data array.
26313 * Design information:
26314 * -------------------
26316 * TreeView uses treeBase for the underlying functionality, and is a very thin wrapper around
26317 * that logic. Most of the design information has now moved to treebase.
26321 * <div doc-module-components="ui.grid.treeView"></div>
26324 var module = angular.module('ui.grid.treeView', ['ui.grid', 'ui.grid.treeBase']);
26328 * @name ui.grid.treeView.constant:uiGridTreeViewConstants
26330 * @description constants available in treeView module, this includes
26331 * all the constants declared in the treeBase module (these are manually copied
26332 * as there isn't an easy way to include constants in another constants file, and
26333 * we don't want to make users include treeBase)
26336 module.constant('uiGridTreeViewConstants', {
26337 featureName: "treeView",
26338 rowHeaderColName: 'treeBaseRowHeaderCol',
26339 EXPANDED: 'expanded',
26340 COLLAPSED: 'collapsed',
26352 * @name ui.grid.treeView.service:uiGridTreeViewService
26354 * @description Services for treeView features
26356 module.service('uiGridTreeViewService', ['$q', 'uiGridTreeViewConstants', 'uiGridTreeBaseConstants', 'uiGridTreeBaseService', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants',
26357 function ($q, uiGridTreeViewConstants, uiGridTreeBaseConstants, uiGridTreeBaseService, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants) {
26361 initializeGrid: function (grid, $scope) {
26362 uiGridTreeBaseService.initializeGrid( grid, $scope );
26366 * @name ui.grid.treeView.grid:treeView
26368 * @description Grid properties and functions added for treeView
26370 grid.treeView = {};
26372 grid.registerRowsProcessor(service.adjustSorting, 60);
26376 * @name ui.grid.treeView.api:PublicApi
26378 * @description Public Api for treeView feature
26391 grid.api.registerEventsFromObject(publicApi.events);
26393 grid.api.registerMethodsFromObject(publicApi.methods);
26397 defaultGridOptions: function (gridOptions) {
26398 //default option to true unless it was explicitly set to false
26401 * @name ui.grid.treeView.api:GridOptions
26403 * @description GridOptions for treeView feature, these are available to be
26404 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
26406 * Many tree options are set on treeBase, make sure to look at that feature in
26407 * conjunction with these options.
26412 * @name enableTreeView
26413 * @propertyOf ui.grid.treeView.api:GridOptions
26414 * @description Enable row tree view for entire grid.
26415 * <br/>Defaults to true
26417 gridOptions.enableTreeView = gridOptions.enableTreeView !== false;
26424 * @name adjustSorting
26425 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26426 * @description Trees cannot be sorted the same as flat lists of rows -
26427 * trees are sorted recursively within each level - so the children of each
26428 * node are sorted, but not the full set of rows.
26430 * To achieve this, we suppress the normal sorting by setting ignoreSort on
26431 * each of the sort columns. When the treeBase rowsProcessor runs it will then
26432 * unignore these, and will perform a recursive sort against the tree that it builds.
26434 * @param {array} renderableRows the rows that we need to pass on through
26435 * @returns {array} renderableRows that we passed on through
26437 adjustSorting: function( renderableRows ) {
26440 grid.columns.forEach( function( column ){
26441 if ( column.sort ){
26442 column.sort.ignoreSort = true;
26446 return renderableRows;
26457 * @name ui.grid.treeView.directive:uiGridTreeView
26461 * @description Adds treeView features to grid
26464 <example module="app">
26465 <file name="app.js">
26466 var app = angular.module('app', ['ui.grid', 'ui.grid.treeView']);
26468 app.controller('MainCtrl', ['$scope', function ($scope) {
26470 { name: 'Bob', title: 'CEO' },
26471 { name: 'Frank', title: 'Lowly Developer' }
26474 $scope.columnDefs = [
26475 {name: 'name', enableCellEdit: true},
26476 {name: 'title', enableCellEdit: true}
26479 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
26482 <file name="index.html">
26483 <div ng-controller="MainCtrl">
26484 <div ui-grid="gridOptions" ui-grid-tree-view></div>
26489 module.directive('uiGridTreeView', ['uiGridTreeViewConstants', 'uiGridTreeViewService', '$templateCache',
26490 function (uiGridTreeViewConstants, uiGridTreeViewService, $templateCache) {
26494 require: '^uiGrid',
26496 compile: function () {
26498 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
26499 if (uiGridCtrl.grid.options.enableTreeView !== false){
26500 uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope);
26503 post: function ($scope, $elm, $attrs, uiGridCtrl) {
26512 angular.module('ui.grid').run(['$templateCache', function($templateCache) {
26515 $templateCache.put('ui-grid/ui-grid-filter',
26516 "<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>"
26520 $templateCache.put('ui-grid/ui-grid-footer',
26521 "<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>"
26525 $templateCache.put('ui-grid/ui-grid-grid-footer',
26526 "<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>"
26530 $templateCache.put('ui-grid/ui-grid-group-panel',
26531 "<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>"
26535 $templateCache.put('ui-grid/ui-grid-header',
26536 "<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>"
26540 $templateCache.put('ui-grid/ui-grid-menu-button',
26541 "<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>"
26545 $templateCache.put('ui-grid/ui-grid-no-header',
26546 "<div class=\"ui-grid-top-panel\"></div>"
26550 $templateCache.put('ui-grid/ui-grid-row',
26551 "<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>"
26555 $templateCache.put('ui-grid/ui-grid',
26556 "<div ui-i18n=\"en\" class=\"ui-grid\"><!-- TODO (c0bra): add \"scoped\" attr here, eventually? --><style ui-grid-style>.grid{{ grid.id }} {\n" +
26557 " /* Styles for the grid */\n" +
26560 " .grid{{ grid.id }} .ui-grid-row, .grid{{ grid.id }} .ui-grid-cell, .grid{{ grid.id }} .ui-grid-cell .ui-grid-vertical-bar {\n" +
26561 " height: {{ grid.options.rowHeight }}px;\n" +
26564 " .grid{{ grid.id }} .ui-grid-row:last-child .ui-grid-cell {\n" +
26565 " border-bottom-width: {{ ((grid.getTotalRowHeight() < grid.getViewportHeight()) && '1') || '0' }}px;\n" +
26568 " {{ grid.verticalScrollbarStyles }}\n" +
26569 " {{ grid.horizontalScrollbarStyles }}\n" +
26572 " .ui-grid[dir=rtl] .ui-grid-viewport {\n" +
26573 " padding-left: {{ grid.verticalScrollbarWidth }}px;\n" +
26577 " {{ 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>"
26581 $templateCache.put('ui-grid/uiGridCell',
26582 "<div class=\"ui-grid-cell-contents\" title=\"TOOLTIP\">{{COL_FIELD CUSTOM_FILTERS}}</div>"
26586 $templateCache.put('ui-grid/uiGridColumnMenu',
26587 "<div class=\"ui-grid-column-menu\"><div ui-grid-menu menu-items=\"menuItems\"><!-- <div class=\"ui-grid-column-menu\">\n" +
26588 " <div class=\"inner\" ng-show=\"menuShown\">\n" +
26590 " <div ng-show=\"grid.options.enableSorting\">\n" +
26591 " <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" +
26592 " <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" +
26593 " <li ng-show=\"col.sort.direction\" ng-click=\"unsortColumn()\"><i class=\"ui-grid-icon-cancel\"></i> Remove Sort</li>\n" +
26597 " </div> --></div></div>"
26601 $templateCache.put('ui-grid/uiGridFooterCell',
26602 "<div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><div>{{ col.getAggregationText() + ( col.getAggregationValue() CUSTOM_FILTERS ) }}</div></div>"
26606 $templateCache.put('ui-grid/uiGridHeaderCell',
26607 "<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=\"{{col.sort.priority ? i18n.headerCell.priority + ' ' + col.sort.priority : null}}\" aria-hidden=\"true\"></i> <sub class=\"ui-grid-sort-priority-number\">{{col.sort.priority}}</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>"
26611 $templateCache.put('ui-grid/uiGridMenu',
26612 "<div class=\"ui-grid-menu\" ng-if=\"shown\"><div class=\"ui-grid-menu-mid\" ng-show=\"shownMid\"><div class=\"ui-grid-menu-inner\"><button type=\"button\" ng-focus=\"focus=true\" ng-blur=\"focus=false\" class=\"ui-grid-menu-close-button\" ng-class=\"{'ui-grid-sr-only': (!focus)}\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"i18n.close\"></i></button><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>"
26616 $templateCache.put('ui-grid/uiGridMenuItem',
26617 "<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>"
26621 $templateCache.put('ui-grid/uiGridRenderContainer',
26622 "<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>"
26626 $templateCache.put('ui-grid/uiGridViewport',
26627 "<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>"
26631 $templateCache.put('ui-grid/cellEditor',
26632 "<div><form name=\"inputForm\"><input type=\"INPUT_TYPE\" ng-class=\"'colt' + col.uid\" ui-grid-editor ng-model=\"MODEL_COL_FIELD\"></form></div>"
26636 $templateCache.put('ui-grid/dropdownEditor',
26637 "<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>"
26641 $templateCache.put('ui-grid/fileChooserEditor',
26642 "<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>"
26646 $templateCache.put('ui-grid/expandableRow',
26647 "<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>"
26651 $templateCache.put('ui-grid/expandableRowHeader',
26652 "<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>"
26656 $templateCache.put('ui-grid/expandableScrollFiller',
26657 "<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>"
26661 $templateCache.put('ui-grid/expandableTopRowHeader',
26662 "<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>"
26666 $templateCache.put('ui-grid/csvLink',
26667 "<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>"
26671 $templateCache.put('ui-grid/importerMenuItem',
26672 "<li class=\"ui-grid-menu-item\"><form><input class=\"ui-grid-importer-file-chooser\" type=\"file\" id=\"files\" name=\"files[]\"></form></li>"
26676 $templateCache.put('ui-grid/importerMenuItemContainer',
26677 "<div ui-grid-importer-menu-item></div>"
26681 $templateCache.put('ui-grid/pagination',
26682 "<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 class=\"first-triangle\"><div class=\"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 class=\"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 class=\"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 class=\"last-triangle\"><div class=\"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>"
26686 $templateCache.put('ui-grid/columnResizer',
26687 "<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>"
26691 $templateCache.put('ui-grid/gridFooterSelectedItems',
26692 "<span ng-if=\"grid.selection.selectedCount !== 0 && grid.options.enableFooterTotalSelected\">({{\"search.selectedItems\" | t}} {{grid.selection.selectedCount}})</span>"
26696 $templateCache.put('ui-grid/selectionHeaderCell',
26697 "<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>"
26701 $templateCache.put('ui-grid/selectionRowHeader',
26702 "<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>"
26706 $templateCache.put('ui-grid/selectionRowHeaderButtons',
26707 "<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>"
26711 $templateCache.put('ui-grid/selectionSelectAllButtons',
26712 "<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>"
26716 $templateCache.put('ui-grid/treeBaseExpandAllButtons',
26717 "<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>"
26721 $templateCache.put('ui-grid/treeBaseHeaderCell',
26722 "<div><div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><ui-grid-tree-base-expand-all-buttons></ui-grid-tree-base-expand-all-buttons></div></div>"
26726 $templateCache.put('ui-grid/treeBaseRowHeader',
26727 "<div class=\"ui-grid-cell-contents\"><ui-grid-tree-base-row-header-buttons></ui-grid-tree-base-row-header-buttons></div>"
26731 $templateCache.put('ui-grid/treeBaseRowHeaderButtons',
26732 "<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>"