2 * ui-grid - v3.1.0 - 2016-01-19
3 * Copyright (c) 2016 ; License: MIT
8 angular.module('ui.grid.i18n', []);
9 angular.module('ui.grid', ['ui.grid.i18n']);
13 angular.module('ui.grid').constant('uiGridConstants', {
14 LOG_DEBUG_MESSAGES: true,
15 LOG_WARN_MESSAGES: true,
16 LOG_ERROR_MESSAGES: true,
17 CUSTOM_FILTERS: /CUSTOM_FILTERS/g,
18 COL_FIELD: /COL_FIELD/g,
19 MODEL_COL_FIELD: /MODEL_COL_FIELD/g,
20 TOOLTIP: /title=\"TOOLTIP\"/g,
21 DISPLAY_CELL_TEMPLATE: /DISPLAY_CELL_TEMPLATE/g,
22 TEMPLATE_REGEXP: /<.+>/,
23 FUNC_REGEXP: /(\([^)]*\))?$/,
26 BRACKET_REGEXP: /^(.*)((?:\s*\[\s*\d+\s*\]\s*)|(?:\s*\[\s*"(?:[^"\\]|\\.)*"\s*\]\s*)|(?:\s*\[\s*'(?:[^'\\]|\\.)*'\s*\]\s*))(.*)$/,
27 COL_CLASS_PREFIX: 'ui-grid-col',
29 GRID_SCROLL: 'uiGridScroll',
30 COLUMN_MENU_SHOWN: 'uiGridColMenuShown',
31 ITEM_DRAGGING: 'uiGridItemDragStart', // For any item being dragged
32 COLUMN_HEADER_CLICK: 'uiGridColumnHeaderClick'
34 // copied from http://www.lsauer.com/2011/08/javascript-keymap-keycodes-in-json.html
83 GREATER_THAN_OR_EQUAL: 64,
85 LESS_THAN_OR_EQUAL: 256,
99 // TODO(c0bra): Create full list of these somehow. NOTE: do any allow a space before or after them?
100 CURRENCY_SYMBOLS: ['ƒ', '$', '£', '$', '¤', '¥', '៛', '₩', '₱', '฿', '₫'],
126 angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUtil', 'uiGridConstants', function ($compile, $parse, gridUtil, uiGridConstants) {
131 compile: function() {
133 pre: function($scope, $elm, $attrs, uiGridCtrl) {
134 function compileTemplate() {
135 var compiledElementFn = $scope.col.compiledElementFn;
137 compiledElementFn($scope, function(clonedElement, scope) {
138 $elm.append(clonedElement);
142 // If the grid controller is present, use it to get the compiled cell template function
143 if (uiGridCtrl && $scope.col.compiledElementFn) {
146 // No controller, compile the element manually (for unit tests)
148 if ( uiGridCtrl && !$scope.col.compiledElementFn ){
149 // gridUtil.logError('Render has been called before precompile. Please log a ui-grid issue');
151 $scope.col.getCompiledElementFn()
152 .then(function (compiledElementFn) {
153 compiledElementFn($scope, function(clonedElement, scope) {
154 $elm.append(clonedElement);
159 var html = $scope.col.cellTemplate
160 .replace(uiGridConstants.MODEL_COL_FIELD, 'row.entity.' + gridUtil.preEval($scope.col.field))
161 .replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
163 var cellElement = $compile(html)($scope);
164 $elm.append(cellElement);
168 post: function($scope, $elm, $attrs, uiGridCtrl) {
169 var initColClass = $scope.col.getColClass(false);
170 $elm.addClass(initColClass);
173 var updateClass = function( grid ){
176 contents.removeClass( classAdded );
180 if (angular.isFunction($scope.col.cellClass)) {
181 classAdded = $scope.col.cellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
184 classAdded = $scope.col.cellClass;
186 contents.addClass(classAdded);
189 if ($scope.col.cellClass) {
193 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
194 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN, uiGridConstants.dataChange.EDIT]);
196 // watch the col and row to see if they change - which would indicate that we've scrolled or sorted or otherwise
197 // changed the row/col that this cell relates to, and we need to re-evaluate cell classes and maybe other things
198 var cellChangeFunction = function( n, o ){
200 if ( classAdded || $scope.col.cellClass ){
204 // See if the column's internal class has changed
205 var newColClass = $scope.col.getColClass(false);
206 if (newColClass !== initColClass) {
207 $elm.removeClass(initColClass);
208 $elm.addClass(newColClass);
209 initColClass = newColClass;
214 // TODO(c0bra): Turn this into a deep array watch
215 /* shouldn't be needed any more given track by col.name
216 var colWatchDereg = $scope.$watch( 'col', cellChangeFunction );
218 var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction );
221 var deregisterFunction = function() {
227 $scope.$on( '$destroy', deregisterFunction );
228 $elm.on( '$destroy', deregisterFunction );
240 angular.module('ui.grid')
241 .service('uiGridColumnMenuService', [ 'i18nService', 'uiGridConstants', 'gridUtil',
242 function ( i18nService, uiGridConstants, gridUtil ) {
245 * @name ui.grid.service:uiGridColumnMenuService
247 * @description Services for working with column menus, factored out
248 * to make the code easier to understand
254 * @methodOf ui.grid.service:uiGridColumnMenuService
256 * @description Sets defaults, puts a reference to the $scope on
257 * the uiGridController
258 * @param {$scope} $scope the $scope from the uiGridColumnMenu
259 * @param {controller} uiGridCtrl the uiGridController for the grid
263 initialize: function( $scope, uiGridCtrl ){
264 $scope.grid = uiGridCtrl.grid;
266 // Store a reference to this link/controller in the main uiGrid controller
267 // to allow showMenu later
268 uiGridCtrl.columnMenuScope = $scope;
270 // Save whether we're shown or not so the columns can check
271 $scope.menuShown = false;
277 * @methodOf ui.grid.service:uiGridColumnMenuService
278 * @name setColMenuItemWatch
279 * @description Setup a watch on $scope.col.menuItems, and update
280 * menuItems based on this. $scope.col needs to be set by the column
281 * before calling the menu.
282 * @param {$scope} $scope the $scope from the uiGridColumnMenu
283 * @param {controller} uiGridCtrl the uiGridController for the grid
287 setColMenuItemWatch: function ( $scope ){
288 var deregFunction = $scope.$watch('col.menuItems', function (n, 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) {
576 uiGridColumnMenuService.initialize( $scope, uiGridCtrl );
578 $scope.defaultMenuItems = uiGridColumnMenuService.getDefaultMenuItems( $scope );
580 // Set the menu items for use with the column menu. The user can later add additional items via the watch
581 $scope.menuItems = $scope.defaultMenuItems;
582 uiGridColumnMenuService.setColMenuItemWatch( $scope );
587 * @methodOf ui.grid.directive:uiGridColumnMenu
589 * @description Shows the column menu. If the menu is already displayed it
590 * calls the menu to ask it to hide (it will animate), then it repositions the menu
591 * to the right place whilst hidden (it will make an assumption on menu width),
592 * then it asks the menu to show (it will animate), then it repositions the menu again
593 * once we can calculate it's size.
594 * @param {GridCol} column the column we want to position below
595 * @param {element} $columnElement the column element we want to position below
597 $scope.showMenu = function(column, $columnElement, event) {
598 // Swap to this column
601 // Get the position information for the column element
602 var colElementPosition = uiGridColumnMenuService.getColumnElementPosition( $scope, column, $columnElement );
604 if ($scope.menuShown) {
605 // we want to hide, then reposition, then show, but we want to wait for animations
606 // we set a variable, and then rely on the menu-hidden event to call the reposition and show
607 $scope.colElement = $columnElement;
608 $scope.colElementPosition = colElementPosition;
609 $scope.hideThenShow = true;
611 $scope.$broadcast('hide-menu', { originalEvent: event });
613 $scope.menuShown = true;
614 uiGridColumnMenuService.repositionMenu( $scope, column, colElementPosition, $elm, $columnElement );
616 $scope.colElement = $columnElement;
617 $scope.colElementPosition = colElementPosition;
618 $scope.$broadcast('show-menu', { originalEvent: event });
625 * @methodOf ui.grid.directive:uiGridColumnMenu
627 * @description Hides the column menu.
628 * @param {boolean} broadcastTrigger true if we were triggered by a broadcast
629 * from the menu itself - in which case don't broadcast again as we'll get
632 $scope.hideMenu = function( broadcastTrigger ) {
633 $scope.menuShown = false;
634 if ( !broadcastTrigger ){
635 $scope.$broadcast('hide-menu');
640 $scope.$on('menu-hidden', function() {
641 if ( $scope.hideThenShow ){
642 delete $scope.hideThenShow;
644 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
645 $scope.$broadcast('show-menu');
647 $scope.menuShown = true;
649 $scope.hideMenu( true );
652 //Focus on the menu button
653 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false);
658 $scope.$on('menu-shown', function() {
659 $timeout( function() {
660 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
661 delete $scope.colElementPosition;
662 delete $scope.columnElement;
668 $scope.sortColumn = function (event, dir) {
669 event.stopPropagation();
671 $scope.grid.sortColumn($scope.col, dir, true)
673 $scope.grid.refresh();
678 $scope.unsortColumn = function () {
681 $scope.grid.refresh();
685 //Since we are hiding this column the default hide action will fail so we need to focus somewhere else.
686 var setFocusOnHideColumn = function(){
688 // Get the UID of the first
689 var focusToGridMenu = function(){
690 return gridUtil.focus.byId('grid-menu', $scope.grid);
694 $scope.grid.columns.some(function(element, index){
695 if (angular.equals(element, $scope.col)) {
701 var previousVisibleCol;
702 // Try and find the next lower or nearest column to focus on
703 $scope.grid.columns.some(function(element, index){
704 if (!element.visible){
706 } // This columns index is below the current column index
707 else if ( index < thisIndex){
708 previousVisibleCol = element;
709 } // This elements index is above this column index and we haven't found one that is lower
710 else if ( index > thisIndex && !previousVisibleCol) {
711 // This is the next best thing
712 previousVisibleCol = element;
713 // We've found one so use it.
715 } // We've reached an element with an index above this column and the previousVisibleCol variable has been set
716 else if (index > thisIndex && previousVisibleCol) {
721 // If found then focus on it
722 if (previousVisibleCol){
723 var colClass = previousVisibleCol.getColClass();
724 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + colClass+ ' .ui-grid-header-cell-primary-focus', true).then(angular.noop, function(reason){
725 if (reason !== 'canceled'){ // If this is canceled then don't perform the action
726 //The fallback action is to focus on the grid menu
727 return focusToGridMenu();
731 // Fallback action to focus on the grid menu
737 $scope.hideColumn = function () {
738 $scope.col.colDef.visible = false;
739 $scope.col.visible = false;
741 $scope.grid.queueGridRefresh();
743 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
744 $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col );
746 // We are hiding so the default action of focusing on the button that opened this menu will fail.
747 setFocusOnHideColumn();
753 controller: ['$scope', function ($scope) {
756 $scope.$watch('menuItems', function (n, o) {
762 return uiGridColumnMenu;
771 angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', 'i18nService', 'gridUtil', function ($compile, $templateCache, i18nService, gridUtil) {
774 compile: function() {
776 pre: function ($scope, $elm, $attrs, controllers) {
777 $scope.col.updateFilters = function( filterable ){
778 $elm.children().remove();
780 var template = $scope.col.filterHeaderTemplate;
782 $elm.append($compile(template)($scope));
786 $scope.$on( '$destroy', function() {
787 delete $scope.col.updateFilters;
790 post: function ($scope, $elm, $attrs, controllers){
791 $scope.aria = i18nService.getSafeText('headerCell.aria');
792 $scope.removeFilter = function(colFilter, index){
793 colFilter.term = null;
794 //Set the focus to the filter input after the action disables the button
795 gridUtil.focus.bySelector($elm, '.ui-grid-filter-input-' + index);
807 angular.module('ui.grid').directive('uiGridFooterCell', ['$timeout', 'gridUtil', 'uiGridConstants', '$compile',
808 function ($timeout, gridUtil, uiGridConstants, $compile) {
809 var uiGridFooterCell = {
818 compile: function compile(tElement, tAttrs, transclude) {
820 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
821 var cellFooter = $compile($scope.col.footerCellTemplate)($scope);
822 $elm.append(cellFooter);
824 post: function ($scope, $elm, $attrs, uiGridCtrl) {
825 //$elm.addClass($scope.col.getColClass(false));
826 $scope.grid = uiGridCtrl.grid;
828 var initColClass = $scope.col.getColClass(false);
829 $elm.addClass(initColClass);
831 // apply any footerCellClass
833 var updateClass = function( grid ){
836 contents.removeClass( classAdded );
840 if (angular.isFunction($scope.col.footerCellClass)) {
841 classAdded = $scope.col.footerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
844 classAdded = $scope.col.footerCellClass;
846 contents.addClass(classAdded);
849 if ($scope.col.footerCellClass) {
853 $scope.col.updateAggregationValue();
855 // Watch for column changes so we can alter the col cell class properly
856 /* shouldn't be needed any more, given track by col.name
857 $scope.$watch('col', function (n, o) {
859 // See if the column's internal class has changed
860 var newColClass = $scope.col.getColClass(false);
861 if (newColClass !== initColClass) {
862 $elm.removeClass(initColClass);
863 $elm.addClass(newColClass);
864 initColClass = newColClass;
871 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
872 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]);
873 // listen for visible rows change and update aggregation values
874 $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue );
875 $scope.grid.api.core.on.rowsRendered( $scope, updateClass );
876 $scope.$on( '$destroy', dataChangeDereg );
882 return uiGridFooterCell;
890 angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
896 require: ['^uiGrid', '^uiGridRenderContainer'],
898 compile: function ($elm, $attrs) {
900 pre: function ($scope, $elm, $attrs, controllers) {
901 var uiGridCtrl = controllers[0];
902 var containerCtrl = controllers[1];
904 $scope.grid = uiGridCtrl.grid;
905 $scope.colContainer = containerCtrl.colContainer;
907 containerCtrl.footer = $elm;
909 var footerTemplate = $scope.grid.options.footerTemplate;
910 gridUtil.getTemplate(footerTemplate)
911 .then(function (contents) {
912 var template = angular.element(contents);
914 var newElm = $compile(template)($scope);
918 // Inject a reference to the footer viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
919 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
921 if (footerViewport) {
922 containerCtrl.footerViewport = footerViewport;
928 post: function ($scope, $elm, $attrs, controllers) {
929 var uiGridCtrl = controllers[0];
930 var containerCtrl = controllers[1];
932 // gridUtil.logDebug('ui-grid-footer link');
934 var grid = uiGridCtrl.grid;
936 // Don't animate footer cells
937 gridUtil.disableAnimations($elm);
939 containerCtrl.footer = $elm;
941 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
942 if (footerViewport) {
943 containerCtrl.footerViewport = footerViewport;
955 angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
963 compile: function ($elm, $attrs) {
965 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
967 $scope.grid = uiGridCtrl.grid;
971 var footerTemplate = $scope.grid.options.gridFooterTemplate;
972 gridUtil.getTemplate(footerTemplate)
973 .then(function (contents) {
974 var template = angular.element(contents);
976 var newElm = $compile(template)($scope);
981 post: function ($scope, $elm, $attrs, controllers) {
993 angular.module('ui.grid').directive('uiGridGroupPanel', ["$compile", "uiGridConstants", "gridUtil", function($compile, uiGridConstants, gridUtil) {
994 var defaultTemplate = 'ui-grid/ui-grid-group-panel';
1001 compile: function($elm, $attrs) {
1003 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
1004 var groupPanelTemplate = $scope.grid.options.groupPanelTemplate || defaultTemplate;
1006 gridUtil.getTemplate(groupPanelTemplate)
1007 .then(function (contents) {
1008 var template = angular.element(contents);
1010 var newElm = $compile(template)($scope);
1011 $elm.append(newElm);
1015 post: function ($scope, $elm, $attrs, uiGridCtrl) {
1016 $elm.bind('$destroy', function() {
1017 // scrollUnbinder();
1029 angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService',
1030 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) {
1031 // Do stuff after mouse has been down this many ms on the header cell
1032 var mousedownTimeout = 500;
1033 var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa
1035 var uiGridHeaderCell = {
1042 require: ['^uiGrid', '^uiGridRenderContainer'],
1044 compile: function() {
1046 pre: function ($scope, $elm, $attrs) {
1047 var cellHeader = $compile($scope.col.headerCellTemplate)($scope);
1048 $elm.append(cellHeader);
1051 post: function ($scope, $elm, $attrs, controllers) {
1052 var uiGridCtrl = controllers[0];
1053 var renderContainerCtrl = controllers[1];
1056 headerCell: i18nService.getSafeText('headerCell'),
1057 sort: i18nService.getSafeText('sort')
1059 $scope.isSortPriorityVisible = function() {
1060 //show sort priority if column is sorted and there is at least one other sorted column
1061 return $scope.col.sort.priority && $scope.grid.columns.some(function(element, index){
1062 return element.sort.priority && element !== $scope.col;
1065 $scope.getSortDirectionAriaLabel = function(){
1066 var col = $scope.col;
1067 //Trying to recreate this sort of thing but it was getting messy having it in the template.
1068 //Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''}
1069 var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none);
1070 var label = sortDirectionText;
1072 if ($scope.isSortPriorityVisible()) {
1073 label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority;
1078 $scope.grid = uiGridCtrl.grid;
1080 $scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId];
1082 var initColClass = $scope.col.getColClass(false);
1083 $elm.addClass(initColClass);
1085 // Hide the menu by default
1086 $scope.menuShown = false;
1088 // Put asc and desc sort directions in scope
1089 $scope.asc = uiGridConstants.ASC;
1090 $scope.desc = uiGridConstants.DESC;
1092 // Store a reference to menu element
1093 var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') );
1095 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
1098 // apply any headerCellClass
1103 var filterDeregisters = [];
1107 * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart).
1108 * Once we have a down event, we need to work out whether we have a click, a drag, or a
1109 * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
1110 * we ignore it. A hold would open the menu.
1112 * So, on down event, we put in place handlers for move and up events, and a timer. If the
1113 * timer expires before we see a move or up, then we have a long press and hence a column menu open.
1114 * If the up happens before the timer, then we have a click, and we sort if the column is sortable.
1115 * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature
1118 * To deal with touch enabled devices that also have mice, we only create our handlers when
1119 * we get the down event, and we create the corresponding handlers - if we're touchstart then
1120 * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup.
1122 * We also suppress the click action whilst this is happening - otherwise after the mouseup there
1123 * will be a click event and that can cause the column menu to close
1127 $scope.downFn = function( event ){
1128 event.stopPropagation();
1130 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
1131 event = event.originalEvent;
1134 // Don't show the menu if it's not the left button
1135 if (event.button && event.button !== 0) {
1138 previousMouseX = event.pageX;
1140 $scope.mousedownStartTime = (new Date()).getTime();
1141 $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout);
1143 $scope.mousedownTimeout.then(function () {
1144 if ( $scope.colMenu ) {
1145 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event);
1149 uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name});
1151 $scope.offAllEvents();
1152 if ( event.type === 'touchstart'){
1153 $document.on('touchend', $scope.upFn);
1154 $document.on('touchmove', $scope.moveFn);
1155 } else if ( event.type === 'mousedown' ){
1156 $document.on('mouseup', $scope.upFn);
1157 $document.on('mousemove', $scope.moveFn);
1161 $scope.upFn = function( event ){
1162 event.stopPropagation();
1163 $timeout.cancel($scope.mousedownTimeout);
1164 $scope.offAllEvents();
1165 $scope.onDownEvents(event.type);
1167 var mousedownEndTime = (new Date()).getTime();
1168 var mousedownTime = mousedownEndTime - $scope.mousedownStartTime;
1170 if (mousedownTime > mousedownTimeout) {
1171 // long click, handled above with mousedown
1175 if ( $scope.sortable ){
1176 $scope.handleClick(event);
1181 $scope.moveFn = function( event ){
1182 // Chrome is known to fire some bogus move events.
1183 var changeValue = event.pageX - previousMouseX;
1184 if ( changeValue === 0 ){ return; }
1186 // we're a move, so do nothing and leave for column move (if enabled) to take over
1187 $timeout.cancel($scope.mousedownTimeout);
1188 $scope.offAllEvents();
1189 $scope.onDownEvents(event.type);
1192 $scope.clickFn = function ( event ){
1193 event.stopPropagation();
1194 $contentsElm.off('click', $scope.clickFn);
1198 $scope.offAllEvents = function(){
1199 $contentsElm.off('touchstart', $scope.downFn);
1200 $contentsElm.off('mousedown', $scope.downFn);
1202 $document.off('touchend', $scope.upFn);
1203 $document.off('mouseup', $scope.upFn);
1205 $document.off('touchmove', $scope.moveFn);
1206 $document.off('mousemove', $scope.moveFn);
1208 $contentsElm.off('click', $scope.clickFn);
1211 $scope.onDownEvents = function( type ){
1212 // If there is a previous event, then wait a while before
1213 // activating the other mode - i.e. if the last event was a touch event then
1214 // don't enable mouse events for a wee while (500ms or so)
1215 // Avoids problems with devices that emulate mouse events when you have touch events
1220 $contentsElm.on('click', $scope.clickFn);
1221 $contentsElm.on('touchstart', $scope.downFn);
1222 $timeout(function(){
1223 $contentsElm.on('mousedown', $scope.downFn);
1224 }, changeModeTimeout);
1228 $contentsElm.on('click', $scope.clickFn);
1229 $contentsElm.on('mousedown', $scope.downFn);
1230 $timeout(function(){
1231 $contentsElm.on('touchstart', $scope.downFn);
1232 }, changeModeTimeout);
1235 $contentsElm.on('click', $scope.clickFn);
1236 $contentsElm.on('touchstart', $scope.downFn);
1237 $contentsElm.on('mousedown', $scope.downFn);
1242 var updateHeaderOptions = function( grid ){
1243 var contents = $elm;
1245 contents.removeClass( classAdded );
1249 if (angular.isFunction($scope.col.headerCellClass)) {
1250 classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
1253 classAdded = $scope.col.headerCellClass;
1255 contents.addClass(classAdded);
1257 $timeout(function (){
1258 var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body'];
1259 $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] );
1262 // Figure out whether this column is sortable or not
1263 if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) {
1264 $scope.sortable = true;
1267 $scope.sortable = false;
1270 // Figure out whether this column is filterable or not
1271 var oldFilterable = $scope.filterable;
1272 if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) {
1273 $scope.filterable = true;
1276 $scope.filterable = false;
1279 if ( oldFilterable !== $scope.filterable){
1280 if ( typeof($scope.col.updateFilters) !== 'undefined' ){
1281 $scope.col.updateFilters($scope.filterable);
1284 // if column is filterable add a filter watcher
1285 if ($scope.filterable) {
1286 $scope.col.filters.forEach( function(filter, i) {
1287 filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) {
1289 uiGridCtrl.grid.api.core.raise.filterChanged();
1290 uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1291 uiGridCtrl.grid.queueGridRefresh();
1295 $scope.$on('$destroy', function() {
1296 filterDeregisters.forEach( function(filterDeregister) {
1301 filterDeregisters.forEach( function(filterDeregister) {
1308 // figure out whether we support column menus
1309 if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
1310 $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){
1311 $scope.colMenu = true;
1313 $scope.colMenu = false;
1318 * @name enableColumnMenu
1319 * @propertyOf ui.grid.class:GridOptions.columnDef
1320 * @description if column menus are enabled, controls the column menus for this specific
1321 * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus
1322 * using this option. If gridOptions.enableColumnMenus === false then you get no column
1323 * menus irrespective of the value of this option ). Defaults to true.
1328 * @name enableColumnMenus
1329 * @propertyOf ui.grid.class:GridOptions.columnDef
1330 * @description Override for column menus everywhere - if set to false then you get no
1331 * column menus. Defaults to true.
1335 $scope.offAllEvents();
1337 if ($scope.sortable || $scope.colMenu) {
1338 $scope.onDownEvents();
1340 $scope.$on('$destroy', function () {
1341 $scope.offAllEvents();
1347 $scope.$watch('col', function (n, o) {
1349 // See if the column's internal class has changed
1350 var newColClass = $scope.col.getColClass(false);
1351 if (newColClass !== initColClass) {
1352 $elm.removeClass(initColClass);
1353 $elm.addClass(newColClass);
1354 initColClass = newColClass;
1359 updateHeaderOptions();
1361 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
1362 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]);
1364 $scope.$on( '$destroy', dataChangeDereg );
1366 $scope.handleClick = function(event) {
1367 // If the shift key is being held down, add this column to the sort
1369 if (event.shiftKey) {
1373 // Sort this column then rebuild the grid's rows
1374 uiGridCtrl.grid.sortColumn($scope.col, add)
1376 if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
1377 uiGridCtrl.grid.refresh();
1382 $scope.toggleMenu = function(event) {
1383 event.stopPropagation();
1385 // If the menu is already showing...
1386 if (uiGridCtrl.columnMenuScope.menuShown) {
1387 // ... and we're the column the menu is on...
1388 if (uiGridCtrl.columnMenuScope.col === $scope.col) {
1390 uiGridCtrl.columnMenuScope.hideMenu();
1392 // ... and we're NOT the column the menu is on
1394 // ... move the menu to our column
1395 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1398 // If the menu is NOT showing
1400 // ... show it on our column
1401 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1409 return uiGridHeaderCell;
1417 angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent',
1418 function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) {
1419 var defaultTemplate = 'ui-grid/ui-grid-header';
1420 var emptyTemplate = 'ui-grid/ui-grid-no-header';
1424 // templateUrl: 'ui-grid/ui-grid-header',
1427 require: ['^uiGrid', '^uiGridRenderContainer'],
1429 compile: function($elm, $attrs) {
1431 pre: function ($scope, $elm, $attrs, controllers) {
1432 var uiGridCtrl = controllers[0];
1433 var containerCtrl = controllers[1];
1435 $scope.grid = uiGridCtrl.grid;
1436 $scope.colContainer = containerCtrl.colContainer;
1438 updateHeaderReferences();
1441 if (!$scope.grid.options.showHeader) {
1442 headerTemplate = emptyTemplate;
1445 headerTemplate = ($scope.grid.options.headerTemplate) ? $scope.grid.options.headerTemplate : defaultTemplate;
1448 gridUtil.getTemplate(headerTemplate)
1449 .then(function (contents) {
1450 var template = angular.element(contents);
1452 var newElm = $compile(template)($scope);
1453 $elm.replaceWith(newElm);
1455 // And update $elm to be the new element
1458 updateHeaderReferences();
1460 if (containerCtrl) {
1461 // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
1462 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1465 if (headerViewport) {
1466 containerCtrl.headerViewport = headerViewport;
1467 angular.element(headerViewport).on('scroll', scrollHandler);
1468 $scope.$on('$destroy', function () {
1469 angular.element(headerViewport).off('scroll', scrollHandler);
1474 $scope.grid.queueRefresh();
1477 function updateHeaderReferences() {
1478 containerCtrl.header = containerCtrl.colContainer.header = $elm;
1480 var headerCanvases = $elm[0].getElementsByClassName('ui-grid-header-canvas');
1482 if (headerCanvases.length > 0) {
1483 containerCtrl.headerCanvas = containerCtrl.colContainer.headerCanvas = headerCanvases[0];
1486 containerCtrl.headerCanvas = null;
1490 function scrollHandler(evt) {
1491 if (uiGridCtrl.grid.isScrollingHorizontally) {
1494 var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid);
1495 var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft);
1497 var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll);
1498 scrollEvent.newScrollLeft = newScrollLeft;
1499 if ( horizScrollPercentage > -1 ){
1500 scrollEvent.x = { percentage: horizScrollPercentage };
1503 uiGridCtrl.grid.scrollContainers(null, scrollEvent);
1507 post: function ($scope, $elm, $attrs, controllers) {
1508 var uiGridCtrl = controllers[0];
1509 var containerCtrl = controllers[1];
1511 // gridUtil.logDebug('ui-grid-header link');
1513 var grid = uiGridCtrl.grid;
1515 // Don't animate header cells
1516 gridUtil.disableAnimations($elm);
1518 function updateColumnWidths() {
1519 // this styleBuilder always runs after the renderContainer, so we can rely on the column widths
1520 // already being populated correctly
1522 var columnCache = containerCtrl.colContainer.visibleColumnCache;
1525 // uiGridCtrl.grid.columns.forEach(function (column) {
1527 var canvasWidth = 0;
1528 columnCache.forEach(function (column) {
1529 ret = ret + column.getColClassDefinition();
1530 canvasWidth += column.drawnWidth;
1533 containerCtrl.colContainer.canvasWidth = canvasWidth;
1535 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
1539 containerCtrl.header = $elm;
1541 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1542 if (headerViewport) {
1543 containerCtrl.headerViewport = headerViewport;
1546 //todo: remove this if by injecting gridCtrl into unit tests
1548 uiGridCtrl.grid.registerStyleComputation({
1550 func: updateColumnWidths
1563 angular.module('ui.grid')
1564 .service('uiGridGridMenuService', [ 'gridUtil', 'i18nService', 'uiGridConstants', function( gridUtil, i18nService, uiGridConstants ) {
1567 * @name ui.grid.gridMenuService
1569 * @description Methods for working with the grid menu
1575 * @methodOf ui.grid.gridMenuService
1577 * @description Sets up the gridMenu. Most importantly, sets our
1578 * scope onto the grid object as grid.gridMenuScope, allowing us
1579 * to operate when passed only the grid. Second most importantly,
1580 * we register the 'addToGridMenu' and 'removeFromGridMenu' methods
1582 * @param {$scope} $scope the scope of this gridMenu
1583 * @param {Grid} grid the grid to which this gridMenu is associated
1585 initialize: function( $scope, grid ){
1586 grid.gridMenuScope = $scope;
1588 $scope.registeredMenuItems = [];
1590 // not certain this is needed, but would be bad to create a memory leak
1591 $scope.$on('$destroy', function() {
1592 if ( $scope.grid && $scope.grid.gridMenuScope ){
1593 $scope.grid.gridMenuScope = null;
1598 if ( $scope.registeredMenuItems ){
1599 $scope.registeredMenuItems = null;
1603 $scope.registeredMenuItems = [];
1607 * @name addToGridMenu
1608 * @methodOf ui.grid.core.api:PublicApi
1609 * @description add items to the grid menu. Used by features
1610 * to add their menu items if they are enabled, can also be used by
1611 * end users to add menu items. This method has the advantage of allowing
1612 * remove again, which can simplify management of which items are included
1613 * in the menu when. (Noting that in most cases the shown and active functions
1614 * provide a better way to handle visibility of menu items)
1615 * @param {Grid} grid the grid on which we are acting
1616 * @param {array} items menu items in the format as described in the tutorial, with
1617 * the added note that if you want to use remove you must also specify an `id` field,
1618 * which is provided when you want to remove an item. The id should be unique.
1621 grid.api.registerMethod( 'core', 'addToGridMenu', service.addToGridMenu );
1625 * @name removeFromGridMenu
1626 * @methodOf ui.grid.core.api:PublicApi
1627 * @description Remove an item from the grid menu based on a provided id. Assumes
1628 * that the id is unique, removes only the last instance of that id. Does nothing if
1629 * the specified id is not found
1630 * @param {Grid} grid the grid on which we are acting
1631 * @param {string} id the id we'd like to remove from the menu
1634 grid.api.registerMethod( 'core', 'removeFromGridMenu', service.removeFromGridMenu );
1640 * @name addToGridMenu
1641 * @propertyOf ui.grid.gridMenuService
1642 * @description add items to the grid menu. Used by features
1643 * to add their menu items if they are enabled, can also be used by
1644 * end users to add menu items. This method has the advantage of allowing
1645 * remove again, which can simplify management of which items are included
1646 * in the menu when. (Noting that in most cases the shown and active functions
1647 * provide a better way to handle visibility of menu items)
1648 * @param {Grid} grid the grid on which we are acting
1649 * @param {array} items menu items in the format as described in the tutorial, with
1650 * the added note that if you want to use remove you must also specify an `id` field,
1651 * which is provided when you want to remove an item. The id should be unique.
1654 addToGridMenu: function( grid, menuItems ) {
1655 if ( !angular.isArray( menuItems ) ) {
1656 gridUtil.logError( 'addToGridMenu: menuItems must be an array, and is not, not adding any items');
1658 if ( grid.gridMenuScope ){
1659 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems ? grid.gridMenuScope.registeredMenuItems : [];
1660 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems.concat( menuItems );
1662 gridUtil.logError( 'Asked to addToGridMenu, but gridMenuScope not present. Timing issue? Please log issue with ui-grid');
1670 * @name removeFromGridMenu
1671 * @methodOf ui.grid.gridMenuService
1672 * @description Remove an item from the grid menu based on a provided id. Assumes
1673 * that the id is unique, removes only the last instance of that id. Does nothing if
1674 * the specified id is not found. If there is no gridMenuScope or registeredMenuItems
1675 * then do nothing silently - the desired result is those menu items not be present and they
1677 * @param {Grid} grid the grid on which we are acting
1678 * @param {string} id the id we'd like to remove from the menu
1681 removeFromGridMenu: function( grid, id ){
1682 var foundIndex = -1;
1684 if ( grid && grid.gridMenuScope ){
1685 grid.gridMenuScope.registeredMenuItems.forEach( function( value, index ) {
1686 if ( value.id === id ){
1687 if (foundIndex > -1) {
1688 gridUtil.logError( 'removeFromGridMenu: found multiple items with the same id, removing only the last' );
1697 if ( foundIndex > -1 ){
1698 grid.gridMenuScope.registeredMenuItems.splice( foundIndex, 1 );
1705 * @name gridMenuCustomItems
1706 * @propertyOf ui.grid.class:GridOptions
1707 * @description (optional) An array of menu items that should be added to
1708 * the gridMenu. Follow the format documented in the tutorial for column
1709 * menu customisation. The context provided to the action function will
1710 * include context.grid. An alternative if working with dynamic menus is to use the
1711 * provided api - core.addToGridMenu and core.removeFromGridMenu, which handles
1712 * some of the management of items for you.
1717 * @name gridMenuShowHideColumns
1718 * @propertyOf ui.grid.class:GridOptions
1719 * @description true by default, whether the grid menu should allow hide/show
1725 * @methodOf ui.grid.gridMenuService
1726 * @name getMenuItems
1727 * @description Decides the menu items to show in the menu. This is a
1730 * - the default menu items that are always included,
1731 * - any menu items that have been provided through the addMenuItem api. These
1732 * are typically added by features within the grid
1733 * - any menu items included in grid.options.gridMenuCustomItems. These can be
1734 * changed dynamically, as they're always recalculated whenever we show the
1736 * @param {$scope} $scope the scope of this gridMenu, from which we can find all
1737 * the information that we need
1738 * @returns {array} an array of menu items that can be shown
1740 getMenuItems: function( $scope ) {
1742 // this is where we add any menu items we want to always include
1745 if ( $scope.grid.options.gridMenuCustomItems ){
1746 if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){
1747 gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not');
1749 menuItems = menuItems.concat( $scope.grid.options.gridMenuCustomItems );
1753 var clearFilters = [{
1754 title: i18nService.getSafeText('gridMenu.clearAllFilters'),
1755 action: function ($event) {
1756 $scope.grid.clearAllFilters(undefined, true, undefined);
1759 return $scope.grid.options.enableFiltering;
1763 menuItems = menuItems.concat( clearFilters );
1765 menuItems = menuItems.concat( $scope.registeredMenuItems );
1767 if ( $scope.grid.options.gridMenuShowHideColumns !== false ){
1768 menuItems = menuItems.concat( service.showHideColumns( $scope ) );
1771 menuItems.sort(function(a, b){
1772 return a.order - b.order;
1781 * @name gridMenuTitleFilter
1782 * @propertyOf ui.grid.class:GridOptions
1783 * @description (optional) A function that takes a title string
1784 * (usually the col.displayName), and converts it into a display value. The function
1785 * must return either a string or a promise.
1787 * Used for internationalization of the grid menu column names - for angular-translate
1788 * you can pass $translate as the function, for i18nService you can pass getSafeText as the
1793 * gridMenuTitleFilter: $translate
1799 * @methodOf ui.grid.gridMenuService
1800 * @name showHideColumns
1801 * @description Adds two menu items for each of the columns in columnDefs. One
1802 * menu item for hide, one menu item for show. Each is visible when appropriate
1803 * (show when column is not visible, hide when column is visible). Each toggles
1804 * the visible property on the columnDef using toggleColumnVisibility
1805 * @param {$scope} $scope of a gridMenu, which contains a reference to the grid
1807 showHideColumns: function( $scope ){
1808 var showHideColumns = [];
1809 if ( !$scope.grid.options.columnDefs || $scope.grid.options.columnDefs.length === 0 || $scope.grid.columns.length === 0 ) {
1810 return showHideColumns;
1813 // add header for columns
1814 showHideColumns.push({
1815 title: i18nService.getSafeText('gridMenu.columns'),
1819 $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; };
1821 $scope.grid.options.columnDefs.forEach( function( colDef, index ){
1822 if ( colDef.enableHiding !== false ){
1823 // add hide menu item - shows an OK icon as we only show when column is already visible
1825 icon: 'ui-grid-icon-ok',
1826 action: function($event) {
1827 $event.stopPropagation();
1828 service.toggleColumnVisibility( this.context.gridCol );
1831 return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined;
1833 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1835 order: 301 + index * 2
1837 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1838 showHideColumns.push( menuItem );
1840 // add show menu item - shows no icon as we only show when column is invisible
1842 icon: 'ui-grid-icon-cancel',
1843 action: function($event) {
1844 $event.stopPropagation();
1845 service.toggleColumnVisibility( this.context.gridCol );
1848 return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined);
1850 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1852 order: 301 + index * 2 + 1
1854 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1855 showHideColumns.push( menuItem );
1858 return showHideColumns;
1864 * @methodOf ui.grid.gridMenuService
1865 * @name setMenuItemTitle
1866 * @description Handles the response from gridMenuTitleFilter, adding it directly to the menu
1867 * item if it returns a string, otherwise waiting for the promise to resolve or reject then
1868 * putting the result into the title
1869 * @param {object} menuItem the menuItem we want to put the title on
1870 * @param {object} colDef the colDef from which we can get displayName, name or field
1871 * @param {Grid} grid the grid, from which we can get the options.gridMenuTitleFilter
1874 setMenuItemTitle: function( menuItem, colDef, grid ){
1875 var title = grid.options.gridMenuTitleFilter( colDef.displayName || gridUtil.readableColumnName(colDef.name) || colDef.field );
1877 if ( typeof(title) === 'string' ){
1878 menuItem.title = title;
1879 } else if ( title.then ){
1880 // must be a promise
1881 menuItem.title = "";
1882 title.then( function( successValue ) {
1883 menuItem.title = successValue;
1884 }, function( errorValue ) {
1885 menuItem.title = errorValue;
1888 gridUtil.logError('Expected gridMenuTitleFilter to return a string or a promise, it has returned neither, bad config');
1889 menuItem.title = 'badconfig';
1895 * @methodOf ui.grid.gridMenuService
1896 * @name toggleColumnVisibility
1897 * @description Toggles the visibility of an individual column. Expects to be
1898 * provided a context that has on it a gridColumn, which is the column that
1899 * we'll operate upon. We change the visibility, and refresh the grid as appropriate
1900 * @param {GridCol} gridCol the column that we want to toggle
1903 toggleColumnVisibility: function( gridCol ) {
1904 gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined );
1906 gridCol.grid.refresh();
1907 gridCol.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1908 gridCol.grid.api.core.raise.columnVisibilityChanged( gridCol );
1917 .directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 'i18nService',
1918 function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) {
1923 require: ['^uiGrid'],
1924 templateUrl: 'ui-grid/ui-grid-menu-button',
1927 link: function ($scope, $elm, $attrs, controllers) {
1928 var uiGridCtrl = controllers[0];
1930 // For the aria label
1932 aria: i18nService.getSafeText('gridMenu.aria')
1935 uiGridGridMenuService.initialize($scope, uiGridCtrl.grid);
1937 $scope.shown = false;
1939 $scope.toggleMenu = function () {
1940 if ( $scope.shown ){
1941 $scope.$broadcast('hide-menu');
1942 $scope.shown = false;
1944 $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope );
1945 $scope.$broadcast('show-menu');
1946 $scope.shown = true;
1950 $scope.$on('menu-hidden', function() {
1951 $scope.shown = false;
1952 gridUtil.focus.bySelector($elm, '.ui-grid-icon-container');
1965 * @name ui.grid.directive:uiGridMenu
1970 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
1973 <doc:example module="app">
1976 var app = angular.module('app', ['ui.grid']);
1978 app.controller('MainCtrl', ['$scope', function ($scope) {
1983 <div ng-controller="MainCtrl">
1984 <div ui-grid-menu shown="true" ></div>
1991 angular.module('ui.grid')
1993 .directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'i18nService',
1994 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18nService) {
2002 require: '?^uiGrid',
2003 templateUrl: 'ui-grid/uiGridMenu',
2005 link: function ($scope, $elm, $attrs, uiGridCtrl) {
2008 var gridMenuMaxHeight;
2010 $scope.dynamicStyles = '';
2013 // magic number of 30 because the grid menu displays somewhat below
2014 // the top of the grid. It is approximately 30px.
2015 gridMenuMaxHeight = uiGridCtrl.grid.gridHeight - 30;
2016 $scope.dynamicStyles = [
2017 '.grid' + uiGridCtrl.grid.id + ' .ui-grid-menu-mid {',
2018 'max-height: ' + gridMenuMaxHeight + 'px;',
2024 close: i18nService.getSafeText('columnMenu.close')
2027 // *** Show/Hide functions ******
2028 $scope.showMenu = function(event, args) {
2029 if ( !$scope.shown ){
2032 * In order to animate cleanly we remove the ng-if, wait a digest cycle, then
2033 * animate the removal of the ng-hide. We can't successfully (so far as I can tell)
2034 * animate removal of the ng-if, as the menu items aren't there yet. And we don't want
2035 * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated
2038 * Note when testing animation that animations don't run on the tutorials. When debugging it looks
2039 * like they do, but angular has a default $animate provider that is just a stub, and that's what's
2040 * being called. ALso don't be fooled by the fact that your browser has actually loaded the
2041 * angular-translate.js, it's not using it. You need to test animations in an external application.
2043 $scope.shown = true;
2045 $timeout( function() {
2046 $scope.shownMid = true;
2047 $scope.$emit('menu-shown');
2049 } else if ( !$scope.shownMid ) {
2050 // we're probably doing a hide then show, so we don't need to wait for ng-if
2051 $scope.shownMid = true;
2052 $scope.$emit('menu-shown');
2055 var docEventType = 'click';
2056 if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') {
2057 docEventType = args.originalEvent.type;
2060 // Turn off an existing document click handler
2061 angular.element(document).off('click touchstart', applyHideMenu);
2063 // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one
2064 $timeout(function() {
2065 angular.element(document).on(docEventType, applyHideMenu);
2067 //automatically set the focus to the first button element in the now open menu.
2068 gridUtil.focus.bySelector($elm, 'button[type=button]', true);
2072 $scope.hideMenu = function(event, args) {
2073 if ( $scope.shown ){
2075 * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to
2076 * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the
2077 * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason.
2079 * The user may have clicked on the menu again whilst
2080 * we're waiting, so we check that the mid isn't shown before applying the ng-if.
2082 $scope.shownMid = false;
2083 $timeout( function() {
2084 if ( !$scope.shownMid ){
2085 $scope.shown = false;
2086 $scope.$emit('menu-hidden');
2091 angular.element(document).off('click touchstart', applyHideMenu);
2094 $scope.$on('hide-menu', function (event, args) {
2095 $scope.hideMenu(event, args);
2098 $scope.$on('show-menu', function (event, args) {
2099 $scope.showMenu(event, args);
2103 // *** Auto hide when click elsewhere ******
2104 var applyHideMenu = function(){
2106 $scope.$apply(function () {
2112 if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) {
2113 $scope.autoHide = true;
2116 if ($scope.autoHide) {
2117 angular.element($window).on('resize', applyHideMenu);
2120 $scope.$on('$destroy', function () {
2121 angular.element(document).off('click touchstart', applyHideMenu);
2125 $scope.$on('$destroy', function() {
2126 angular.element($window).off('resize', applyHideMenu);
2130 $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollBegin($scope, applyHideMenu ));
2133 $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu ));
2137 controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
2145 .directive('uiGridMenuItem', ['gridUtil', '$compile', 'i18nService', function (gridUtil, $compile, i18nService) {
2146 var uiGridMenuItem = {
2157 screenReaderOnly: '='
2159 require: ['?^uiGrid', '^uiGridMenu'],
2160 templateUrl: 'ui-grid/uiGridMenuItem',
2162 compile: function($elm, $attrs) {
2164 pre: function ($scope, $elm, $attrs, controllers) {
2165 var uiGridCtrl = controllers[0],
2166 uiGridMenuCtrl = controllers[1];
2168 if ($scope.templateUrl) {
2169 gridUtil.getTemplate($scope.templateUrl)
2170 .then(function (contents) {
2171 var template = angular.element(contents);
2173 var newElm = $compile(template)($scope);
2174 $elm.replaceWith(newElm);
2178 post: function ($scope, $elm, $attrs, controllers) {
2179 var uiGridCtrl = controllers[0],
2180 uiGridMenuCtrl = controllers[1];
2182 // TODO(c0bra): validate that shown and active are functions if they're defined. An exception is already thrown above this though
2183 // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') {
2184 // throw new TypeError("$scope.shown is defined but not a function");
2186 if (typeof($scope.shown) === 'undefined' || $scope.shown === null) {
2187 $scope.shown = function() { return true; };
2190 $scope.itemShown = function () {
2192 if ($scope.context) {
2193 context.context = $scope.context;
2196 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2197 context.grid = uiGridCtrl.grid;
2200 return $scope.shown.call(context);
2203 $scope.itemAction = function($event,title) {
2204 gridUtil.logDebug('itemAction');
2205 $event.stopPropagation();
2207 if (typeof($scope.action) === 'function') {
2210 if ($scope.context) {
2211 context.context = $scope.context;
2214 // Add the grid to the function call context if the uiGrid controller is present
2215 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2216 context.grid = uiGridCtrl.grid;
2219 $scope.action.call(context, $event, title);
2221 if ( !$scope.leaveOpen ){
2222 $scope.$emit('hide-menu');
2225 * XXX: Fix after column refactor
2226 * Ideally the focus would remain on the item.
2227 * However, since there are two menu items that have their 'show' property toggled instead. This is a quick fix.
2229 gridUtil.focus.bySelector(angular.element(gridUtil.closestElm($elm, ".ui-grid-menu-items")), 'button[type=button]', true);
2234 $scope.i18n = i18nService.get();
2240 return uiGridMenuItem;
2249 * @name ui.grid.directive:uiGridOneBind
2250 * @summary A group of directives that provide a one time bind to a dom element.
2251 * @description A group of directives that provide a one time bind to a dom element.
2252 * As one time bindings are not supported in Angular 1.2.* this directive provdes this capability.
2253 * This is done to reduce the number of watchers on the dom.
2255 * <h2>Short Example ({@link ui.grid.directive:uiGridOneBindSrc ui-grid-one-bind-src})</h2>
2257 <div ng-init="imageName = 'myImageDir.jpg'">
2258 <img ui-grid-one-bind-src="imageName"></img>
2263 <div ng-init="imageName = 'myImageDir.jpg'">
2264 <img ui-grid-one-bind-src="imageName" src="myImageDir.jpg"></img>
2268 <h2>Short Example ({@link ui.grid.directive:uiGridOneBindText ui-grid-one-bind-text})</h2>
2270 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text"></div>
2274 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text">Add this text</div>
2277 * <b>Note:</b> This behavior is slightly different for the {@link ui.grid.directive:uiGridOneBindIdGrid uiGridOneBindIdGrid}
2278 * and {@link ui.grid.directive:uiGridOneBindAriaLabelledbyGrid uiGridOneBindAriaLabelledbyGrid} directives.
2281 //https://github.com/joshkurz/Black-Belt-AngularJS-Directives/blob/master/directives/Optimization/oneBind.js
2282 var oneBinders = angular.module('ui.grid');
2286 * @name ui.grid.directive:uiGridOneBindSrc
2287 * @memberof ui.grid.directive:uiGridOneBind
2290 * @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>.
2291 * @description One time binding for the src dom tag.
2294 {tag: 'Src', method: 'attr'},
2297 * @name ui.grid.directive:uiGridOneBindText
2300 * @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>.
2301 * @description One time binding for the text dom tag.
2303 {tag: 'Text', method: 'text'},
2306 * @name ui.grid.directive:uiGridOneBindHref
2309 * @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>.
2310 * @description One time binding for the href dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2312 {tag: 'Href', method: 'attr'},
2315 * @name ui.grid.directive:uiGridOneBindClass
2318 * @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>.
2319 * @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.
2320 * this is to prevent the watcher from being removed before the scope is initialized.
2321 * @param {Array} uiGridOneBindClass An array of classes to bind to this element.
2322 * @description One time binding for the class dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2324 {tag: 'Class', method: 'addClass'},
2327 * @name ui.grid.directive:uiGridOneBindHtml
2330 * @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>.
2331 * @description One time binding for the html method on a dom element. For more information see {@link ui.grid.directive:uiGridOneBind}.
2333 {tag: 'Html', method: 'html'},
2336 * @name ui.grid.directive:uiGridOneBindAlt
2339 * @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>.
2340 * @description One time binding for the alt dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2342 {tag: 'Alt', method: 'attr'},
2345 * @name ui.grid.directive:uiGridOneBindStyle
2348 * @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>.
2349 * @description One time binding for the style dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2351 {tag: 'Style', method: 'css'},
2354 * @name ui.grid.directive:uiGridOneBindValue
2357 * @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>.
2358 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2360 {tag: 'Value', method: 'attr'},
2363 * @name ui.grid.directive:uiGridOneBindId
2366 * @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>.
2367 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2369 {tag: 'Id', method: 'attr'},
2372 * @name ui.grid.directive:uiGridOneBindIdGrid
2375 * @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>.
2376 * @description One time binding for the id dom tag.
2377 * <h1>Important Note!</h1>
2378 * If the id tag passed as a parameter does <b>not</b> contain the grid id as a substring
2379 * then the directive will search the scope and the parent controller (if it is a uiGridController) for the grid.id value.
2380 * 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.
2381 * This is done in order to ensure uniqueness of id tags across the grid.
2382 * This is to prevent two grids in the same document having duplicate id tags.
2384 {tag: 'Id', directiveName:'IdGrid', method: 'attr', appendGridId: true},
2387 * @name ui.grid.directive:uiGridOneBindTitle
2390 * @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>.
2391 * @description One time binding for the title dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2393 {tag: 'Title', method: 'attr'},
2396 * @name ui.grid.directive:uiGridOneBindAriaLabel
2399 * @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>.
2400 * @description One time binding for the aria-label dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2403 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text"></div>
2407 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text" aria-label="Add this text"></div>
2410 {tag: 'Label', method: 'attr', aria:true},
2413 * @name ui.grid.directive:uiGridOneBindAriaLabelledby
2416 * @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>.
2417 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2420 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId"></div>
2424 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId" aria-labelledby="gridID32"></div>
2427 {tag: 'Labelledby', method: 'attr', aria:true},
2430 * @name ui.grid.directive:uiGridOneBindAriaLabelledbyGrid
2433 * @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>.
2434 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2435 * 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
2436 * grid id to each one.
2439 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId"></div>
2441 * Will become ([grid.id] will be replaced by the actual grid id):
2443 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId" aria-labelledby-Grid="[grid.id]-gridID32"></div>
2446 {tag: 'Labelledby', directiveName:'LabelledbyGrid', appendGridId:true, method: 'attr', aria:true},
2449 * @name ui.grid.directive:uiGridOneBindAriaDescribedby
2452 * @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>.
2453 * @description One time binding for the aria-describedby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2456 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId"></div>
2460 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId" aria-describedby="gridID32"></div>
2463 {tag: 'Describedby', method: 'attr', aria:true},
2466 * @name ui.grid.directive:uiGridOneBindAriaDescribedbyGrid
2469 * @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>.
2470 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2471 * 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
2472 * grid id to each one.
2475 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId"></div>
2477 * Will become ([grid.id] will be replaced by the actual grid id):
2479 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId" aria-describedby="[grid.id]-gridID32"></div>
2482 {tag: 'Describedby', directiveName:'DescribedbyGrid', appendGridId:true, method: 'attr', aria:true}],
2485 var baseDirectiveName = 'uiGridOneBind';
2486 //If it is an aria tag then append the aria label seperately
2487 //This is done because the aria tags are formatted aria-* and the directive name can't have a '-' character in it.
2488 //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.
2489 var directiveName = (v.aria ? baseDirectiveName + 'Aria' : baseDirectiveName) + (v.directiveName ? v.directiveName : v.tag);
2490 oneBinders.directive(directiveName, ['gridUtil', function(gridUtil){
2493 require: ['?uiGrid','?^uiGrid'],
2494 link: function(scope, iElement, iAttrs, controllers){
2495 /* Appends the grid id to the beginnig of the value. */
2496 var appendGridId = function(val){
2497 var grid; //Get an instance of the grid if its available
2498 //If its available in the scope then we don't need to try to find it elsewhere
2502 //Another possible location to try to find the grid
2503 else if (scope.col && scope.col.grid){
2504 grid = scope.col.grid;
2506 //Last ditch effort: Search through the provided controllers.
2507 else if (!controllers.some( //Go through the controllers till one has the element we need
2508 function(controller){
2509 if (controller && controller.grid) {
2510 grid = controller.grid;
2511 return true; //We've found the grid
2514 //We tried our best to find it for you
2515 gridUtil.logError("["+directiveName+"] A valid grid could not be found to bind id. Are you using this directive " +
2516 "within the correct scope? Trying to generate id: [gridID]-" + val);
2517 throw new Error("No valid grid could be found");
2521 var idRegex = new RegExp(grid.id.toString());
2522 //If the grid id hasn't been appended already in the template declaration
2523 if (!idRegex.test(val)){
2524 val = grid.id.toString() + '-' + val;
2530 // The watch returns a function to remove itself.
2531 var rmWatcher = scope.$watch(iAttrs[directiveName], function(newV){
2533 //If we are trying to add an id element then we also apply the grid id if it isn't already there
2534 if (v.appendGridId) {
2535 var newIdString = null;
2536 //Append the id to all of the new ids.
2537 angular.forEach( newV.split(' '), function(s){
2538 newIdString = (newIdString ? (newIdString + ' ') : '') + appendGridId(s);
2543 // Append this newValue to the dom element.
2545 case 'attr': //The attr method takes two paraams the tag and the value
2547 //If it is an aria element then append the aria prefix
2548 iElement[v.method]('aria-' + v.tag.toLowerCase(),newV);
2550 iElement[v.method](v.tag.toLowerCase(),newV);
2554 //Pulled from https://github.com/Pasvaz/bindonce/blob/master/bindonce.js
2555 if (angular.isObject(newV) && !angular.isArray(newV)) {
2557 var nonNullFound = false; //We don't want to remove the binding unless the key is actually defined
2558 angular.forEach(newV, function (value, index) {
2559 if (value !== null && typeof(value) !== "undefined"){
2560 nonNullFound = true; //A non null value for a key was found so the object must have been initialized
2561 if (value) {results.push(index);}
2564 //A non null value for a key wasn't found so assume that the scope values haven't been fully initialized
2566 return; // If not initialized then the watcher should not be removed yet.
2572 iElement.addClass(angular.isArray(newV) ? newV.join(' ') : newV);
2578 iElement[v.method](newV);
2582 //Removes the watcher on itself after the bind
2585 // True ensures that equality is determined using angular.equals instead of ===
2586 }, true); //End rm watchers
2587 } //End compile function
2588 }; //End directive return
2589 } // End directive function
2591 }); // End angular foreach
2597 var module = angular.module('ui.grid');
2599 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'uiGridConstants', 'gridUtil', 'ScrollEvent',
2600 function($timeout, $document, uiGridConstants, gridUtil, ScrollEvent) {
2604 templateUrl: 'ui-grid/uiGridRenderContainer',
2605 require: ['^uiGrid', 'uiGridRenderContainer'],
2608 rowContainerName: '=',
2609 colContainerName: '=',
2610 bindScrollHorizontal: '=',
2611 bindScrollVertical: '=',
2612 enableVerticalScrollbar: '=',
2613 enableHorizontalScrollbar: '='
2615 controller: 'uiGridRenderContainer as RenderContainer',
2616 compile: function () {
2618 pre: function prelink($scope, $elm, $attrs, controllers) {
2620 var uiGridCtrl = controllers[0];
2621 var containerCtrl = controllers[1];
2622 var grid = $scope.grid = uiGridCtrl.grid;
2624 // Verify that the render container for this element exists
2625 if (!$scope.rowContainerName) {
2626 throw "No row render container name specified";
2628 if (!$scope.colContainerName) {
2629 throw "No column render container name specified";
2632 if (!grid.renderContainers[$scope.rowContainerName]) {
2633 throw "Row render container '" + $scope.rowContainerName + "' is not registered.";
2635 if (!grid.renderContainers[$scope.colContainerName]) {
2636 throw "Column render container '" + $scope.colContainerName + "' is not registered.";
2639 var rowContainer = $scope.rowContainer = grid.renderContainers[$scope.rowContainerName];
2640 var colContainer = $scope.colContainer = grid.renderContainers[$scope.colContainerName];
2642 containerCtrl.containerId = $scope.containerId;
2643 containerCtrl.rowContainer = rowContainer;
2644 containerCtrl.colContainer = colContainer;
2646 post: function postlink($scope, $elm, $attrs, controllers) {
2648 var uiGridCtrl = controllers[0];
2649 var containerCtrl = controllers[1];
2651 var grid = uiGridCtrl.grid;
2652 var rowContainer = containerCtrl.rowContainer;
2653 var colContainer = containerCtrl.colContainer;
2654 var scrollTop = null;
2655 var scrollLeft = null;
2658 var renderContainer = grid.renderContainers[$scope.containerId];
2660 // Put the container name on this element as a class
2661 $elm.addClass('ui-grid-render-container-' + $scope.containerId);
2663 // Scroll the render container viewport when the mousewheel is used
2664 gridUtil.on.mousewheel($elm, function (event) {
2665 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.RenderContainerMouseWheel);
2666 if (event.deltaY !== 0) {
2667 var scrollYAmount = event.deltaY * -1 * event.deltaFactor;
2669 scrollTop = containerCtrl.viewport[0].scrollTop;
2671 // Get the scroll percentage
2672 scrollEvent.verticalScrollLength = rowContainer.getVerticalScrollLength();
2673 var scrollYPercentage = (scrollTop + scrollYAmount) / scrollEvent.verticalScrollLength;
2675 // If we should be scrolled 100%, make sure the scrollTop matches the maximum scroll length
2676 // Viewports that have "overflow: hidden" don't let the mousewheel scroll all the way to the bottom without this check
2677 if (scrollYPercentage >= 1 && scrollTop < scrollEvent.verticalScrollLength) {
2678 containerCtrl.viewport[0].scrollTop = scrollEvent.verticalScrollLength;
2681 // Keep scrollPercentage within the range 0-1.
2682 if (scrollYPercentage < 0) { scrollYPercentage = 0; }
2683 else if (scrollYPercentage > 1) { scrollYPercentage = 1; }
2685 scrollEvent.y = { percentage: scrollYPercentage, pixels: scrollYAmount };
2687 if (event.deltaX !== 0) {
2688 var scrollXAmount = event.deltaX * event.deltaFactor;
2690 // Get the scroll percentage
2691 scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport, grid);
2692 scrollEvent.horizontalScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
2693 var scrollXPercentage = (scrollLeft + scrollXAmount) / scrollEvent.horizontalScrollLength;
2695 // Keep scrollPercentage within the range 0-1.
2696 if (scrollXPercentage < 0) { scrollXPercentage = 0; }
2697 else if (scrollXPercentage > 1) { scrollXPercentage = 1; }
2699 scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount };
2702 // Let the parent container scroll if the grid is already at the top/bottom
2703 if ((event.deltaY !== 0 && (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop))) ||
2704 (event.deltaX !== 0 && (scrollEvent.atLeft(scrollLeft) || scrollEvent.atRight(scrollLeft)))) {
2705 //parent controller scrolls
2708 event.preventDefault();
2709 event.stopPropagation();
2710 scrollEvent.fireThrottledScrollingEvent('', scrollEvent);
2715 $elm.bind('$destroy', function() {
2716 $elm.unbind('keydown');
2718 ['touchstart', 'touchmove', 'touchend','keydown', 'wheel', 'mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'].forEach(function (eventName) {
2719 $elm.unbind(eventName);
2723 // TODO(c0bra): Handle resizing the inner canvas based on the number of elements
2727 var canvasWidth = colContainer.canvasWidth;
2728 var viewportWidth = colContainer.getViewportWidth();
2730 var canvasHeight = rowContainer.getCanvasHeight();
2732 //add additional height for scrollbar on left and right container
2733 //if ($scope.containerId !== 'body') {
2734 // canvasHeight -= grid.scrollbarHeight;
2737 var viewportHeight = rowContainer.getViewportHeight();
2738 //shorten the height to make room for a scrollbar placeholder
2739 if (colContainer.needsHScrollbarPlaceholder()) {
2740 viewportHeight -= grid.scrollbarHeight;
2743 var headerViewportWidth,
2744 footerViewportWidth;
2745 headerViewportWidth = footerViewportWidth = colContainer.getHeaderViewportWidth();
2747 // Set canvas dimensions
2748 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }';
2750 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2752 if (renderContainer.explicitHeaderCanvasHeight) {
2753 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }';
2756 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: inherit; }';
2759 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }';
2760 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }';
2762 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2763 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-viewport { width: ' + footerViewportWidth + 'px; }';
2768 uiGridCtrl.grid.registerStyleComputation({
2779 module.controller('uiGridRenderContainer', ['$scope', 'gridUtil', function ($scope, gridUtil) {
2788 angular.module('ui.grid').directive('uiGridRow', ['gridUtil', function(gridUtil) {
2792 // templateUrl: 'ui-grid/ui-grid-row',
2793 require: ['^uiGrid', '^uiGridRenderContainer'],
2796 //rowRenderIndex is added to scope to give the true visual index of the row to any directives that need it
2799 compile: function() {
2801 pre: function($scope, $elm, $attrs, controllers) {
2802 var uiGridCtrl = controllers[0];
2803 var containerCtrl = controllers[1];
2805 var grid = uiGridCtrl.grid;
2807 $scope.grid = uiGridCtrl.grid;
2808 $scope.colContainer = containerCtrl.colContainer;
2810 // Function for attaching the template to this scope
2811 var clonedElement, cloneScope;
2812 function compileTemplate() {
2813 $scope.row.getRowTemplateFn.then(function (compiledElementFn) {
2814 // var compiledElementFn = $scope.row.compiledElementFn;
2816 // Create a new scope for the contents of this row, so we can destroy it later if need be
2817 var newScope = $scope.$new();
2819 compiledElementFn(newScope, function (newElm, scope) {
2820 // If we already have a cloned element, we need to remove it and destroy its scope
2821 if (clonedElement) {
2822 clonedElement.remove();
2823 cloneScope.$destroy();
2826 // Empty the row and append the new element
2827 $elm.empty().append(newElm);
2829 // Save the new cloned element and scope
2830 clonedElement = newElm;
2831 cloneScope = newScope;
2836 // Initially attach the compiled template to this scope
2839 // If the row's compiled element function changes, we need to replace this element's contents with the new compiled template
2840 $scope.$watch('row.getRowTemplateFn', function (newFunc, oldFunc) {
2841 if (newFunc !== oldFunc) {
2846 post: function($scope, $elm, $attrs, controllers) {
2860 * @name ui.grid.directive:uiGridStyle
2865 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
2868 <doc:example module="app">
2871 var app = angular.module('app', ['ui.grid']);
2873 app.controller('MainCtrl', ['$scope', function ($scope) {
2874 $scope.myStyle = '.blah { border: 1px solid }';
2878 <div ng-controller="MainCtrl">
2879 <style ui-grid-style>{{ myStyle }}</style>
2880 <span class="blah">I am in a box.</span>
2884 it('should apply the right class to the element', function () {
2885 element(by.css('.blah')).getCssValue('border-top-width')
2887 expect(c).toContain('1px');
2895 angular.module('ui.grid').directive('uiGridStyle', ['gridUtil', '$interpolate', function(gridUtil, $interpolate) {
2899 // require: '?^uiGrid',
2900 link: function($scope, $elm, $attrs, uiGridCtrl) {
2901 // gridUtil.logDebug('ui-grid-style link');
2902 // if (uiGridCtrl === undefined) {
2903 // gridUtil.logWarn('[ui-grid-style link] uiGridCtrl is undefined!');
2906 var interpolateFn = $interpolate($elm.text(), true);
2908 if (interpolateFn) {
2909 $scope.$watch(interpolateFn, function(value) {
2914 // uiGridCtrl.recalcRowStyles = function() {
2915 // var offset = (scope.options.offsetTop || 0) - (scope.options.excessRows * scope.options.rowHeight);
2916 // var rowHeight = scope.options.rowHeight;
2919 // var rowStyleCount = uiGridCtrl.minRowsToRender() + (scope.options.excessRows * 2);
2920 // for (var i = 1; i <= rowStyleCount; i++) {
2921 // ret = ret + ' .grid' + scope.gridId + ' .ui-grid-row:nth-child(' + i + ') { top: ' + offset + 'px; }';
2922 // offset = offset + rowHeight;
2925 // scope.rowStyles = ret;
2928 // uiGridCtrl.styleComputions.push(uiGridCtrl.recalcRowStyles);
2939 angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', '$log',
2940 function(gridUtil, ScrollEvent, uiGridConstants, $log) {
2944 controllerAs: 'Viewport',
2945 templateUrl: 'ui-grid/uiGridViewport',
2946 require: ['^uiGrid', '^uiGridRenderContainer'],
2947 link: function($scope, $elm, $attrs, controllers) {
2948 // gridUtil.logDebug('viewport post-link');
2950 var uiGridCtrl = controllers[0];
2951 var containerCtrl = controllers[1];
2953 $scope.containerCtrl = containerCtrl;
2955 var rowContainer = containerCtrl.rowContainer;
2956 var colContainer = containerCtrl.colContainer;
2958 var grid = uiGridCtrl.grid;
2960 $scope.grid = uiGridCtrl.grid;
2962 // Put the containers in scope so we can get rows and columns from them
2963 $scope.rowContainer = containerCtrl.rowContainer;
2964 $scope.colContainer = containerCtrl.colContainer;
2966 // Register this viewport with its container
2967 containerCtrl.viewport = $elm;
2970 $elm.on('scroll', scrollHandler);
2972 var ignoreScroll = false;
2974 function scrollHandler(evt) {
2975 //Leaving in this commented code in case it can someday be used
2976 //It does improve performance, but because the horizontal scroll is normalized,
2977 // using this code will lead to the column header getting slightly out of line with columns
2979 //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) {
2980 // //don't ask for scrollTop if we just set it
2981 // ignoreScroll = false;
2984 //ignoreScroll = true;
2986 var newScrollTop = $elm[0].scrollTop;
2987 var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid);
2989 var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop);
2990 var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft);
2992 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll);
2993 scrollEvent.newScrollLeft = newScrollLeft;
2994 scrollEvent.newScrollTop = newScrollTop;
2995 if ( horizScrollPercentage > -1 ){
2996 scrollEvent.x = { percentage: horizScrollPercentage };
2999 if ( vertScrollPercentage > -1 ){
3000 scrollEvent.y = { percentage: vertScrollPercentage };
3003 grid.scrollContainers($scope.$parent.containerId, scrollEvent);
3006 if ($scope.$parent.bindScrollVertical) {
3007 grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll);
3010 if ($scope.$parent.bindScrollHorizontal) {
3011 grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll);
3012 grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader);
3013 grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter);
3016 function syncVerticalScroll(scrollEvent){
3017 containerCtrl.prevScrollArgs = scrollEvent;
3018 var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport);
3019 $elm[0].scrollTop = newScrollTop;
3023 function syncHorizontalScroll(scrollEvent){
3024 containerCtrl.prevScrollArgs = scrollEvent;
3025 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3026 $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3029 function syncHorizontalHeader(scrollEvent){
3030 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3031 if (containerCtrl.headerViewport) {
3032 containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3036 function syncHorizontalFooter(scrollEvent){
3037 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3038 if (containerCtrl.footerViewport) {
3039 containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3045 controller: ['$scope', function ($scope) {
3046 this.rowStyle = function (index) {
3047 var rowContainer = $scope.rowContainer;
3048 var colContainer = $scope.colContainer;
3052 if (index === 0 && rowContainer.currentTopRow !== 0) {
3053 // The row offset-top is just the height of the rows above the current top-most row, which are no longer rendered
3054 var hiddenRowWidth = (rowContainer.currentTopRow) * rowContainer.grid.options.rowHeight;
3056 // return { 'margin-top': hiddenRowWidth + 'px' };
3057 styles['margin-top'] = hiddenRowWidth + 'px';
3060 if (colContainer.currentFirstColumn !== 0) {
3061 if (colContainer.grid.isRTL()) {
3062 styles['margin-right'] = colContainer.columnOffset + 'px';
3065 styles['margin-left'] = colContainer.columnOffset + 'px';
3080 angular.module('ui.grid')
3081 .directive('uiGridVisible', function uiGridVisibleAction() {
3082 return function ($scope, $elm, $attr) {
3083 $scope.$watch($attr.uiGridVisible, function (visible) {
3084 // $elm.css('visibility', visible ? 'visible' : 'hidden');
3085 $elm[visible ? 'removeClass' : 'addClass']('ui-grid-invisible');
3094 angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants',
3095 '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile',
3096 function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants,
3097 $templateCache, gridClassFactory, $timeout, $parse, $compile) {
3098 // gridUtil.logDebug('ui-grid controller');
3102 self.grid = gridClassFactory.createGrid($scope.uiGrid);
3104 //assign $scope.$parent if appScope not already assigned
3105 self.grid.appScope = self.grid.appScope || $scope.$parent;
3107 $elm.addClass('grid' + self.grid.id);
3108 self.grid.rtl = gridUtil.getStyles($elm[0])['direction'] === 'rtl';
3111 // angular.extend(self.grid.options, );
3113 //all properties of grid are available on scope
3114 $scope.grid = self.grid;
3116 if ($attrs.uiGridColumns) {
3117 $attrs.$observe('uiGridColumns', function(value) {
3118 self.grid.options.columnDefs = value;
3119 self.grid.buildColumns()
3121 self.grid.preCompileCellTemplates();
3123 self.grid.refreshCanvas(true);
3129 // if fastWatch is set we watch only the length and the reference, not every individual object
3130 var deregFunctions = [];
3131 if (self.grid.options.fastWatch) {
3132 self.uiGrid = $scope.uiGrid;
3133 if (angular.isString($scope.uiGrid.data)) {
3134 deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) );
3135 deregFunctions.push( $scope.$parent.$watch(function() {
3136 if ( self.grid.appScope[$scope.uiGrid.data] ){
3137 return self.grid.appScope[$scope.uiGrid.data].length;
3141 }, dataWatchFunction) );
3143 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3144 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, dataWatchFunction) );
3146 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3147 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, columnDefsWatchFunction) );
3149 if (angular.isString($scope.uiGrid.data)) {
3150 deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) );
3152 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3154 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3158 function columnDefsWatchFunction(n, o) {
3160 self.grid.options.columnDefs = n;
3161 self.grid.buildColumns({ orderByColumnDefs: true })
3164 self.grid.preCompileCellTemplates();
3166 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.COLUMN);
3173 function dataWatchFunction(newData) {
3174 // gridUtil.logDebug('dataWatch fired');
3177 if ( self.grid.options.fastWatch ){
3178 if (angular.isString($scope.uiGrid.data)) {
3179 newData = self.grid.appScope[$scope.uiGrid.data];
3181 newData = $scope.uiGrid.data;
3185 mostRecentData = newData;
3188 // columns length is greater than the number of row header columns, which don't count because they're created automatically
3189 var hasColumns = self.grid.columns.length > (self.grid.rowHeaderColumns ? self.grid.rowHeaderColumns.length : 0);
3192 // If we have no columns
3194 // ... and we don't have a ui-grid-columns attribute, which would define columns for us
3195 !$attrs.uiGridColumns &&
3196 // ... and we have no pre-defined columns
3197 self.grid.options.columnDefs.length === 0 &&
3198 // ... but we DO have data
3201 // ... then build the column definitions from the data that we have
3202 self.grid.buildColumnDefsFromData(newData);
3205 // If we haven't built columns before and either have some columns defined or some data defined
3206 if (!hasColumns && (self.grid.options.columnDefs.length > 0 || newData.length > 0)) {
3207 // Build the column set, then pre-compile the column cell templates
3208 promises.push(self.grid.buildColumns()
3210 self.grid.preCompileCellTemplates();
3214 $q.all(promises).then(function() {
3215 // use most recent data, rather than the potentially outdated data passed into watcher handler
3216 self.grid.modifyRows(mostRecentData)
3218 // if (self.viewport) {
3219 self.grid.redrawInPlace(true);
3222 $scope.$evalAsync(function() {
3223 self.grid.refreshCanvas(true);
3224 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW);
3231 var styleWatchDereg = $scope.$watch(function () { return self.grid.styleComputations; }, function() {
3232 self.grid.refreshCanvas(true);
3235 $scope.$on('$destroy', function() {
3236 deregFunctions.forEach( function( deregFn ){ deregFn(); });
3240 self.fireEvent = function(eventName, args) {
3241 // Add the grid to the event arguments if it's not there
3242 if (typeof(args) === 'undefined' || args === undefined) {
3246 if (typeof(args.grid) === 'undefined' || args.grid === undefined) {
3247 args.grid = self.grid;
3250 $scope.$broadcast(eventName, args);
3253 self.innerCompile = function innerCompile(elm) {
3254 $compile(elm)($scope);
3261 * @name ui.grid.directive:uiGrid
3264 * @param {Object} uiGrid Options for the grid to use
3266 * @description Create a very basic grid.
3269 <example module="app">
3270 <file name="app.js">
3271 var app = angular.module('app', ['ui.grid']);
3273 app.controller('MainCtrl', ['$scope', function ($scope) {
3275 { name: 'Bob', title: 'CEO' },
3276 { name: 'Frank', title: 'Lowly Developer' }
3280 <file name="index.html">
3281 <div ng-controller="MainCtrl">
3282 <div ui-grid="{ data: data }"></div>
3287 angular.module('ui.grid').directive('uiGrid', uiGridDirective);
3289 uiGridDirective.$inject = ['$compile', '$templateCache', '$timeout', '$window', 'gridUtil', 'uiGridConstants'];
3290 function uiGridDirective($compile, $templateCache, $timeout, $window, gridUtil, uiGridConstants) {
3292 templateUrl: 'ui-grid/ui-grid',
3298 controller: 'uiGridController',
3299 compile: function () {
3301 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3302 var grid = uiGridCtrl.grid;
3303 // Initialize scrollbars (TODO: move to controller??)
3304 uiGridCtrl.scrollbars = [];
3305 grid.element = $elm;
3308 // See if the grid has a rendered width, if not, wait a bit and try again
3309 var sizeCheckInterval = 100; // ms
3310 var maxSizeChecks = 20; // 2 seconds total
3313 // Setup (event listeners) the grid
3316 // And initialize it
3319 // Mark rendering complete so API events can happen
3320 grid.renderingComplete();
3322 // If the grid doesn't have size currently, wait for a bit to see if it gets size
3327 function checkSize() {
3328 // If the grid has no width and we haven't checked more than <maxSizeChecks> times, check again in <sizeCheckInterval> milliseconds
3329 if ($elm[0].offsetWidth <= 0 && sizeChecks < maxSizeChecks) {
3330 setTimeout(checkSize, sizeCheckInterval);
3338 // Setup event listeners and watchers
3340 // Bind to window resize events
3341 angular.element($window).on('resize', gridResize);
3343 // Unbind from window resize events when the grid is destroyed
3344 $elm.on('$destroy', function () {
3345 angular.element($window).off('resize', gridResize);
3348 // If we add a left container after render, we need to watch and react
3349 $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) {
3350 if (newValue === oldValue) {
3353 grid.refreshCanvas(true);
3356 // If we add a right container after render, we need to watch and react
3357 $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) {
3358 if (newValue === oldValue) {
3361 grid.refreshCanvas(true);
3365 // Initialize the directive
3367 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3369 // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from
3370 grid.canvasWidth = uiGridCtrl.grid.gridWidth;
3372 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3374 // 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
3375 if (grid.gridHeight <= grid.options.rowHeight && grid.options.enableMinHeightCheck) {
3379 // Run initial canvas refresh
3380 grid.refreshCanvas(true);
3383 // Set the grid's height ourselves in the case that its height would be unusably small
3384 function autoAdjustHeight() {
3385 // Figure out the new height
3386 var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight;
3387 var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0;
3388 var footerHeight = grid.calcFooterHeight();
3390 var scrollbarHeight = 0;
3391 if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3392 scrollbarHeight = gridUtil.getScrollbarWidth();
3395 var maxNumberOfFilters = 0;
3396 // Calculates the maximum number of filters in the columns
3397 angular.forEach(grid.options.columnDefs, function(col) {
3398 if (col.hasOwnProperty('filter')) {
3399 if (maxNumberOfFilters < 1) {
3400 maxNumberOfFilters = 1;
3403 else if (col.hasOwnProperty('filters')) {
3404 if (maxNumberOfFilters < col.filters.length) {
3405 maxNumberOfFilters = col.filters.length;
3410 if (grid.options.enableFiltering) {
3411 var allColumnsHaveFilteringTurnedOff = grid.options.columnDefs.every(function(col) {
3412 return col.enableFiltering === false;
3415 if (!allColumnsHaveFilteringTurnedOff) {
3416 maxNumberOfFilters++;
3420 var filterHeight = maxNumberOfFilters * headerHeight;
3422 var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight;
3424 $elm.css('height', newHeight + 'px');
3426 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3429 // Resize the grid on window resize events
3430 function gridResize($event) {
3431 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3432 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3434 grid.refreshCanvas(true);
3447 // TODO: rename this file to ui-grid-pinned-container.js
3449 angular.module('ui.grid').directive('uiGridPinnedContainer', ['gridUtil', function (gridUtil) {
3453 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>',
3455 side: '=uiGridPinnedContainer'
3458 compile: function compile() {
3460 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3461 // gridUtil.logDebug('ui-grid-pinned-container ' + $scope.side + ' link');
3463 var grid = uiGridCtrl.grid;
3467 $elm.addClass('ui-grid-pinned-container-' + $scope.side);
3469 // Monkey-patch the viewport width function
3470 if ($scope.side === 'left' || $scope.side === 'right') {
3471 grid.renderContainers[$scope.side].getViewportWidth = monkeyPatchedGetViewportWidth;
3474 function monkeyPatchedGetViewportWidth() {
3475 /*jshint validthis: true */
3478 var viewportWidth = 0;
3479 self.visibleColumnCache.forEach(function (column) {
3480 viewportWidth += column.drawnWidth;
3483 var adjustment = self.getViewportAdjustment();
3485 viewportWidth = viewportWidth + adjustment.width;
3487 return viewportWidth;
3490 function updateContainerWidth() {
3491 if ($scope.side === 'left' || $scope.side === 'right') {
3492 var cols = grid.renderContainers[$scope.side].visibleColumnCache;
3494 for (var i = 0; i < cols.length; i++) {
3496 width += col.drawnWidth || col.width || 0;
3503 function updateContainerDimensions() {
3506 // Column containers
3507 if ($scope.side === 'left' || $scope.side === 'right') {
3508 myWidth = updateContainerWidth();
3510 // gridUtil.logDebug('myWidth', myWidth);
3512 // TODO(c0bra): Subtract sum of col widths from grid viewport width and update it
3513 $elm.attr('style', null);
3515 // var myHeight = grid.renderContainers.body.getViewportHeight(); // + grid.horizontalScrollbarHeight;
3517 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; } ';
3523 grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
3524 myWidth = updateContainerWidth();
3526 // Subtract our own width
3527 adjustment.width -= myWidth;
3528 adjustment.side = $scope.side;
3533 // Register style computation to adjust for columns in `side`'s render container
3534 grid.registerStyleComputation({
3536 func: updateContainerDimensions
3547 angular.module('ui.grid')
3548 .factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout','ScrollEvent',
3549 function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout, ScrollEvent) {
3553 * @name ui.grid.core.api:PublicApi
3554 * @description Public Api for the core grid features
3560 * @name ui.grid.class:Grid
3561 * @description Grid is the main viewModel. Any properties or methods needed to maintain state are defined in
3562 * this prototype. One instance of Grid is created per Grid directive instance.
3563 * @param {object} options Object map of options to pass into the grid. An 'id' property is expected.
3565 var Grid = function Grid(options) {
3567 // Get the id out of the options, then remove it
3568 if (options !== undefined && typeof(options.id) !== 'undefined' && options.id) {
3569 if (!/^[_a-zA-Z0-9-]+$/.test(options.id)) {
3570 throw new Error("Grid id '" + options.id + '" is invalid. It must follow CSS selector syntax rules.');
3574 throw new Error('No ID provided. An ID must be given when creating a grid.');
3577 self.id = options.id;
3580 // Get default options
3581 self.options = GridOptions.initialize( options );
3586 * @propertyOf ui.grid.class:Grid
3587 * @description reference to the application scope (the parent scope of the ui-grid element). Assigned in ui-grid controller
3589 * use gridOptions.appScopeProvider to override the default assignment of $scope.$parent with any reference
3591 self.appScope = self.options.appScopeProvider;
3593 self.headerHeight = self.options.headerRowHeight;
3598 * @name footerHeight
3599 * @propertyOf ui.grid.class:Grid
3600 * @description returns the total footer height gridFooter + columnFooter
3602 self.footerHeight = self.calcFooterHeight();
3607 * @name columnFooterHeight
3608 * @propertyOf ui.grid.class:Grid
3609 * @description returns the total column footer height
3611 self.columnFooterHeight = self.calcColumnFooterHeight();
3614 self.gridHeight = 0;
3616 self.columnBuilders = [];
3617 self.rowBuilders = [];
3618 self.rowsProcessors = [];
3619 self.columnsProcessors = [];
3620 self.styleComputations = [];
3621 self.viewportAdjusters = [];
3622 self.rowHeaderColumns = [];
3623 self.dataChangeCallbacks = {};
3624 self.verticalScrollSyncCallBackFns = {};
3625 self.horizontalScrollSyncCallBackFns = {};
3627 // self.visibleRowCache = [];
3629 // Set of 'render' containers for self grid, which can render sets of rows
3630 self.renderContainers = {};
3633 self.renderContainers.body = new GridRenderContainer('body', self);
3635 self.cellValueGetterCache = {};
3637 // Cached function to use with custom row templates
3638 self.getRowTemplateFn = null;
3641 //representation of the rows on the grid.
3642 //these are wrapped references to the actual data rows (options.data)
3645 //represents the columns on the grid
3650 * @name isScrollingVertically
3651 * @propertyOf ui.grid.class:Grid
3652 * @description set to true when Grid is scrolling vertically. Set to false via debounced method
3654 self.isScrollingVertically = false;
3658 * @name isScrollingHorizontally
3659 * @propertyOf ui.grid.class:Grid
3660 * @description set to true when Grid is scrolling horizontally. Set to false via debounced method
3662 self.isScrollingHorizontally = false;
3666 * @name scrollDirection
3667 * @propertyOf ui.grid.class:Grid
3668 * @description set one of the uiGridConstants.scrollDirection values (UP, DOWN, LEFT, RIGHT, NONE), which tells
3669 * us which direction we are scrolling. Set to NONE via debounced method
3671 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3673 //if true, grid will not respond to any scroll events
3674 self.disableScrolling = false;
3677 function vertical (scrollEvent) {
3678 self.isScrollingVertically = false;
3679 self.api.core.raise.scrollEnd(scrollEvent);
3680 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3683 var debouncedVertical = gridUtil.debounce(vertical, self.options.scrollDebounce);
3684 var debouncedVerticalMinDelay = gridUtil.debounce(vertical, 0);
3686 function horizontal (scrollEvent) {
3687 self.isScrollingHorizontally = false;
3688 self.api.core.raise.scrollEnd(scrollEvent);
3689 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3692 var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce);
3693 var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0);
3698 * @name flagScrollingVertically
3699 * @methodOf ui.grid.class:Grid
3700 * @description sets isScrollingVertically to true and sets it to false in a debounced function
3702 self.flagScrollingVertically = function(scrollEvent) {
3703 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3704 self.api.core.raise.scrollBegin(scrollEvent);
3706 self.isScrollingVertically = true;
3707 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3708 debouncedVerticalMinDelay(scrollEvent);
3711 debouncedVertical(scrollEvent);
3717 * @name flagScrollingHorizontally
3718 * @methodOf ui.grid.class:Grid
3719 * @description sets isScrollingHorizontally to true and sets it to false in a debounced function
3721 self.flagScrollingHorizontally = function(scrollEvent) {
3722 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3723 self.api.core.raise.scrollBegin(scrollEvent);
3725 self.isScrollingHorizontally = true;
3726 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3727 debouncedHorizontalMinDelay(scrollEvent);
3730 debouncedHorizontal(scrollEvent);
3734 self.scrollbarHeight = 0;
3735 self.scrollbarWidth = 0;
3736 if (self.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3737 self.scrollbarHeight = gridUtil.getScrollbarWidth();
3740 if (self.options.enableVerticalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3741 self.scrollbarWidth = gridUtil.getScrollbarWidth();
3746 self.api = new GridApi(self);
3751 * @methodOf ui.grid.core.api:PublicApi
3752 * @description Refresh the rendered grid on screen.
3753 * The refresh method re-runs both the columnProcessors and the
3754 * rowProcessors, as well as calling refreshCanvas to update all
3755 * the grid sizing. In general you should prefer to use queueGridRefresh
3756 * instead, which is basically a debounced version of refresh.
3758 * If you only want to resize the grid, not regenerate all the rows
3759 * and columns, you should consider directly calling refreshCanvas instead.
3762 self.api.registerMethod( 'core', 'refresh', this.refresh );
3766 * @name queueGridRefresh
3767 * @methodOf ui.grid.core.api:PublicApi
3768 * @description Request a refresh of the rendered grid on screen, if multiple
3769 * calls to queueGridRefresh are made within a digest cycle only one will execute.
3770 * The refresh method re-runs both the columnProcessors and the
3771 * rowProcessors, as well as calling refreshCanvas to update all
3772 * the grid sizing. In general you should prefer to use queueGridRefresh
3773 * instead, which is basically a debounced version of refresh.
3776 self.api.registerMethod( 'core', 'queueGridRefresh', this.queueGridRefresh );
3781 * @methodOf ui.grid.core.api:PublicApi
3782 * @description Runs only the rowProcessors, columns remain as they were.
3783 * It then calls redrawInPlace and refreshCanvas, which adjust the grid sizing.
3784 * @returns {promise} promise that is resolved when render completes?
3787 self.api.registerMethod( 'core', 'refreshRows', this.refreshRows );
3791 * @name queueRefresh
3792 * @methodOf ui.grid.core.api:PublicApi
3793 * @description Requests execution of refreshCanvas, if multiple requests are made
3794 * during a digest cycle only one will run. RefreshCanvas updates the grid sizing.
3795 * @returns {promise} promise that is resolved when render completes?
3798 self.api.registerMethod( 'core', 'queueRefresh', this.queueRefresh );
3802 * @name handleWindowResize
3803 * @methodOf ui.grid.core.api:PublicApi
3804 * @description Trigger a grid resize, normally this would be picked
3805 * up by a watch on window size, but in some circumstances it is necessary
3806 * to call this manually
3807 * @returns {promise} promise that is resolved when render completes?
3810 self.api.registerMethod( 'core', 'handleWindowResize', this.handleWindowResize );
3815 * @name addRowHeaderColumn
3816 * @methodOf ui.grid.core.api:PublicApi
3817 * @description adds a row header column to the grid
3818 * @param {object} column def
3821 self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn );
3825 * @name scrollToIfNecessary
3826 * @methodOf ui.grid.core.api:PublicApi
3827 * @description Scrolls the grid to make a certain row and column combo visible,
3828 * in the case that it is not completely visible on the screen already.
3829 * @param {GridRow} gridRow row to make visible
3830 * @param {GridCol} gridCol column to make visible
3831 * @returns {promise} a promise that is resolved when scrolling is complete
3834 self.api.registerMethod( 'core', 'scrollToIfNecessary', function(gridRow, gridCol) { return self.scrollToIfNecessary(gridRow, gridCol);} );
3839 * @methodOf ui.grid.core.api:PublicApi
3840 * @description Scroll the grid such that the specified
3841 * row and column is in view
3842 * @param {object} rowEntity gridOptions.data[] array instance to make visible
3843 * @param {object} colDef to make visible
3844 * @returns {promise} a promise that is resolved after any scrolling is finished
3846 self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} );
3850 * @name registerRowsProcessor
3851 * @methodOf ui.grid.core.api:PublicApi
3853 * Register a "rows processor" function. When the rows are updated,
3854 * the grid calls each registered "rows processor", which has a chance
3855 * to alter the set of rows (sorting, etc) as long as the count is not
3858 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
3859 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3860 * return the updated rows list, which is passed to the next processor in the chain
3861 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3862 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
3864 * At present allRowsVisible is running at 50, sort manipulations running at 60-65, filter is running at 100,
3865 * sort is at 200, grouping and treeview at 400-410, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
3867 self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor );
3871 * @name registerColumnsProcessor
3872 * @methodOf ui.grid.core.api:PublicApi
3874 * Register a "columns processor" function. When the columns are updated,
3875 * the grid calls each registered "columns processor", which has a chance
3876 * to alter the set of columns as long as the count is not
3879 * @param {function(renderedColumnsToProcess, rows )} processorFunction columns processor function, which
3880 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3881 * return the updated columns list, which is passed to the next processor in the chain
3882 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3883 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
3885 * 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)
3887 self.api.registerMethod( 'core', 'registerColumnsProcessor', this.registerColumnsProcessor );
3893 * @name sortHandleNulls
3894 * @methodOf ui.grid.core.api:PublicApi
3895 * @description A null handling method that can be used when building custom sort
3899 * mySortFn = function(a, b) {
3900 * var nulls = $scope.gridApi.core.sortHandleNulls(a, b);
3901 * if ( nulls !== null ){
3904 * // your code for sorting here
3907 * @param {object} a sort value a
3908 * @param {object} b sort value b
3909 * @returns {number} null if there were no nulls/undefineds, otherwise returns
3910 * a sort value that should be passed back from the sort function
3913 self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls );
3919 * @methodOf ui.grid.core.api:PublicApi
3920 * @description The sort criteria on one or more columns has
3921 * changed. Provides as parameters the grid and the output of
3922 * getColumnSorting, which is an array of gridColumns
3923 * that have sorting on them, sorted in priority order.
3925 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3926 * @param {Function} callBack Will be called when the event is emited. The function passes back an array of columns with
3927 * sorts on them, in priority order.
3931 * gridApi.core.on.sortChanged( $scope, function(sortColumns){
3936 self.api.registerEvent( 'core', 'sortChanged' );
3940 * @name columnVisibilityChanged
3941 * @methodOf ui.grid.core.api:PublicApi
3942 * @description The visibility of a column has changed,
3943 * the column itself is passed out as a parameter of the event.
3945 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3946 * @param {Function} callBack Will be called when the event is emited. The function passes back the GridCol that has changed.
3950 * gridApi.core.on.columnVisibilityChanged( $scope, function (column) {
3955 self.api.registerEvent( 'core', 'columnVisibilityChanged' );
3959 * @name notifyDataChange
3960 * @methodOf ui.grid.core.api:PublicApi
3961 * @description Notify the grid that a data or config change has occurred,
3962 * where that change isn't something the grid was otherwise noticing. This
3963 * might be particularly relevant where you've changed values within the data
3964 * and you'd like cell classes to be re-evaluated, or changed config within
3965 * the columnDef and you'd like headerCellClasses to be re-evaluated.
3966 * @param {string} type one of the
3967 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN), which tells
3968 * us which refreshes to fire.
3971 self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange );
3975 * @name clearAllFilters
3976 * @methodOf ui.grid.core.api:PublicApi
3977 * @description Clears all filters and optionally refreshes the visible rows.
3978 * @param {object} refreshRows Defaults to true.
3979 * @param {object} clearConditions Defaults to false.
3980 * @param {object} clearFlags Defaults to false.
3981 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
3983 self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters);
3985 self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]);
3986 self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]);
3987 self.registerDataChangeCallback( self.updateFooterHeightCallback, [uiGridConstants.dataChange.OPTIONS]);
3989 self.registerStyleComputation({
3991 func: self.getFooterStyles
3995 Grid.prototype.calcFooterHeight = function () {
3996 if (!this.hasFooter()) {
4001 if (this.options.showGridFooter) {
4002 height += this.options.gridFooterHeight;
4005 height += this.calcColumnFooterHeight();
4010 Grid.prototype.calcColumnFooterHeight = function () {
4013 if (this.options.showColumnFooter) {
4014 height += this.options.columnFooterHeight;
4020 Grid.prototype.getFooterStyles = function () {
4021 var style = '.grid' + this.id + ' .ui-grid-footer-aggregates-row { height: ' + this.options.columnFooterHeight + 'px; }';
4022 style += ' .grid' + this.id + ' .ui-grid-footer-info { height: ' + this.options.gridFooterHeight + 'px; }';
4026 Grid.prototype.hasFooter = function () {
4027 return this.options.showGridFooter || this.options.showColumnFooter;
4033 * @methodOf ui.grid.class:Grid
4034 * @description Returns true if grid is RightToLeft
4036 Grid.prototype.isRTL = function () {
4043 * @name registerColumnBuilder
4044 * @methodOf ui.grid.class:Grid
4045 * @description When the build creates columns from column definitions, the columnbuilders will be called to add
4046 * additional properties to the column.
4047 * @param {function(colDef, col, gridOptions)} columnBuilder function to be called
4049 Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) {
4050 this.columnBuilders.push(columnBuilder);
4055 * @name buildColumnDefsFromData
4056 * @methodOf ui.grid.class:Grid
4057 * @description Populates columnDefs from the provided data
4058 * @param {function(colDef, col, gridOptions)} rowBuilder function to be called
4060 Grid.prototype.buildColumnDefsFromData = function (dataRows){
4061 this.options.columnDefs = gridUtil.getColumnsFromData(dataRows, this.options.excludeProperties);
4066 * @name registerRowBuilder
4067 * @methodOf ui.grid.class:Grid
4068 * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add
4069 * additional properties to the row.
4070 * @param {function(row, gridOptions)} rowBuilder function to be called
4072 Grid.prototype.registerRowBuilder = function registerRowBuilder(rowBuilder) {
4073 this.rowBuilders.push(rowBuilder);
4079 * @name registerDataChangeCallback
4080 * @methodOf ui.grid.class:Grid
4081 * @description When a data change occurs, the data change callbacks of the specified type
4082 * will be called. The rules are:
4084 * - when the data watch fires, that is considered a ROW change (the data watch only notices
4085 * added or removed rows)
4086 * - when the api is called to inform us of a change, the declared type of that change is used
4087 * - when a cell edit completes, the EDIT callbacks are triggered
4088 * - when the columnDef watch fires, the COLUMN callbacks are triggered
4089 * - when the options watch fires, the OPTIONS callbacks are triggered
4091 * For a given event:
4092 * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks
4093 * - ROW calls ROW and ALL callbacks
4094 * - EDIT calls EDIT and ALL callbacks
4095 * - COLUMN calls COLUMN and ALL callbacks
4096 * - OPTIONS calls OPTIONS and ALL callbacks
4098 * @param {function(grid)} callback function to be called
4099 * @param {array} types the types of data change you want to be informed of. Values from
4100 * the uiGridConstants.dataChange values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to
4102 * @returns {function} deregister function - a function that can be called to deregister this callback
4104 Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) {
4105 var uid = gridUtil.nextUid();
4107 types = [uiGridConstants.dataChange.ALL];
4109 if ( !Array.isArray(types)){
4110 gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types );
4112 this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this };
4115 var deregisterFunction = function() {
4116 delete self.dataChangeCallbacks[uid];
4118 return deregisterFunction;
4123 * @name callDataChangeCallbacks
4124 * @methodOf ui.grid.class:Grid
4125 * @description Calls the callbacks based on the type of data change that
4126 * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the
4127 * event type is matching, or if the type is ALL.
4128 * @param {number} type the type of event that occurred - one of the
4129 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN, OPTIONS)
4131 Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) {
4132 angular.forEach( this.dataChangeCallbacks, function( callback, uid ){
4133 if ( callback.types.indexOf( uiGridConstants.dataChange.ALL ) !== -1 ||
4134 callback.types.indexOf( type ) !== -1 ||
4135 type === uiGridConstants.dataChange.ALL ) {
4136 if (callback._this) {
4137 callback.callback.apply(callback._this,this);
4140 callback.callback( this );
4148 * @name notifyDataChange
4149 * @methodOf ui.grid.class:Grid
4150 * @description Notifies us that a data change has occurred, used in the public
4151 * api for users to tell us when they've changed data or some other event that
4152 * our watches cannot pick up
4153 * @param {string} type the type of event that occurred - one of the
4154 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN)
4156 Grid.prototype.notifyDataChange = function notifyDataChange(type) {
4157 var constants = uiGridConstants.dataChange;
4158 if ( type === constants.ALL ||
4159 type === constants.COLUMN ||
4160 type === constants.EDIT ||
4161 type === constants.ROW ||
4162 type === constants.OPTIONS ){
4163 this.callDataChangeCallbacks( type );
4165 gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type);
4172 * @name columnRefreshCallback
4173 * @methodOf ui.grid.class:Grid
4174 * @description refreshes the grid when a column refresh
4175 * is notified, which triggers handling of the visible flag.
4176 * This is called on uiGridConstants.dataChange.COLUMN, and is
4177 * registered as a dataChangeCallback in grid.js
4178 * @param {string} name column name
4180 Grid.prototype.columnRefreshCallback = function columnRefreshCallback( grid ){
4181 grid.buildColumns();
4182 grid.queueGridRefresh();
4188 * @name processRowsCallback
4189 * @methodOf ui.grid.class:Grid
4190 * @description calls the row processors, specifically
4191 * intended to reset the sorting when an edit is called,
4192 * registered as a dataChangeCallback on uiGridConstants.dataChange.EDIT
4193 * @param {string} name column name
4195 Grid.prototype.processRowsCallback = function processRowsCallback( grid ){
4196 grid.queueGridRefresh();
4202 * @name updateFooterHeightCallback
4203 * @methodOf ui.grid.class:Grid
4204 * @description recalculates the footer height,
4205 * registered as a dataChangeCallback on uiGridConstants.dataChange.OPTIONS
4206 * @param {string} name column name
4208 Grid.prototype.updateFooterHeightCallback = function updateFooterHeightCallback( grid ){
4209 grid.footerHeight = grid.calcFooterHeight();
4210 grid.columnFooterHeight = grid.calcColumnFooterHeight();
4217 * @methodOf ui.grid.class:Grid
4218 * @description returns a grid column for the column name
4219 * @param {string} name column name
4221 Grid.prototype.getColumn = function getColumn(name) {
4222 var columns = this.columns.filter(function (column) {
4223 return column.colDef.name === name;
4225 return columns.length > 0 ? columns[0] : null;
4231 * @methodOf ui.grid.class:Grid
4232 * @description returns a grid colDef for the column name
4233 * @param {string} name column.field
4235 Grid.prototype.getColDef = function getColDef(name) {
4236 var colDefs = this.options.columnDefs.filter(function (colDef) {
4237 return colDef.name === name;
4239 return colDefs.length > 0 ? colDefs[0] : null;
4245 * @methodOf ui.grid.class:Grid
4246 * @description uses the first row of data to assign colDef.type for any types not defined.
4251 * @propertyOf ui.grid.class:GridOptions.columnDef
4252 * @description the type of the column, used in sorting. If not provided then the
4253 * grid will guess the type. Add this only if the grid guessing is not to your
4254 * satisfaction. One of:
4261 * Note that if you choose date, your dates should be in a javascript date type
4264 Grid.prototype.assignTypes = function(){
4266 self.options.columnDefs.forEach(function (colDef, index) {
4268 //Assign colDef type if not specified
4270 var col = new GridColumn(colDef, index, self);
4271 var firstRow = self.rows.length > 0 ? self.rows[0] : null;
4273 colDef.type = gridUtil.guessType(self.getCellValue(firstRow, col));
4276 colDef.type = 'string';
4285 * @name isRowHeaderColumn
4286 * @methodOf ui.grid.class:Grid
4287 * @description returns true if the column is a row Header
4288 * @param {object} column column
4290 Grid.prototype.isRowHeaderColumn = function isRowHeaderColumn(column) {
4291 return this.rowHeaderColumns.indexOf(column) !== -1;
4296 * @name addRowHeaderColumn
4297 * @methodOf ui.grid.class:Grid
4298 * @description adds a row header column to the grid
4299 * @param {object} column def
4301 Grid.prototype.addRowHeaderColumn = function addRowHeaderColumn(colDef) {
4303 var rowHeaderCol = new GridColumn(colDef, gridUtil.nextUid(), self);
4304 rowHeaderCol.isRowHeader = true;
4306 self.createRightContainer();
4307 rowHeaderCol.renderContainer = 'right';
4310 self.createLeftContainer();
4311 rowHeaderCol.renderContainer = 'left';
4314 // relies on the default column builder being first in array, as it is instantiated
4315 // as part of grid creation
4316 self.columnBuilders[0](colDef,rowHeaderCol,self.options)
4318 rowHeaderCol.enableFiltering = false;
4319 rowHeaderCol.enableSorting = false;
4320 rowHeaderCol.enableHiding = false;
4321 self.rowHeaderColumns.push(rowHeaderCol);
4324 self.preCompileCellTemplates();
4325 self.queueGridRefresh();
4332 * @name getOnlyDataColumns
4333 * @methodOf ui.grid.class:Grid
4334 * @description returns all columns except for rowHeader columns
4336 Grid.prototype.getOnlyDataColumns = function getOnlyDataColumns() {
4339 self.columns.forEach(function (col) {
4340 if (self.rowHeaderColumns.indexOf(col) === -1) {
4349 * @name buildColumns
4350 * @methodOf ui.grid.class:Grid
4351 * @description creates GridColumn objects from the columnDefinition. Calls each registered
4352 * columnBuilder to further process the column
4353 * @param {object} options An object contains options to use when building columns
4355 * * **orderByColumnDefs**: defaults to **false**. When true, `buildColumns` will reorder existing columns according to the order within the column definitions.
4357 * @returns {Promise} a promise to load any needed column resources
4359 Grid.prototype.buildColumns = function buildColumns(opts) {
4361 orderByColumnDefs: false
4364 angular.extend(options, opts);
4366 // gridUtil.logDebug('buildColumns');
4368 var builderPromises = [];
4369 var headerOffset = self.rowHeaderColumns.length;
4372 // Remove any columns for which a columnDef cannot be found
4373 // Deliberately don't use forEach, as it doesn't like splice being called in the middle
4374 // Also don't cache columns.length, as it will change during this operation
4375 for (i = 0; i < self.columns.length; i++){
4376 if (!self.getColDef(self.columns[i].name)) {
4377 self.columns.splice(i, 1);
4382 //add row header columns to the grid columns array _after_ columns without columnDefs have been removed
4383 self.rowHeaderColumns.forEach(function (rowHeaderColumn) {
4384 self.columns.unshift(rowHeaderColumn);
4388 // look at each column def, and update column properties to match. If the column def
4389 // doesn't have a column, then splice in a new gridCol
4390 self.options.columnDefs.forEach(function (colDef, index) {
4391 self.preprocessColDef(colDef);
4392 var col = self.getColumn(colDef.name);
4395 col = new GridColumn(colDef, gridUtil.nextUid(), self);
4396 self.columns.splice(index + headerOffset, 0, col);
4399 // tell updateColumnDef that the column was pre-existing
4400 col.updateColumnDef(colDef, false);
4403 self.columnBuilders.forEach(function (builder) {
4404 builderPromises.push(builder.call(self, colDef, col, self.options));
4408 /*** Reorder columns if necessary ***/
4409 if (!!options.orderByColumnDefs) {
4410 // Create a shallow copy of the columns as a cache
4411 var columnCache = self.columns.slice(0);
4413 // We need to allow for the "row headers" when mapping from the column defs array to the columns array
4414 // 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]
4416 // 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
4417 // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result
4418 var len = Math.min(self.options.columnDefs.length, self.columns.length);
4419 for (i = 0; i < len; i++) {
4420 // If the column at this index has a different name than the column at the same index in the column defs...
4421 if (self.columns[i + headerOffset].name !== self.options.columnDefs[i].name) {
4422 // Replace the one in the cache with the appropriate column
4423 columnCache[i + headerOffset] = self.getColumn(self.options.columnDefs[i].name);
4426 // Otherwise just copy over the one from the initial columns
4427 columnCache[i + headerOffset] = self.columns[i + headerOffset];
4431 // Empty out the columns array, non-destructively
4432 self.columns.length = 0;
4434 // And splice in the updated, ordered columns from the cache
4435 Array.prototype.splice.apply(self.columns, [0, 0].concat(columnCache));
4438 return $q.all(builderPromises).then(function(){
4439 if (self.rows.length > 0){
4447 * @name preCompileCellTemplates
4448 * @methodOf ui.grid.class:Grid
4449 * @description precompiles all cell templates
4451 Grid.prototype.preCompileCellTemplates = function() {
4454 var preCompileTemplate = function( col ) {
4455 var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col));
4456 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
4458 var compiledElementFn = $compile(html);
4459 col.compiledElementFn = compiledElementFn;
4461 if (col.compiledElementFnDefer) {
4462 col.compiledElementFnDefer.resolve(col.compiledElementFn);
4466 this.columns.forEach(function (col) {
4467 if ( col.cellTemplate ){
4468 preCompileTemplate( col );
4469 } else if ( col.cellTemplatePromise ){
4470 col.cellTemplatePromise.then( function() {
4471 preCompileTemplate( col );
4479 * @name getGridQualifiedColField
4480 * @methodOf ui.grid.class:Grid
4481 * @description Returns the $parse-able accessor for a column within its $scope
4482 * @param {GridColumn} col col object
4484 Grid.prototype.getQualifiedColField = function (col) {
4485 return 'row.entity.' + gridUtil.preEval(col.field);
4490 * @name createLeftContainer
4491 * @methodOf ui.grid.class:Grid
4492 * @description creates the left render container if it doesn't already exist
4494 Grid.prototype.createLeftContainer = function() {
4495 if (!this.hasLeftContainer()) {
4496 this.renderContainers.left = new GridRenderContainer('left', this, { disableColumnOffset: true });
4502 * @name createRightContainer
4503 * @methodOf ui.grid.class:Grid
4504 * @description creates the right render container if it doesn't already exist
4506 Grid.prototype.createRightContainer = function() {
4507 if (!this.hasRightContainer()) {
4508 this.renderContainers.right = new GridRenderContainer('right', this, { disableColumnOffset: true });
4514 * @name hasLeftContainer
4515 * @methodOf ui.grid.class:Grid
4516 * @description returns true if leftContainer exists
4518 Grid.prototype.hasLeftContainer = function() {
4519 return this.renderContainers.left !== undefined;
4524 * @name hasRightContainer
4525 * @methodOf ui.grid.class:Grid
4526 * @description returns true if rightContainer exists
4528 Grid.prototype.hasRightContainer = function() {
4529 return this.renderContainers.right !== undefined;
4534 * undocumented function
4535 * @name preprocessColDef
4536 * @methodOf ui.grid.class:Grid
4537 * @description defaults the name property from field to maintain backwards compatibility with 2.x
4538 * validates that name or field is present
4540 Grid.prototype.preprocessColDef = function preprocessColDef(colDef) {
4543 if (!colDef.field && !colDef.name) {
4544 throw new Error('colDef.name or colDef.field property is required');
4547 //maintain backwards compatibility with 2.x
4548 //field was required in 2.x. now name is required
4549 if (colDef.name === undefined && colDef.field !== undefined) {
4550 // See if the column name already exists:
4551 var newName = colDef.field,
4553 while (self.getColumn(newName)) {
4554 newName = colDef.field + counter.toString();
4557 colDef.name = newName;
4561 // 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
4562 Grid.prototype.newInN = function newInN(o, n, oAccessor, nAccessor) {
4566 for (var i = 0; i < n.length; i++) {
4567 var nV = nAccessor ? n[i][nAccessor] : n[i];
4570 for (var j = 0; j < o.length; j++) {
4571 var oV = oAccessor ? o[j][oAccessor] : o[j];
4572 if (self.options.rowEquality(nV, oV)) {
4588 * @methodOf ui.grid.class:Grid
4589 * @description returns the GridRow that contains the rowEntity
4590 * @param {object} rowEntity the gridOptions.data array element instance
4591 * @param {array} rows [optional] the rows to look in - if not provided then
4592 * looks in grid.rows
4594 Grid.prototype.getRow = function getRow(rowEntity, lookInRows) {
4597 lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows;
4599 var rows = lookInRows.filter(function (row) {
4600 return self.options.rowEquality(row.entity, rowEntity);
4602 return rows.length > 0 ? rows[0] : null;
4609 * @methodOf ui.grid.class:Grid
4610 * @description creates or removes GridRow objects from the newRawData array. Calls each registered
4611 * rowBuilder to further process the row
4612 * @param {array} newRawData Modified set of data
4614 * This method aims to achieve three things:
4615 * 1. the resulting rows array is in the same order as the newRawData, we'll call
4616 * rowsProcessors immediately after to sort the data anyway
4617 * 2. if we have row hashing available, we try to use the rowHash to find the row
4618 * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected
4620 * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates
4621 * the newRows and newHash
4624 * newRawData.forEach newEntity
4625 * if (hashing enabled)
4626 * check oldHash for newEntity
4628 * look for old row directly in oldRows
4629 * if !oldRowFound // must be a new row
4631 * append to the newRows and add to newHash
4632 * run the processors
4635 * Rows are identified using the hashKey if configured. If not configured, then rows
4636 * are identified using the gridOptions.rowEquality function
4638 * This method is useful when trying to select rows immediately after loading data without
4639 * using a $timeout/$interval, e.g.:
4641 * $scope.gridOptions.data = someData;
4642 * $scope.gridApi.grid.modifyRows($scope.gridOptions.data);
4643 * $scope.gridApi.selection.selectRow($scope.gridOptions.data[0]);
4645 * OR to persist row selection after data update (e.g. rows selected, new data loaded, want
4646 * originally selected rows to be re-selected))
4648 Grid.prototype.modifyRows = function modifyRows(newRawData) {
4650 var oldRows = self.rows.slice(0);
4651 var oldRowHash = self.rowHashMap || self.createRowHashMap();
4652 self.rowHashMap = self.createRowHashMap();
4653 self.rows.length = 0;
4655 newRawData.forEach( function( newEntity, i ) {
4657 if ( self.options.enableRowHashing ){
4658 // if hashing is enabled, then this row will be in the hash if we already know about it
4659 newRow = oldRowHash.get( newEntity );
4661 // otherwise, manually search the oldRows to see if we can find this row
4662 newRow = self.getRow(newEntity, oldRows);
4665 // if we didn't find the row, it must be new, so create it
4667 newRow = self.processRowBuilders(new GridRow(newEntity, i, self));
4670 self.rows.push( newRow );
4671 self.rowHashMap.put( newEntity, newRow );
4676 var p1 = $q.when(self.processRowsProcessors(self.rows))
4677 .then(function (renderableRows) {
4678 return self.setVisibleRows(renderableRows);
4681 var p2 = $q.when(self.processColumnsProcessors(self.columns))
4682 .then(function (renderableColumns) {
4683 return self.setVisibleColumns(renderableColumns);
4686 return $q.all([p1, p2]);
4691 * Private Undocumented Method
4693 * @methodOf ui.grid.class:Grid
4694 * @description adds the newRawData array of rows to the grid and calls all registered
4695 * rowBuilders. this keyword will reference the grid
4697 Grid.prototype.addRows = function addRows(newRawData) {
4700 var existingRowCount = self.rows.length;
4701 for (var i = 0; i < newRawData.length; i++) {
4702 var newRow = self.processRowBuilders(new GridRow(newRawData[i], i + existingRowCount, self));
4704 if (self.options.enableRowHashing) {
4705 var found = self.rowHashMap.get(newRow.entity);
4711 self.rows.push(newRow);
4717 * @name processRowBuilders
4718 * @methodOf ui.grid.class:Grid
4719 * @description processes all RowBuilders for the gridRow
4720 * @param {GridRow} gridRow reference to gridRow
4721 * @returns {GridRow} the gridRow with all additional behavior added
4723 Grid.prototype.processRowBuilders = function processRowBuilders(gridRow) {
4726 self.rowBuilders.forEach(function (builder) {
4727 builder.call(self, gridRow, self.options);
4735 * @name registerStyleComputation
4736 * @methodOf ui.grid.class:Grid
4737 * @description registered a styleComputation function
4739 * If the function returns a value it will be appended into the grid's `<style>` block
4740 * @param {function($scope)} styleComputation function
4742 Grid.prototype.registerStyleComputation = function registerStyleComputation(styleComputationInfo) {
4743 this.styleComputations.push(styleComputationInfo);
4747 // NOTE (c0bra): We already have rowBuilders. I think these do exactly the same thing...
4748 // Grid.prototype.registerRowFilter = function(filter) {
4749 // // TODO(c0bra): validate filter?
4751 // this.rowFilters.push(filter);
4754 // Grid.prototype.removeRowFilter = function(filter) {
4755 // var idx = this.rowFilters.indexOf(filter);
4757 // if (typeof(idx) !== 'undefined' && idx !== undefined) {
4758 // this.rowFilters.slice(idx, 1);
4762 // Grid.prototype.processRowFilters = function(rows) {
4764 // self.rowFilters.forEach(function (filter) {
4765 // filter.call(self, rows);
4772 * @name registerRowsProcessor
4773 * @methodOf ui.grid.class:Grid
4776 * Register a "rows processor" function. When the rows are updated,
4777 * the grid calls each registered "rows processor", which has a chance
4778 * to alter the set of rows (sorting, etc) as long as the count is not
4781 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
4782 * is run in the context of the grid (i.e. this for the function will be the grid), and must
4783 * return the updated rows list, which is passed to the next processor in the chain
4784 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4785 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
4787 * 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)
4790 Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor, priority) {
4791 if (!angular.isFunction(processor)) {
4792 throw 'Attempt to register non-function rows processor: ' + processor;
4795 this.rowsProcessors.push({processor: processor, priority: priority});
4796 this.rowsProcessors.sort(function sortByPriority( a, b ){
4797 return a.priority - b.priority;
4803 * @name removeRowsProcessor
4804 * @methodOf ui.grid.class:Grid
4805 * @param {function(renderableRows)} rows processor function
4806 * @description Remove a registered rows processor
4808 Grid.prototype.removeRowsProcessor = function removeRowsProcessor(processor) {
4810 this.rowsProcessors.forEach(function(rowsProcessor, index){
4811 if ( rowsProcessor.processor === processor ){
4817 this.rowsProcessors.splice(idx, 1);
4822 * Private Undocumented Method
4823 * @name processRowsProcessors
4824 * @methodOf ui.grid.class:Grid
4825 * @param {Array[GridRow]} The array of "renderable" rows
4826 * @param {Array[GridColumn]} The array of columns
4827 * @description Run all the registered rows processors on the array of renderable rows
4829 Grid.prototype.processRowsProcessors = function processRowsProcessors(renderableRows) {
4832 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4833 var myRenderableRows = renderableRows.slice(0);
4835 // Return myRenderableRows with no processing if we have no rows processors
4836 if (self.rowsProcessors.length === 0) {
4837 return $q.when(myRenderableRows);
4840 // Counter for iterating through rows processors
4843 // Promise for when we're done with all the processors
4844 var finished = $q.defer();
4846 // This function will call the processor in self.rowsProcessors at index 'i', and then
4847 // when done will call the next processor in the list, using the output from the processor
4848 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4850 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4852 function startProcessor(i, renderedRowsToProcess) {
4853 // Get the processor at 'i'
4854 var processor = self.rowsProcessors[i].processor;
4856 // Call the processor, passing in the rows to process and the current columns
4857 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4858 return $q.when( processor.call(self, renderedRowsToProcess, self.columns) )
4859 .then(function handleProcessedRows(processedRows) {
4861 if (!processedRows) {
4862 throw "Processor at index " + i + " did not return a set of renderable rows";
4865 if (!angular.isArray(processedRows)) {
4866 throw "Processor at index " + i + " did not return an array";
4869 // Processor is done, increment the counter
4872 // If we're not done with the processors, call the next one
4873 if (i <= self.rowsProcessors.length - 1) {
4874 return startProcessor(i, processedRows);
4876 // We're done! Resolve the 'finished' promise
4878 finished.resolve(processedRows);
4883 // Start on the first processor
4884 startProcessor(0, myRenderableRows);
4886 return finished.promise;
4889 Grid.prototype.setVisibleRows = function setVisibleRows(rows) {
4892 // Reset all the render container row caches
4893 for (var i in self.renderContainers) {
4894 var container = self.renderContainers[i];
4896 container.canvasHeightShouldUpdate = true;
4898 if ( typeof(container.visibleRowCache) === 'undefined' ){
4899 container.visibleRowCache = [];
4901 container.visibleRowCache.length = 0;
4905 // rows.forEach(function (row) {
4906 for (var ri = 0; ri < rows.length; ri++) {
4909 var targetContainer = (typeof(row.renderContainer) !== 'undefined' && row.renderContainer) ? row.renderContainer : 'body';
4911 // If the row is visible
4913 self.renderContainers[targetContainer].visibleRowCache.push(row);
4916 self.api.core.raise.rowsRendered(this.api);
4921 * @name registerColumnsProcessor
4922 * @methodOf ui.grid.class:Grid
4923 * @param {function(renderedColumnsToProcess, rows)} columnProcessor column processor function, which
4924 * is run in the context of the grid (i.e. this for the function will be the grid), and
4925 * which must return an updated renderedColumnsToProcess which can be passed to the next processor
4927 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4928 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
4930 * 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)
4933 Register a "columns processor" function. When the columns are updated,
4934 the grid calls each registered "columns processor", which has a chance
4935 to alter the set of columns, as long as the count is not modified.
4937 Grid.prototype.registerColumnsProcessor = function registerColumnsProcessor(processor, priority) {
4938 if (!angular.isFunction(processor)) {
4939 throw 'Attempt to register non-function rows processor: ' + processor;
4942 this.columnsProcessors.push({processor: processor, priority: priority});
4943 this.columnsProcessors.sort(function sortByPriority( a, b ){
4944 return a.priority - b.priority;
4948 Grid.prototype.removeColumnsProcessor = function removeColumnsProcessor(processor) {
4949 var idx = this.columnsProcessors.indexOf(processor);
4951 if (typeof(idx) !== 'undefined' && idx !== undefined) {
4952 this.columnsProcessors.splice(idx, 1);
4956 Grid.prototype.processColumnsProcessors = function processColumnsProcessors(renderableColumns) {
4959 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4960 var myRenderableColumns = renderableColumns.slice(0);
4962 // Return myRenderableRows with no processing if we have no rows processors
4963 if (self.columnsProcessors.length === 0) {
4964 return $q.when(myRenderableColumns);
4967 // Counter for iterating through rows processors
4970 // Promise for when we're done with all the processors
4971 var finished = $q.defer();
4973 // This function will call the processor in self.rowsProcessors at index 'i', and then
4974 // when done will call the next processor in the list, using the output from the processor
4975 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4977 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4979 function startProcessor(i, renderedColumnsToProcess) {
4980 // Get the processor at 'i'
4981 var processor = self.columnsProcessors[i].processor;
4983 // Call the processor, passing in the rows to process and the current columns
4984 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4985 return $q.when( processor.call(self, renderedColumnsToProcess, self.rows) )
4986 .then(function handleProcessedRows(processedColumns) {
4988 if (!processedColumns) {
4989 throw "Processor at index " + i + " did not return a set of renderable rows";
4992 if (!angular.isArray(processedColumns)) {
4993 throw "Processor at index " + i + " did not return an array";
4996 // Processor is done, increment the counter
4999 // If we're not done with the processors, call the next one
5000 if (i <= self.columnsProcessors.length - 1) {
5001 return startProcessor(i, myRenderableColumns);
5003 // We're done! Resolve the 'finished' promise
5005 finished.resolve(myRenderableColumns);
5010 // Start on the first processor
5011 startProcessor(0, myRenderableColumns);
5013 return finished.promise;
5016 Grid.prototype.setVisibleColumns = function setVisibleColumns(columns) {
5017 // gridUtil.logDebug('setVisibleColumns');
5021 // Reset all the render container row caches
5022 for (var i in self.renderContainers) {
5023 var container = self.renderContainers[i];
5025 container.visibleColumnCache.length = 0;
5028 for (var ci = 0; ci < columns.length; ci++) {
5029 var column = columns[ci];
5031 // If the column is visible
5032 if (column.visible) {
5033 // If the column has a container specified
5034 if (typeof(column.renderContainer) !== 'undefined' && column.renderContainer) {
5035 self.renderContainers[column.renderContainer].visibleColumnCache.push(column);
5037 // If not, put it into the body container
5039 self.renderContainers.body.visibleColumnCache.push(column);
5047 * @name handleWindowResize
5048 * @methodOf ui.grid.class:Grid
5049 * @description Triggered when the browser window resizes; automatically resizes the grid
5050 * @returns {Promise} A resolved promise once the window resize has completed.
5052 Grid.prototype.handleWindowResize = function handleWindowResize($event) {
5055 self.gridWidth = gridUtil.elementWidth(self.element);
5056 self.gridHeight = gridUtil.elementHeight(self.element);
5058 return self.queueRefresh();
5063 * @name queueRefresh
5064 * @methodOf ui.grid.class:Grid
5065 * @description queues a grid refreshCanvas, a way of debouncing all the refreshes we might otherwise issue
5067 Grid.prototype.queueRefresh = function queueRefresh() {
5070 if (self.refreshCanceller) {
5071 $timeout.cancel(self.refreshCanceller);
5074 self.refreshCanceller = $timeout(function () {
5075 self.refreshCanvas(true);
5078 self.refreshCanceller.then(function () {
5079 self.refreshCanceller = null;
5082 return self.refreshCanceller;
5088 * @name queueGridRefresh
5089 * @methodOf ui.grid.class:Grid
5090 * @description queues a grid refresh, a way of debouncing all the refreshes we might otherwise issue
5092 Grid.prototype.queueGridRefresh = function queueGridRefresh() {
5095 if (self.gridRefreshCanceller) {
5096 $timeout.cancel(self.gridRefreshCanceller);
5099 self.gridRefreshCanceller = $timeout(function () {
5103 self.gridRefreshCanceller.then(function () {
5104 self.gridRefreshCanceller = null;
5107 return self.gridRefreshCanceller;
5113 * @name updateCanvasHeight
5114 * @methodOf ui.grid.class:Grid
5115 * @description flags all render containers to update their canvas height
5117 Grid.prototype.updateCanvasHeight = function updateCanvasHeight() {
5120 for (var containerId in self.renderContainers) {
5121 if (self.renderContainers.hasOwnProperty(containerId)) {
5122 var container = self.renderContainers[containerId];
5123 container.canvasHeightShouldUpdate = true;
5131 * @methodOf ui.grid.class:Grid
5132 * @description calls each styleComputation function
5134 // TODO: this used to take $scope, but couldn't see that it was used
5135 Grid.prototype.buildStyles = function buildStyles() {
5136 // gridUtil.logDebug('buildStyles');
5140 self.customStyles = '';
5142 self.styleComputations
5143 .sort(function(a, b) {
5144 if (a.priority === null) { return 1; }
5145 if (b.priority === null) { return -1; }
5146 if (a.priority === null && b.priority === null) { return 0; }
5147 return a.priority - b.priority;
5149 .forEach(function (compInfo) {
5150 // this used to provide $scope as a second parameter, but I couldn't find any
5151 // style builders that used it, so removed it as part of moving to grid from controller
5152 var ret = compInfo.func.call(self);
5154 if (angular.isString(ret)) {
5155 self.customStyles += '\n' + ret;
5161 Grid.prototype.minColumnsToRender = function minColumnsToRender() {
5163 var viewport = this.getViewportWidth();
5167 self.columns.forEach(function(col, i) {
5168 if (totalWidth < viewport) {
5169 totalWidth += col.drawnWidth;
5174 for (var j = i; j >= i - min; j--) {
5175 currWidth += self.columns[j].drawnWidth;
5177 if (currWidth < viewport) {
5186 Grid.prototype.getBodyHeight = function getBodyHeight() {
5187 // Start with the viewportHeight
5188 var bodyHeight = this.getViewportHeight();
5190 // Add the horizontal scrollbar height if there is one
5191 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5192 // bodyHeight = bodyHeight + this.horizontalScrollbarHeight;
5198 // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border)
5199 // TODO(c0bra): account for footer height
5200 Grid.prototype.getViewportHeight = function getViewportHeight() {
5203 var viewPortHeight = this.gridHeight - this.headerHeight - this.footerHeight;
5205 // Account for native horizontal scrollbar, if present
5206 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5207 // viewPortHeight = viewPortHeight - this.horizontalScrollbarHeight;
5210 var adjustment = self.getViewportAdjustment();
5212 viewPortHeight = viewPortHeight + adjustment.height;
5214 //gridUtil.logDebug('viewPortHeight', viewPortHeight);
5216 return viewPortHeight;
5219 Grid.prototype.getViewportWidth = function getViewportWidth() {
5222 var viewPortWidth = this.gridWidth;
5224 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5225 // viewPortWidth = viewPortWidth - this.verticalScrollbarWidth;
5228 var adjustment = self.getViewportAdjustment();
5230 viewPortWidth = viewPortWidth + adjustment.width;
5232 //gridUtil.logDebug('getviewPortWidth', viewPortWidth);
5234 return viewPortWidth;
5237 Grid.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
5238 var viewPortWidth = this.getViewportWidth();
5240 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5241 // viewPortWidth = viewPortWidth + this.verticalScrollbarWidth;
5244 return viewPortWidth;
5247 Grid.prototype.addVerticalScrollSync = function (containerId, callBackFn) {
5248 this.verticalScrollSyncCallBackFns[containerId] = callBackFn;
5251 Grid.prototype.addHorizontalScrollSync = function (containerId, callBackFn) {
5252 this.horizontalScrollSyncCallBackFns[containerId] = callBackFn;
5256 * Scroll needed containers by calling their ScrollSyncs
5257 * @param sourceContainerId the containerId that has already set it's top/left.
5258 * can be empty string which means all containers need to set top/left
5259 * @param scrollEvent
5261 Grid.prototype.scrollContainers = function (sourceContainerId, scrollEvent) {
5263 if (scrollEvent.y) {
5264 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5265 var verts = ['body','left', 'right'];
5267 this.flagScrollingVertically(scrollEvent);
5269 if (sourceContainerId === 'body') {
5270 verts = ['left', 'right'];
5272 else if (sourceContainerId === 'left') {
5273 verts = ['body', 'right'];
5275 else if (sourceContainerId === 'right') {
5276 verts = ['body', 'left'];
5279 for (var i = 0; i < verts.length; i++) {
5281 if (this.verticalScrollSyncCallBackFns[id]) {
5282 this.verticalScrollSyncCallBackFns[id](scrollEvent);
5288 if (scrollEvent.x) {
5289 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5290 var horizs = ['body','bodyheader', 'bodyfooter'];
5292 this.flagScrollingHorizontally(scrollEvent);
5293 if (sourceContainerId === 'body') {
5294 horizs = ['bodyheader', 'bodyfooter'];
5297 for (var j = 0; j < horizs.length; j++) {
5298 var idh = horizs[j];
5299 if (this.horizontalScrollSyncCallBackFns[idh]) {
5300 this.horizontalScrollSyncCallBackFns[idh](scrollEvent);
5308 Grid.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
5309 this.viewportAdjusters.push(func);
5312 Grid.prototype.removeViewportAdjuster = function registerViewportAdjuster(func) {
5313 var idx = this.viewportAdjusters.indexOf(func);
5315 if (typeof(idx) !== 'undefined' && idx !== undefined) {
5316 this.viewportAdjusters.splice(idx, 1);
5320 Grid.prototype.getViewportAdjustment = function getViewportAdjustment() {
5323 var adjustment = { height: 0, width: 0 };
5325 self.viewportAdjusters.forEach(function (func) {
5326 adjustment = func.call(this, adjustment);
5332 Grid.prototype.getVisibleRowCount = function getVisibleRowCount() {
5335 // this.rows.forEach(function (row) {
5336 // if (row.visible) {
5341 // return this.visibleRowCache.length;
5342 return this.renderContainers.body.visibleRowCache.length;
5345 Grid.prototype.getVisibleRows = function getVisibleRows() {
5346 return this.renderContainers.body.visibleRowCache;
5349 Grid.prototype.getVisibleColumnCount = function getVisibleColumnCount() {
5352 // this.rows.forEach(function (row) {
5353 // if (row.visible) {
5358 // return this.visibleRowCache.length;
5359 return this.renderContainers.body.visibleColumnCache.length;
5363 Grid.prototype.searchRows = function searchRows(renderableRows) {
5364 return rowSearcher.search(this, renderableRows, this.columns);
5367 Grid.prototype.sortByColumn = function sortByColumn(renderableRows) {
5368 return rowSorter.sort(this, renderableRows, this.columns);
5373 * @name getCellValue
5374 * @methodOf ui.grid.class:Grid
5375 * @description Gets the value of a cell for a particular row and column
5376 * @param {GridRow} row Row to access
5377 * @param {GridColumn} col Column to access
5379 Grid.prototype.getCellValue = function getCellValue(row, col){
5380 if ( typeof(row.entity[ '$$' + col.uid ]) !== 'undefined' ) {
5381 return row.entity[ '$$' + col.uid].rendered;
5382 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined' ){
5383 return row.entity[col.field];
5385 if (!col.cellValueGetterCache) {
5386 col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col));
5389 return col.cellValueGetterCache(row);
5395 * @name getCellDisplayValue
5396 * @methodOf ui.grid.class:Grid
5397 * @description Gets the displayed value of a cell after applying any the `cellFilter`
5398 * @param {GridRow} row Row to access
5399 * @param {GridColumn} col Column to access
5401 Grid.prototype.getCellDisplayValue = function getCellDisplayValue(row, col) {
5402 if ( !col.cellDisplayGetterCache ) {
5403 var custom_filter = col.cellFilter ? " | " + col.cellFilter : "";
5405 if (typeof(row.entity['$$' + col.uid]) !== 'undefined') {
5406 col.cellDisplayGetterCache = $parse(row.entity['$$' + col.uid].rendered + custom_filter);
5407 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined') {
5408 col.cellDisplayGetterCache = $parse(row.entity[col.field] + custom_filter);
5410 col.cellDisplayGetterCache = $parse(row.getEntityQualifiedColField(col) + custom_filter);
5414 return col.cellDisplayGetterCache(row);
5418 Grid.prototype.getNextColumnSortPriority = function getNextColumnSortPriority() {
5422 self.columns.forEach(function (col) {
5423 if (col.sort && col.sort.priority && col.sort.priority > p) {
5424 p = col.sort.priority;
5433 * @name resetColumnSorting
5434 * @methodOf ui.grid.class:Grid
5435 * @description Return the columns that the grid is currently being sorted by
5436 * @param {GridColumn} [excludedColumn] Optional GridColumn to exclude from having its sorting reset
5438 Grid.prototype.resetColumnSorting = function resetColumnSorting(excludeCol) {
5441 self.columns.forEach(function (col) {
5442 if (col !== excludeCol && !col.suppressRemoveSort) {
5450 * @name getColumnSorting
5451 * @methodOf ui.grid.class:Grid
5452 * @description Return the columns that the grid is currently being sorted by
5453 * @returns {Array[GridColumn]} An array of GridColumn objects
5455 Grid.prototype.getColumnSorting = function getColumnSorting() {
5458 var sortedCols = [], myCols;
5460 // Iterate through all the columns, sorted by priority
5461 // Make local copy of column list, because sorting is in-place and we do not want to
5462 // change the original sequence of columns
5463 myCols = self.columns.slice(0);
5464 myCols.sort(rowSorter.prioritySort).forEach(function (col) {
5465 if (col.sort && typeof(col.sort.direction) !== 'undefined' && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
5466 sortedCols.push(col);
5476 * @methodOf ui.grid.class:Grid
5477 * @description Set the sorting on a given column, optionally resetting any existing sorting on the Grid.
5478 * Emits the sortChanged event whenever the sort criteria are changed.
5479 * @param {GridColumn} column Column to set the sorting on
5480 * @param {uiGridConstants.ASC|uiGridConstants.DESC} [direction] Direction to sort by, either descending or ascending.
5481 * If not provided, the column will iterate through the sort directions
5482 * specified in the {@link ui.grid.class:GridOptions.columnDef#sortDirectionCycle sortDirectionCycle} attribute.
5483 * @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
5484 * by this column only
5485 * @returns {Promise} A resolved promise that supplies the column.
5488 Grid.prototype.sortColumn = function sortColumn(column, directionOrAdd, add) {
5492 if (typeof(column) === 'undefined' || !column) {
5493 throw new Error('No column parameter provided');
5496 // Second argument can either be a direction or whether to add this column to the existing sort.
5497 // If it's a boolean, it's an add, otherwise, it's a direction
5498 if (typeof(directionOrAdd) === 'boolean') {
5499 add = directionOrAdd;
5502 direction = directionOrAdd;
5506 self.resetColumnSorting(column);
5507 column.sort.priority = 0;
5508 // Get the actual priority since there may be columns which have suppressRemoveSort set
5509 column.sort.priority = self.getNextColumnSortPriority();
5511 else if (!column.sort.priority){
5512 column.sort.priority = self.getNextColumnSortPriority();
5516 // Find the current position in the cycle (or -1).
5517 var i = column.sortDirectionCycle.indexOf(column.sort.direction ? column.sort.direction : null);
5518 // Proceed to the next position in the cycle (or start at the beginning).
5519 i = (i+1) % column.sortDirectionCycle.length;
5520 // If suppressRemoveSort is set, and the next position in the cycle would
5521 // remove the sort, skip it.
5522 if (column.colDef && column.suppressRemoveSort && !column.sortDirectionCycle[i]) {
5523 i = (i+1) % column.sortDirectionCycle.length;
5526 if (column.sortDirectionCycle[i]) {
5527 column.sort.direction = column.sortDirectionCycle[i];
5533 column.sort.direction = direction;
5536 self.api.core.raise.sortChanged( self, self.getColumnSorting() );
5538 return $q.when(column);
5542 * communicate to outside world that we are done with initial rendering
5544 Grid.prototype.renderingComplete = function(){
5545 if (angular.isFunction(this.options.onRegisterApi)) {
5546 this.options.onRegisterApi(this.api);
5548 this.api.core.raise.renderingComplete( this.api );
5551 Grid.prototype.createRowHashMap = function createRowHashMap() {
5554 var hashMap = new RowHashMap();
5555 hashMap.grid = self;
5564 * @methodOf ui.grid.class:Grid
5565 * @description Refresh the rendered grid on screen.
5566 * @param {boolean} [rowsAltered] Optional flag for refreshing when the number of rows has changed.
5568 Grid.prototype.refresh = function refresh(rowsAltered) {
5571 var p1 = self.processRowsProcessors(self.rows).then(function (renderableRows) {
5572 self.setVisibleRows(renderableRows);
5575 var p2 = self.processColumnsProcessors(self.columns).then(function (renderableColumns) {
5576 self.setVisibleColumns(renderableColumns);
5579 return $q.all([p1, p2]).then(function () {
5580 self.redrawInPlace(rowsAltered);
5582 self.refreshCanvas(true);
5589 * @methodOf ui.grid.class:Grid
5590 * @description Refresh the rendered rows on screen? Note: not functional at present
5591 * @returns {promise} promise that is resolved when render completes?
5594 Grid.prototype.refreshRows = function refreshRows() {
5597 return self.processRowsProcessors(self.rows)
5598 .then(function (renderableRows) {
5599 self.setVisibleRows(renderableRows);
5601 self.redrawInPlace();
5603 self.refreshCanvas( true );
5609 * @name refreshCanvas
5610 * @methodOf ui.grid.class:Grid
5611 * @description Builds all styles and recalculates much of the grid sizing
5612 * @param {object} buildStyles optional parameter. Use TBD
5613 * @returns {promise} promise that is resolved when the canvas
5614 * has been refreshed
5617 Grid.prototype.refreshCanvas = function(buildStyles) {
5626 // Get all the header heights
5627 var containerHeadersToRecalc = [];
5628 for (var containerId in self.renderContainers) {
5629 if (self.renderContainers.hasOwnProperty(containerId)) {
5630 var container = self.renderContainers[containerId];
5632 // Skip containers that have no canvasWidth set yet
5633 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5637 if (container.header || container.headerCanvas) {
5638 container.explicitHeaderHeight = container.explicitHeaderHeight || null;
5639 container.explicitHeaderCanvasHeight = container.explicitHeaderCanvasHeight || null;
5641 containerHeadersToRecalc.push(container);
5648 * Here we loop through the headers, measuring each element as well as any header "canvas" it has within it.
5650 * If any header is less than the largest header height, it will be resized to that so that we don't have headers
5651 * with different heights, which looks like a rendering problem
5653 * We'll do the same thing with the header canvases, and give the header CELLS an explicit height if their canvas
5654 * is smaller than the largest canvas height. That was header cells without extra controls like filtering don't
5655 * appear shorter than other cells.
5658 if (containerHeadersToRecalc.length > 0) {
5659 // Build the styles without the explicit header heights
5664 // Putting in a timeout as it's not calculating after the grid element is rendered and filled out
5665 $timeout(function() {
5666 // var oldHeaderHeight = self.grid.headerHeight;
5667 // self.grid.headerHeight = gridUtil.outerElementHeight(self.header);
5669 var rebuildStyles = false;
5671 // Get all the header heights
5672 var maxHeaderHeight = 0;
5673 var maxHeaderCanvasHeight = 0;
5675 var getHeight = function(oldVal, newVal){
5676 if ( oldVal !== newVal){
5677 rebuildStyles = true;
5681 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5682 container = containerHeadersToRecalc[i];
5684 // Skip containers that have no canvasWidth set yet
5685 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5689 if (container.header) {
5690 var headerHeight = container.headerHeight = getHeight(container.headerHeight, parseInt(gridUtil.outerElementHeight(container.header), 10));
5692 // 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
5693 var topBorder = gridUtil.getBorderSize(container.header, 'top');
5694 var bottomBorder = gridUtil.getBorderSize(container.header, 'bottom');
5695 var innerHeaderHeight = parseInt(headerHeight - topBorder - bottomBorder, 10);
5697 innerHeaderHeight = innerHeaderHeight < 0 ? 0 : innerHeaderHeight;
5699 container.innerHeaderHeight = innerHeaderHeight;
5701 // If the header doesn't have an explicit height set, save the largest header height for use later
5702 // 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
5703 if (!container.explicitHeaderHeight && innerHeaderHeight > maxHeaderHeight) {
5704 maxHeaderHeight = innerHeaderHeight;
5708 if (container.headerCanvas) {
5709 var headerCanvasHeight = container.headerCanvasHeight = getHeight(container.headerCanvasHeight, parseInt(gridUtil.outerElementHeight(container.headerCanvas), 10));
5712 // If the header doesn't have an explicit canvas height, save the largest header canvas height for use later
5713 // 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
5714 if (!container.explicitHeaderCanvasHeight && headerCanvasHeight > maxHeaderCanvasHeight) {
5715 maxHeaderCanvasHeight = headerCanvasHeight;
5720 // Go through all the headers
5721 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5722 container = containerHeadersToRecalc[i];
5725 1. We have a max header height
5726 2. This container has a header height defined
5727 3. And either this container has an explicit header height set, OR its header height is less than the max
5731 Give this container's header an explicit height so it will line up with the tallest header
5734 maxHeaderHeight > 0 && typeof(container.headerHeight) !== 'undefined' && container.headerHeight !== null &&
5735 (container.explicitHeaderHeight || container.headerHeight < maxHeaderHeight)
5737 container.explicitHeaderHeight = getHeight(container.explicitHeaderHeight, maxHeaderHeight);
5740 // Do the same as above except for the header canvas
5742 maxHeaderCanvasHeight > 0 && typeof(container.headerCanvasHeight) !== 'undefined' && container.headerCanvasHeight !== null &&
5743 (container.explicitHeaderCanvasHeight || container.headerCanvasHeight < maxHeaderCanvasHeight)
5745 container.explicitHeaderCanvasHeight = getHeight(container.explicitHeaderCanvasHeight, maxHeaderCanvasHeight);
5749 // Rebuild styles if the header height has changed
5750 // The header height is used in body/viewport calculations and those are then used in other styles so we need it to be available
5751 if (buildStyles && rebuildStyles) {
5759 // Timeout still needs to be here to trigger digest after styles have been rebuilt
5760 $timeout(function() {
5771 * @name redrawCanvas
5772 * @methodOf ui.grid.class:Grid
5773 * @description Redraw the rows and columns based on our current scroll position
5774 * @param {boolean} [rowsAdded] Optional to indicate rows are added and the scroll percentage must be recalculated
5777 Grid.prototype.redrawInPlace = function redrawInPlace(rowsAdded) {
5778 // gridUtil.logDebug('redrawInPlace');
5782 for (var i in self.renderContainers) {
5783 var container = self.renderContainers[i];
5785 // gridUtil.logDebug('redrawing container', i);
5788 container.adjustRows(container.prevScrollTop, null);
5789 container.adjustColumns(container.prevScrollLeft, null);
5792 container.adjustRows(null, container.prevScrolltopPercentage);
5793 container.adjustColumns(null, container.prevScrollleftPercentage);
5800 * @name hasLeftContainerColumns
5801 * @methodOf ui.grid.class:Grid
5802 * @description returns true if leftContainer has columns
5804 Grid.prototype.hasLeftContainerColumns = function () {
5805 return this.hasLeftContainer() && this.renderContainers.left.renderedColumns.length > 0;
5810 * @name hasRightContainerColumns
5811 * @methodOf ui.grid.class:Grid
5812 * @description returns true if rightContainer has columns
5814 Grid.prototype.hasRightContainerColumns = function () {
5815 return this.hasRightContainer() && this.renderContainers.right.renderedColumns.length > 0;
5820 * @methodOf ui.grid.class:Grid
5821 * @name scrollToIfNecessary
5822 * @description Scrolls the grid to make a certain row and column combo visible,
5823 * in the case that it is not completely visible on the screen already.
5824 * @param {GridRow} gridRow row to make visible
5825 * @param {GridCol} gridCol column to make visible
5826 * @returns {promise} a promise that is resolved when scrolling is complete
5828 Grid.prototype.scrollToIfNecessary = function (gridRow, gridCol) {
5831 var scrollEvent = new ScrollEvent(self, 'uiGrid.scrollToIfNecessary');
5833 // Alias the visible row and column caches
5834 var visRowCache = self.renderContainers.body.visibleRowCache;
5835 var visColCache = self.renderContainers.body.visibleColumnCache;
5837 /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/
5839 // 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
5840 var topBound = self.renderContainers.body.prevScrollTop + self.headerHeight;
5842 // Don't the let top boundary be less than 0
5843 topBound = (topBound < 0) ? 0 : topBound;
5845 // The left boundary is the current X scroll position
5846 var leftBound = self.renderContainers.body.prevScrollLeft;
5848 // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height.
5849 // Basically this is the viewport height added on to the scroll position
5850 var bottomBound = self.renderContainers.body.prevScrollTop + self.gridHeight - self.renderContainers.body.headerHeight - self.footerHeight - self.scrollbarWidth;
5852 // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows
5853 //if (self.horizontalScrollbarHeight) {
5854 // bottomBound = bottomBound - self.horizontalScrollbarHeight;
5857 // The right position is the current X scroll position minus the grid width
5858 var rightBound = self.renderContainers.body.prevScrollLeft + Math.ceil(self.renderContainers.body.getViewportWidth());
5860 // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells
5861 //if (self.verticalScrollbarWidth) {
5862 // rightBound = rightBound - self.verticalScrollbarWidth;
5865 // We were given a row to scroll to
5866 if (gridRow !== null) {
5867 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5868 var seekRowIndex = visRowCache.indexOf(gridRow);
5870 // Total vertical scroll length of the grid
5871 var scrollLength = (self.renderContainers.body.getCanvasHeight() - self.renderContainers.body.getViewportHeight());
5873 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5874 //if (self.horizontalScrollbarHeight && self.horizontalScrollbarHeight > 0) {
5875 // scrollLength = scrollLength + self.horizontalScrollbarHeight;
5878 // This is the minimum amount of pixels we need to scroll vertical in order to see this row.
5879 var pixelsToSeeRow = (seekRowIndex * self.options.rowHeight + self.headerHeight);
5881 // Don't let the pixels required to see the row be less than zero
5882 pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow;
5884 var scrollPixels, percentage;
5886 // 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...
5887 if (pixelsToSeeRow < topBound) {
5888 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5889 // to get the full position we need
5890 scrollPixels = self.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow);
5892 // Turn the scroll position into a percentage and make it an argument for a scroll event
5893 percentage = scrollPixels / scrollLength;
5894 scrollEvent.y = { percentage: percentage };
5896 // 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...
5897 else if (pixelsToSeeRow > bottomBound) {
5898 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5899 // to get the full position we need
5900 scrollPixels = pixelsToSeeRow - bottomBound + self.renderContainers.body.prevScrollTop;
5902 // Turn the scroll position into a percentage and make it an argument for a scroll event
5903 percentage = scrollPixels / scrollLength;
5904 scrollEvent.y = { percentage: percentage };
5908 // We were given a column to scroll to
5909 if (gridCol !== null) {
5910 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5911 var seekColumnIndex = visColCache.indexOf(gridCol);
5913 // Total vertical scroll length of the grid
5914 var horizScrollLength = (self.renderContainers.body.getCanvasWidth() - self.renderContainers.body.getViewportWidth());
5916 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5917 // if (self.verticalScrollbarWidth && self.verticalScrollbarWidth > 0) {
5918 // horizScrollLength = horizScrollLength + self.verticalScrollbarWidth;
5921 // This is the minimum amount of pixels we need to scroll vertical in order to see this column
5922 var columnLeftEdge = 0;
5923 for (var i = 0; i < seekColumnIndex; i++) {
5924 var col = visColCache[i];
5925 columnLeftEdge += col.drawnWidth;
5927 columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge;
5929 var columnRightEdge = columnLeftEdge + gridCol.drawnWidth;
5931 // Don't let the pixels required to see the column be less than zero
5932 columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge;
5934 var horizScrollPixels, horizPercentage;
5936 // 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...
5937 if (columnLeftEdge < leftBound) {
5938 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5939 // to get the full position we need
5940 horizScrollPixels = self.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge);
5942 // Turn the scroll position into a percentage and make it an argument for a scroll event
5943 horizPercentage = horizScrollPixels / horizScrollLength;
5944 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5945 scrollEvent.x = { percentage: horizPercentage };
5947 // 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...
5948 else if (columnRightEdge > rightBound) {
5949 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5950 // to get the full position we need
5951 horizScrollPixels = columnRightEdge - rightBound + self.renderContainers.body.prevScrollLeft;
5953 // Turn the scroll position into a percentage and make it an argument for a scroll event
5954 horizPercentage = horizScrollPixels / horizScrollLength;
5955 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5956 scrollEvent.x = { percentage: horizPercentage };
5960 var deferred = $q.defer();
5962 // If we need to scroll on either the x or y axes, fire a scroll event
5963 if (scrollEvent.y || scrollEvent.x) {
5964 scrollEvent.withDelay = false;
5965 self.scrollContainers('',scrollEvent);
5966 var dereg = self.api.core.on.scrollEnd(null,function() {
5967 deferred.resolve(scrollEvent);
5975 return deferred.promise;
5980 * @methodOf ui.grid.class:Grid
5982 * @description Scroll the grid such that the specified
5983 * row and column is in view
5984 * @param {object} rowEntity gridOptions.data[] array instance to make visible
5985 * @param {object} colDef to make visible
5986 * @returns {promise} a promise that is resolved after any scrolling is finished
5988 Grid.prototype.scrollTo = function (rowEntity, colDef) {
5989 var gridRow = null, gridCol = null;
5991 if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) {
5992 gridRow = this.getRow(rowEntity);
5995 if (colDef !== null && typeof(colDef) !== 'undefined' ) {
5996 gridCol = this.getColumn(colDef.name ? colDef.name : colDef.field);
5998 return this.scrollToIfNecessary(gridRow, gridCol);
6003 * @name clearAllFilters
6004 * @methodOf ui.grid.class:Grid
6005 * @description Clears all filters and optionally refreshes the visible rows.
6006 * @param {object} refreshRows Defaults to true.
6007 * @param {object} clearConditions Defaults to false.
6008 * @param {object} clearFlags Defaults to false.
6009 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
6011 Grid.prototype.clearAllFilters = function clearAllFilters(refreshRows, clearConditions, clearFlags) {
6012 // Default `refreshRows` to true because it will be the most commonly desired behaviour.
6013 if (refreshRows === undefined) {
6016 if (clearConditions === undefined) {
6017 clearConditions = false;
6019 if (clearFlags === undefined) {
6023 this.columns.forEach(function(column) {
6024 column.filters.forEach(function(filter) {
6025 filter.term = undefined;
6027 if (clearConditions) {
6028 filter.condition = undefined;
6032 filter.flags = undefined;
6038 return this.refreshRows();
6043 // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?)
6044 function RowHashMap() {}
6046 RowHashMap.prototype = {
6048 * Store key value pair
6049 * @param key key to store can be any type
6050 * @param value value to store can be any type
6052 put: function(key, value) {
6053 this[this.grid.options.rowIdentity(key)] = value;
6058 * @returns {Object} the value for the key
6060 get: function(key) {
6061 return this[this.grid.options.rowIdentity(key)];
6065 * Remove the key/value pair
6068 remove: function(key) {
6069 var value = this[key = this.grid.options.rowIdentity(key)];
6085 angular.module('ui.grid')
6086 .factory('GridApi', ['$q', '$rootScope', 'gridUtil', 'uiGridConstants', 'GridRow', 'uiGridGridMenuService',
6087 function ($q, $rootScope, gridUtil, uiGridConstants, GridRow, uiGridGridMenuService) {
6090 * @name ui.grid.class:GridApi
6091 * @description GridApi provides the ability to register public methods events inside the grid and allow
6092 * for other components to use the api via featureName.raise.methodName and featureName.on.eventName(function(args){}.
6094 * To listen to events, you must add a callback to gridOptions.onRegisterApi
6096 * $scope.gridOptions.onRegisterApi = function(gridApi){
6097 * gridApi.cellNav.on.navigate($scope,function(newRowCol, oldRowCol){
6098 * $log.log('navigation event');
6102 * @param {object} grid grid that owns api
6104 var GridApi = function GridApi(grid) {
6106 this.listeners = [];
6110 * @name renderingComplete
6111 * @methodOf ui.grid.core.api:PublicApi
6112 * @description Rendering is complete, called at the same
6113 * time as `onRegisterApi`, but provides a way to obtain
6114 * that same event within features without stopping end
6115 * users from getting at the onRegisterApi method.
6117 * Included in gridApi so that it's always there - otherwise
6118 * there is still a timing problem with when a feature can
6121 * @param {GridApi} gridApi the grid api, as normally
6122 * returned in the onRegisterApi method
6126 * gridApi.core.on.renderingComplete( grid );
6129 this.registerEvent( 'core', 'renderingComplete' );
6133 * @name filterChanged
6134 * @eventOf ui.grid.core.api:PublicApi
6135 * @description is raised after the filter is changed. The nature
6136 * of the watch expression doesn't allow notification of what changed,
6137 * so the receiver of this event will need to re-extract the filter
6138 * conditions from the columns.
6141 this.registerEvent( 'core', 'filterChanged' );
6145 * @name setRowInvisible
6146 * @methodOf ui.grid.core.api:PublicApi
6147 * @description Sets an override on the row to make it always invisible,
6148 * which will override any filtering or other visibility calculations.
6149 * If the row is currently visible then sets it to invisible and calls
6150 * both grid refresh and emits the rowsVisibleChanged event
6151 * @param {object} rowEntity gridOptions.data[] array instance
6153 this.registerMethod( 'core', 'setRowInvisible', GridRow.prototype.setRowInvisible );
6157 * @name clearRowInvisible
6158 * @methodOf ui.grid.core.api:PublicApi
6159 * @description Clears any override on visibility for the row so that it returns to
6160 * using normal filtering and other visibility calculations.
6161 * If the row is currently invisible then sets it to visible and calls
6162 * both grid refresh and emits the rowsVisibleChanged event
6163 * TODO: if a filter is active then we can't just set it to visible?
6164 * @param {object} rowEntity gridOptions.data[] array instance
6166 this.registerMethod( 'core', 'clearRowInvisible', GridRow.prototype.clearRowInvisible );
6170 * @name getVisibleRows
6171 * @methodOf ui.grid.core.api:PublicApi
6172 * @description Returns all visible rows
6173 * @param {Grid} grid the grid you want to get visible rows from
6174 * @returns {array} an array of gridRow
6176 this.registerMethod( 'core', 'getVisibleRows', this.grid.getVisibleRows );
6180 * @name rowsVisibleChanged
6181 * @eventOf ui.grid.core.api:PublicApi
6182 * @description is raised after the rows that are visible
6183 * change. The filtering is zero-based, so it isn't possible
6184 * to say which rows changed (unlike in the selection feature).
6185 * We can plausibly know which row was changed when setRowInvisible
6186 * is called, but in that situation the user already knows which row
6187 * they changed. When a filter runs we don't know what changed,
6188 * and that is the one that would have been useful.
6191 this.registerEvent( 'core', 'rowsVisibleChanged' );
6195 * @name rowsRendered
6196 * @eventOf ui.grid.core.api:PublicApi
6197 * @description is raised after the cache of visible rows is changed.
6199 this.registerEvent( 'core', 'rowsRendered' );
6205 * @eventOf ui.grid.core.api:PublicApi
6206 * @description is raised when scroll begins. Is throttled, so won't be raised too frequently
6208 this.registerEvent( 'core', 'scrollBegin' );
6213 * @eventOf ui.grid.core.api:PublicApi
6214 * @description is raised when scroll has finished. Is throttled, so won't be raised too frequently
6216 this.registerEvent( 'core', 'scrollEnd' );
6220 * @name canvasHeightChanged
6221 * @eventOf ui.grid.core.api:PublicApi
6222 * @description is raised when the canvas height has changed
6224 * arguments: oldHeight, newHeight
6226 this.registerEvent( 'core', 'canvasHeightChanged');
6231 * @name ui.grid.class:suppressEvents
6232 * @methodOf ui.grid.class:GridApi
6233 * @description Used to execute a function while disabling the specified event listeners.
6234 * Disables the listenerFunctions, executes the callbackFn, and then enables
6235 * the listenerFunctions again
6236 * @param {object} listenerFuncs listenerFunc or array of listenerFuncs to suppress. These must be the same
6237 * functions that were used in the .on.eventName method
6238 * @param {object} callBackFn function to execute
6241 * var navigate = function (newRowCol, oldRowCol){
6242 * //do something on navigate
6245 * gridApi.cellNav.on.navigate(scope,navigate);
6248 * //call the scrollTo event and suppress our navigate listener
6249 * //scrollTo will still raise the event for other listeners
6250 * gridApi.suppressEvents(navigate, function(){
6251 * gridApi.cellNav.scrollTo(aRow, aCol);
6256 GridApi.prototype.suppressEvents = function (listenerFuncs, callBackFn) {
6258 var listeners = angular.isArray(listenerFuncs) ? listenerFuncs : [listenerFuncs];
6260 //find all registered listeners
6261 var foundListeners = self.listeners.filter(function(listener) {
6262 return listeners.some(function(l) {
6263 return listener.handler === l;
6267 //deregister all the listeners
6268 foundListeners.forEach(function(l){
6274 //reregister all the listeners
6275 foundListeners.forEach(function(l){
6276 l.dereg = registerEventWithAngular(l.eventId, l.handler, self.grid, l._this);
6283 * @name registerEvent
6284 * @methodOf ui.grid.class:GridApi
6285 * @description Registers a new event for the given feature. The event will get a
6286 * .raise and .on prepended to it
6288 * .raise.eventName() - takes no arguments
6291 * .on.eventName(scope, callBackFn, _this)
6293 * scope - a scope reference to add a deregister call to the scopes .$on('destroy'). Scope is optional and can be a null value,
6294 * but in this case you must deregister it yourself via the returned deregister function
6296 * callBackFn - The function to call
6298 * _this - optional this context variable for callbackFn. If omitted, grid.api will be used for the context
6300 * .on.eventName returns a dereg funtion that will remove the listener. It's not necessary to use it as the listener
6301 * will be removed when the scope is destroyed.
6302 * @param {string} featureName name of the feature that raises the event
6303 * @param {string} eventName name of the event
6305 GridApi.prototype.registerEvent = function (featureName, eventName) {
6307 if (!self[featureName]) {
6308 self[featureName] = {};
6311 var feature = self[featureName];
6317 var eventId = self.grid.id + featureName + eventName;
6319 // gridUtil.logDebug('Creating raise event method ' + featureName + '.raise.' + eventName);
6320 feature.raise[eventName] = function () {
6321 $rootScope.$emit.apply($rootScope, [eventId].concat(Array.prototype.slice.call(arguments)));
6324 // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName);
6325 feature.on[eventName] = function (scope, handler, _this) {
6326 if ( scope !== null && typeof(scope.$on) === 'undefined' ){
6327 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');
6330 var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this);
6332 //track our listener so we can turn off and on
6333 var listener = {handler: handler, dereg: deregAngularOn, eventId: eventId, scope: scope, _this:_this};
6334 self.listeners.push(listener);
6336 var removeListener = function(){
6338 var index = self.listeners.indexOf(listener);
6339 self.listeners.splice(index,1);
6342 //destroy tracking when scope is destroyed
6344 scope.$on('$destroy', function() {
6350 return removeListener;
6354 function registerEventWithAngular(eventId, handler, grid, _this) {
6355 return $rootScope.$on(eventId, function (event) {
6356 var args = Array.prototype.slice.call(arguments);
6357 args.splice(0, 1); //remove evt argument
6358 handler.apply(_this ? _this : grid.api, args);
6364 * @name registerEventsFromObject
6365 * @methodOf ui.grid.class:GridApi
6366 * @description Registers features and events from a simple objectMap.
6367 * eventObjectMap must be in this format (multiple features allowed)
6371 * eventNameOne:function(args){},
6372 * eventNameTwo:function(args){}
6376 * @param {object} eventObjectMap map of feature/event names
6378 GridApi.prototype.registerEventsFromObject = function (eventObjectMap) {
6381 angular.forEach(eventObjectMap, function (featProp, featPropName) {
6382 var feature = {name: featPropName, events: []};
6383 angular.forEach(featProp, function (prop, propName) {
6384 feature.events.push(propName);
6386 features.push(feature);
6389 features.forEach(function (feature) {
6390 feature.events.forEach(function (event) {
6391 self.registerEvent(feature.name, event);
6399 * @name registerMethod
6400 * @methodOf ui.grid.class:GridApi
6401 * @description Registers a new event for the given feature
6402 * @param {string} featureName name of the feature
6403 * @param {string} methodName name of the method
6404 * @param {object} callBackFn function to execute
6405 * @param {object} _this binds callBackFn 'this' to _this. Defaults to gridApi.grid
6407 GridApi.prototype.registerMethod = function (featureName, methodName, callBackFn, _this) {
6408 if (!this[featureName]) {
6409 this[featureName] = {};
6412 var feature = this[featureName];
6414 feature[methodName] = gridUtil.createBoundedWrapper(_this || this.grid, callBackFn);
6419 * @name registerMethodsFromObject
6420 * @methodOf ui.grid.class:GridApi
6421 * @description Registers features and methods from a simple objectMap.
6422 * eventObjectMap must be in this format (multiple features allowed)
6426 * methodNameOne:function(args){},
6427 * methodNameTwo:function(args){}
6429 * @param {object} eventObjectMap map of feature/event names
6430 * @param {object} _this binds this to _this for all functions. Defaults to gridApi.grid
6432 GridApi.prototype.registerMethodsFromObject = function (methodMap, _this) {
6435 angular.forEach(methodMap, function (featProp, featPropName) {
6436 var feature = {name: featPropName, methods: []};
6437 angular.forEach(featProp, function (prop, propName) {
6438 feature.methods.push({name: propName, fn: prop});
6440 features.push(feature);
6443 features.forEach(function (feature) {
6444 feature.methods.forEach(function (method) {
6445 self.registerMethod(feature.name, method.name, method.fn, _this);
6459 angular.module('ui.grid')
6460 .factory('GridColumn', ['gridUtil', 'uiGridConstants', 'i18nService', function(gridUtil, uiGridConstants, i18nService) {
6463 * ******************************************************************************************
6464 * PaulL1: Ugly hack here in documentation. These properties are clearly properties of GridColumn,
6465 * and need to be noted as such for those extending and building ui-grid itself.
6466 * However, from an end-developer perspective, they interact with all these through columnDefs,
6467 * and they really need to be documented there. I feel like they're relatively static, and
6468 * I can't find an elegant way for ngDoc to reference to both....so I've duplicated each
6469 * comment block. Ugh.
6476 * @propertyOf ui.grid.class:GridColumn
6477 * @description (mandatory) each column should have a name, although for backward
6478 * compatibility with 2.x name can be omitted if field is present
6485 * @propertyOf ui.grid.class:GridOptions.columnDef
6486 * @description (mandatory) each column should have a name, although for backward
6487 * compatibility with 2.x name can be omitted if field is present
6494 * @propertyOf ui.grid.class:GridColumn
6495 * @description Column name that will be shown in the header. If displayName is not
6496 * provided then one is generated using the name.
6503 * @propertyOf ui.grid.class:GridOptions.columnDef
6504 * @description Column name that will be shown in the header. If displayName is not
6505 * provided then one is generated using the name.
6512 * @propertyOf ui.grid.class:GridColumn
6513 * @description field must be provided if you wish to bind to a
6514 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6515 * array element. Can be a complex expression: <code>employee.address.city</code>, or can be a function: <code>employee.getFullAddress()</code>.
6516 * See the angular docs on binding expressions.
6523 * @propertyOf ui.grid.class:GridOptions.columnDef
6524 * @description field must be provided if you wish to bind to a
6525 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6526 * 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. *
6532 * @propertyOf ui.grid.class:GridColumn
6533 * @description Filter on this column.
6535 * <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>
6541 * @name ui.grid.class:GridColumn
6542 * @description Represents the viewModel for each column. Any state or methods needed for a Grid Column
6543 * are defined on this prototype
6544 * @param {ColumnDef} colDef the column def to associate with this column
6545 * @param {number} uid the unique and immutable uid we'd like to allocate to this column
6546 * @param {Grid} grid the grid we'd like to create this column in
6548 function GridColumn(colDef, uid, grid) {
6554 self.updateColumnDef(colDef, true);
6556 self.aggregationValue = undefined;
6558 // The footer cell registers to listen for the rowsRendered event, and calls this. Needed to be
6559 // in something with a scope so that the dereg would get called
6560 self.updateAggregationValue = function() {
6562 // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name);
6566 * @name aggregationType
6567 * @propertyOf ui.grid.class:GridOptions.columnDef
6568 * @description The aggregation that you'd like to show in the columnFooter for this
6569 * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`,
6570 * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`,
6571 * `uiGridConstants.aggregationTypes.max`.
6573 * You can also provide a function as the aggregation type, in this case your function needs to accept the full
6574 * set of visible rows, and return a value that should be shown
6576 if (!self.aggregationType) {
6577 self.aggregationValue = undefined;
6582 var visibleRows = self.grid.getVisibleRows();
6584 var cellValues = function(){
6586 visibleRows.forEach(function (row) {
6587 var cellValue = self.grid.getCellValue(row, self);
6588 var cellNumber = Number(cellValue);
6589 if (!isNaN(cellNumber)) {
6590 values.push(cellNumber);
6596 if (angular.isFunction(self.aggregationType)) {
6597 self.aggregationValue = self.aggregationType(visibleRows, self);
6599 else if (self.aggregationType === uiGridConstants.aggregationTypes.count) {
6600 self.aggregationValue = self.grid.getVisibleRowCount();
6602 else if (self.aggregationType === uiGridConstants.aggregationTypes.sum) {
6603 cellValues().forEach(function (value) {
6606 self.aggregationValue = result;
6608 else if (self.aggregationType === uiGridConstants.aggregationTypes.avg) {
6609 cellValues().forEach(function (value) {
6612 result = result / cellValues().length;
6613 self.aggregationValue = result;
6615 else if (self.aggregationType === uiGridConstants.aggregationTypes.min) {
6616 self.aggregationValue = Math.min.apply(null, cellValues());
6618 else if (self.aggregationType === uiGridConstants.aggregationTypes.max) {
6619 self.aggregationValue = Math.max.apply(null, cellValues());
6622 self.aggregationValue = '\u00A0';
6626 // var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true, context: self.name });
6630 * @name getAggregationValue
6631 * @methodOf ui.grid.class:GridColumn
6632 * @description gets the aggregation value based on the aggregation type for this column.
6633 * Debounced using scrollDebounce option setting
6635 this.getAggregationValue = function() {
6636 // if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) {
6637 // throttledUpdateAggregationValue();
6640 return self.aggregationValue;
6647 * @methodOf ui.grid.class:GridColumn
6648 * @description Hides the column by setting colDef.visible = false
6650 GridColumn.prototype.hideColumn = function() {
6651 this.colDef.visible = false;
6657 * @methodOf ui.grid.class:GridColumn
6658 * @name setPropertyOrDefault
6659 * @description Sets a property on the column using the passed in columnDef, and
6660 * setting the defaultValue if the value cannot be found on the colDef
6661 * @param {ColumnDef} colDef the column def to look in for the property value
6662 * @param {string} propName the property name we'd like to set
6663 * @param {object} defaultValue the value to use if the colDef doesn't provide the setting
6665 GridColumn.prototype.setPropertyOrDefault = function (colDef, propName, defaultValue) {
6668 // Use the column definition filter if we were passed it
6669 if (typeof(colDef[propName]) !== 'undefined' && colDef[propName]) {
6670 self[propName] = colDef[propName];
6672 // Otherwise use our own if it's set
6673 else if (typeof(self[propName]) !== 'undefined') {
6674 self[propName] = self[propName];
6676 // Default to empty object for the filter
6678 self[propName] = defaultValue ? defaultValue : {};
6687 * @propertyOf ui.grid.class:GridOptions.columnDef
6688 * @description sets the column width. Can be either
6689 * a number or a percentage, or an * for auto.
6691 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', width: 100},
6692 * { field: 'field2', width: '20%'},
6693 * { field: 'field3', width: '*' }]; </pre>
6700 * @propertyOf ui.grid.class:GridOptions.columnDef
6701 * @description sets the minimum column width. Should be a number.
6703 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', minWidth: 100}]; </pre>
6710 * @propertyOf ui.grid.class:GridOptions.columnDef
6711 * @description sets the maximum column width. Should be a number.
6713 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', maxWidth: 100}]; </pre>
6720 * @propertyOf ui.grid.class:GridOptions.columnDef
6721 * @description sets whether or not the column is visible
6722 * </br>Default is true
6724 * <pre> $scope.gridOptions.columnDefs = [
6725 * { field: 'field1', visible: true},
6726 * { field: 'field2', visible: false }
6734 * @propertyOf ui.grid.class:GridOptions.columnDef
6735 * @description An object of sort information, attributes are:
6737 * - direction: values are uiGridConstants.ASC or uiGridConstants.DESC
6738 * - ignoreSort: if set to true this sort is ignored (used by tree to manipulate the sort functionality)
6739 * - priority: says what order to sort the columns in (lower priority gets sorted first).
6742 * $scope.gridOptions.columnDefs = [{
6745 * direction: uiGridConstants.ASC,
6756 * @name sortingAlgorithm
6757 * @propertyOf ui.grid.class:GridOptions.columnDef
6758 * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters
6759 * like any normal sorting function with additional 'rowA', 'rowB', and 'direction' parameters
6760 * that are the row objects and the current direction of the sort respectively.
6767 * @propertyOf ui.grid.class:GridOptions.columnDef
6768 * @description Specify multiple filter fields.
6770 * <pre>$scope.gridOptions.columnDefs = [
6772 * field: 'field1', filters: [
6775 * condition: uiGridConstants.filter.STARTS_WITH,
6776 * placeholder: 'starts with...',
6777 * ariaLabel: 'Filter for field1',
6778 * flags: { caseSensitive: false },
6779 * type: uiGridConstants.filter.SELECT,
6780 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6783 * condition: uiGridConstants.filter.ENDS_WITH,
6784 * placeholder: 'ends with...'
6796 * @propertyOf ui.grid.class:GridColumn
6797 * @description Filters for this column. Includes 'term' property bound to filter input elements.
6801 * term: 'foo', // ngModel for <input>
6802 * condition: uiGridConstants.filter.STARTS_WITH,
6803 * placeholder: 'starts with...',
6804 * ariaLabel: 'Filter for foo',
6805 * flags: { caseSensitive: false },
6806 * type: uiGridConstants.filter.SELECT,
6807 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6811 * condition: uiGridConstants.filter.ENDS_WITH,
6812 * placeholder: 'ends with...'
6822 * @propertyOf ui.grid.class:GridOptions.columnDef
6823 * @description used to add menu items to a column. Refer to the tutorial on this
6824 * functionality. A number of settings are supported:
6826 * - title: controls the title that is displayed in the menu
6827 * - icon: the icon shown alongside that title
6828 * - action: the method to call when the menu is clicked
6829 * - shown: a function to evaluate to determine whether or not to show the item
6830 * - active: a function to evaluate to determine whether or not the item is currently selected
6831 * - context: context to pass to the action function, available in this.context in your handler
6832 * - leaveOpen: if set to true, the menu should stay open after the action, defaults to false
6834 * <pre> $scope.gridOptions.columnDefs = [
6835 * { field: 'field1', menuItems: [
6837 * title: 'Outer Scope Alert',
6838 * icon: 'ui-grid-icon-info-circled',
6839 * action: function($event) {
6840 * this.context.blargh(); // $scope.blargh() would work too, this is just an example
6842 * shown: function() { return true; },
6843 * active: function() { return true; },
6848 * action: function() {
6849 * alert('Grid ID: ' + this.grid.id);
6858 * @methodOf ui.grid.class:GridColumn
6859 * @name updateColumnDef
6860 * @description Moves settings from the columnDef down onto the column,
6861 * and sets properties as appropriate
6862 * @param {ColumnDef} colDef the column def to look in for the property value
6863 * @param {boolean} isNew whether the column is being newly created, if not
6864 * we're updating an existing column, and some items such as the sort shouldn't
6867 GridColumn.prototype.updateColumnDef = function(colDef, isNew) {
6870 self.colDef = colDef;
6872 if (colDef.name === undefined) {
6873 throw new Error('colDef.name is required for column at index ' + self.grid.options.columnDefs.indexOf(colDef));
6876 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6878 if (!angular.isNumber(self.width) || !self.hasCustomWidth || colDef.allowCustomWidthOverride) {
6879 var colDefWidth = colDef.width;
6880 var parseErrorMsg = "Cannot parse column width '" + colDefWidth + "' for column named '" + colDef.name + "'";
6881 self.hasCustomWidth = false;
6883 if (!angular.isString(colDefWidth) && !angular.isNumber(colDefWidth)) {
6885 } else if (angular.isString(colDefWidth)) {
6886 // See if it ends with a percent
6887 if (gridUtil.endsWith(colDefWidth, '%')) {
6888 // If so we should be able to parse the non-percent-sign part to a number
6889 var percentStr = colDefWidth.replace(/%/g, '');
6890 var percent = parseInt(percentStr, 10);
6891 if (isNaN(percent)) {
6892 throw new Error(parseErrorMsg);
6894 self.width = colDefWidth;
6896 // And see if it's a number string
6897 else if (colDefWidth.match(/^(\d+)$/)) {
6898 self.width = parseInt(colDefWidth.match(/^(\d+)$/)[1], 10);
6900 // Otherwise it should be a string of asterisks
6901 else if (colDefWidth.match(/^\*+$/)) {
6902 self.width = colDefWidth;
6904 // No idea, throw an Error
6906 throw new Error(parseErrorMsg);
6909 // Is a number, use it as the width
6911 self.width = colDefWidth;
6915 ['minWidth', 'maxWidth'].forEach(function (name) {
6916 var minOrMaxWidth = colDef[name];
6917 var parseErrorMsg = "Cannot parse column " + name + " '" + minOrMaxWidth + "' for column named '" + colDef.name + "'";
6919 if (!angular.isString(minOrMaxWidth) && !angular.isNumber(minOrMaxWidth)) {
6920 //Sets default minWidth and maxWidth values
6921 self[name] = ((name === 'minWidth') ? 30 : 9000);
6922 } else if (angular.isString(minOrMaxWidth)) {
6923 if (minOrMaxWidth.match(/^(\d+)$/)) {
6924 self[name] = parseInt(minOrMaxWidth.match(/^(\d+)$/)[1], 10);
6926 throw new Error(parseErrorMsg);
6929 self[name] = minOrMaxWidth;
6933 //use field if it is defined; name if it is not
6934 self.field = (colDef.field === undefined) ? colDef.name : colDef.field;
6936 if ( typeof( self.field ) !== 'string' ){
6937 gridUtil.logError( 'Field is not a string, this is likely to break the code, Field is: ' + self.field );
6940 self.name = colDef.name;
6942 // Use colDef.displayName as long as it's not undefined, otherwise default to the field name
6943 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6945 //self.originalIndex = index;
6947 self.aggregationType = angular.isDefined(colDef.aggregationType) ? colDef.aggregationType : null;
6948 self.footerCellTemplate = angular.isDefined(colDef.footerCellTemplate) ? colDef.footerCellTemplate : null;
6953 * @propertyOf ui.grid.class:GridOptions.columnDef
6954 * @description Whether or not to show a tooltip when a user hovers over the cell.
6955 * If set to false, no tooltip. If true, the cell value is shown in the tooltip (useful
6956 * if you have long values in your cells), if a function then that function is called
6957 * passing in the row and the col `cellTooltip( row, col )`, and the return value is shown in the tooltip,
6958 * if it is a static string then displays that static string.
6963 if ( typeof(colDef.cellTooltip) === 'undefined' || colDef.cellTooltip === false ) {
6964 self.cellTooltip = false;
6965 } else if ( colDef.cellTooltip === true ){
6966 self.cellTooltip = function(row, col) {
6967 return self.grid.getCellValue( row, col );
6969 } else if (typeof(colDef.cellTooltip) === 'function' ){
6970 self.cellTooltip = colDef.cellTooltip;
6972 self.cellTooltip = function ( row, col ){
6973 return col.colDef.cellTooltip;
6979 * @name headerTooltip
6980 * @propertyOf ui.grid.class:GridOptions.columnDef
6981 * @description Whether or not to show a tooltip when a user hovers over the header cell.
6982 * If set to false, no tooltip. If true, the displayName is shown in the tooltip (useful
6983 * if you have long values in your headers), if a function then that function is called
6984 * passing in the row and the col `headerTooltip( col )`, and the return value is shown in the tooltip,
6985 * if a static string then shows that static string.
6990 if ( typeof(colDef.headerTooltip) === 'undefined' || colDef.headerTooltip === false ) {
6991 self.headerTooltip = false;
6992 } else if ( colDef.headerTooltip === true ){
6993 self.headerTooltip = function(col) {
6994 return col.displayName;
6996 } else if (typeof(colDef.headerTooltip) === 'function' ){
6997 self.headerTooltip = colDef.headerTooltip;
6999 self.headerTooltip = function ( col ) {
7000 return col.colDef.headerTooltip;
7007 * @name footerCellClass
7008 * @propertyOf ui.grid.class:GridOptions.columnDef
7009 * @description footerCellClass can be a string specifying the class to append to a cell
7010 * or it can be a function(grid, row, col, rowRenderIndex, colRenderIndex) that returns a class name
7013 self.footerCellClass = colDef.footerCellClass;
7018 * @propertyOf ui.grid.class:GridOptions.columnDef
7019 * @description cellClass can be a string specifying the class to append to a cell
7020 * or it can be a function(grid, row, col, rowRenderIndex, colRenderIndex) that returns a class name
7023 self.cellClass = colDef.cellClass;
7027 * @name headerCellClass
7028 * @propertyOf ui.grid.class:GridOptions.columnDef
7029 * @description headerCellClass can be a string specifying the class to append to a cell
7030 * or it can be a function(grid, row, col, rowRenderIndex, colRenderIndex) that returns a class name
7033 self.headerCellClass = colDef.headerCellClass;
7038 * @propertyOf ui.grid.class:GridOptions.columnDef
7039 * @description cellFilter is a filter to apply to the content of each cell
7042 * gridOptions.columnDefs[0].cellFilter = 'date'
7045 self.cellFilter = colDef.cellFilter ? colDef.cellFilter : "";
7049 * @name sortCellFiltered
7050 * @propertyOf ui.grid.class:GridOptions.columnDef
7051 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7052 * sorting the data. Note that when using this option uiGrid will assume that the displayed value is
7053 * a string, and use the {@link ui.grid.class:RowSorter#sortAlpha sortAlpha} `sortFn`. It is possible
7054 * 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}
7055 * for the column which hanldes the returned type. You may specify one of the `sortingAlgorithms`
7056 * found in the {@link ui.grid.RowSorter rowSorter} service.
7058 self.sortCellFiltered = colDef.sortCellFiltered ? true : false;
7062 * @name filterCellFiltered
7063 * @propertyOf ui.grid.class:GridOptions.columnDef
7064 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7065 * applying "search" `filters`.
7067 self.filterCellFiltered = colDef.filterCellFiltered ? true : false;
7071 * @name headerCellFilter
7072 * @propertyOf ui.grid.class:GridOptions.columnDef
7073 * @description headerCellFilter is a filter to apply to the content of the column header
7076 * gridOptions.columnDefs[0].headerCellFilter = 'translate'
7079 self.headerCellFilter = colDef.headerCellFilter ? colDef.headerCellFilter : "";
7083 * @name footerCellFilter
7084 * @propertyOf ui.grid.class:GridOptions.columnDef
7085 * @description footerCellFilter is a filter to apply to the content of the column footer
7088 * gridOptions.columnDefs[0].footerCellFilter = 'date'
7091 self.footerCellFilter = colDef.footerCellFilter ? colDef.footerCellFilter : "";
7093 self.visible = gridUtil.isNullOrUndefined(colDef.visible) || colDef.visible;
7095 self.headerClass = colDef.headerClass;
7096 //self.cursor = self.sortable ? 'pointer' : 'default';
7098 // Turn on sorting by default
7099 self.enableSorting = typeof(colDef.enableSorting) !== 'undefined' ? colDef.enableSorting : true;
7100 self.sortingAlgorithm = colDef.sortingAlgorithm;
7104 * @name sortDirectionCycle
7105 * @propertyOf ui.grid.class:GridOptions.columnDef
7106 * @description (optional) An array of sort directions, specifying the order that they
7107 * should cycle through as the user repeatedly clicks on the column heading.
7108 * The default is `[null, uiGridConstants.ASC, uiGridConstants.DESC]`. Null
7109 * refers to the unsorted state. This does not affect the initial sort
7110 * direction; use the {@link ui.grid.class:GridOptions.columnDef#sort sort}
7111 * property for that. If
7112 * {@link ui.grid.class:GridOptions.columnDef#suppressRemoveSort suppressRemoveSort}
7113 * is also set, the unsorted state will be skipped even if it is listed here.
7114 * Each direction may not appear in the list more than once (e.g. `[ASC,
7115 * DESC, DESC]` is not allowed), and the list may not be empty.
7117 self.sortDirectionCycle = typeof(colDef.sortDirectionCycle) !== 'undefined' ?
7118 colDef.sortDirectionCycle :
7119 [null, uiGridConstants.ASC, uiGridConstants.DESC];
7123 * @name suppressRemoveSort
7124 * @propertyOf ui.grid.class:GridOptions.columnDef
7125 * @description (optional) False by default. When enabled, this setting hides the removeSort option
7126 * in the menu, and prevents users from manually removing the sort
7128 if ( typeof(self.suppressRemoveSort) === 'undefined'){
7129 self.suppressRemoveSort = typeof(colDef.suppressRemoveSort) !== 'undefined' ? colDef.suppressRemoveSort : false;
7134 * @name enableFiltering
7135 * @propertyOf ui.grid.class:GridOptions.columnDef
7136 * @description turn off filtering for an individual column, where
7137 * you've turned on filtering for the overall grid
7140 * gridOptions.columnDefs[0].enableFiltering = false;
7143 // Turn on filtering by default (it's disabled by default at the Grid level)
7144 self.enableFiltering = typeof(colDef.enableFiltering) !== 'undefined' ? colDef.enableFiltering : true;
7146 // self.menuItems = colDef.menuItems;
7147 self.setPropertyOrDefault(colDef, 'menuItems', []);
7149 // Use the column definition sort if we were passed it, but only if this is a newly added column
7151 self.setPropertyOrDefault(colDef, 'sort');
7154 // Set up default filters array for when one is not provided.
7155 // In other words, this (in column def):
7157 // filter: { term: 'something', flags: {}, condition: [CONDITION] }
7159 // is just shorthand for this:
7161 // filters: [{ term: 'something', flags: {}, condition: [CONDITION] }]
7163 var defaultFilters = [];
7164 if (colDef.filter) {
7165 defaultFilters.push(colDef.filter);
7167 else if ( colDef.filters ){
7168 defaultFilters = colDef.filters;
7170 // Add an empty filter definition object, which will
7171 // translate to a guessed condition and no pre-populated
7172 // value for the filter <input>.
7173 defaultFilters.push({});
7179 * @propertyOf ui.grid.class:GridOptions.columnDef
7180 * @description Specify a single filter field on this column.
7182 * A filter consists of a condition, a term, and a placeholder:
7184 * - condition defines how rows are chosen as matching the filter term. This can be set to
7185 * one of the constants in uiGridConstants.filter, or you can supply a custom filter function
7186 * that gets passed the following arguments: [searchTerm, cellValue, row, column].
7187 * - term: If set, the filter field will be pre-populated
7189 * - placeholder: String that will be set to the `<input>.placeholder` attribute.
7190 * - ariaLabel: String that will be set to the `<input>.ariaLabel` attribute. This is what is read as a label to screen reader users.
7191 * - noTerm: set this to true if you have defined a custom function in condition, and
7192 * your custom function doesn't require a term (so it can run even when the term is null)
7193 * - flags: only flag currently available is `caseSensitive`, set to false if you don't want
7194 * case sensitive matching
7195 * - type: defaults to uiGridConstants.filter.INPUT, which gives a text box. If set to uiGridConstants.filter.SELECT
7196 * then a select box will be shown with options selectOptions
7197 * - selectOptions: options in the format `[ { value: 1, label: 'male' }]`. No i18n filter is provided, you need
7198 * to perform the i18n on the values before you provide them
7199 * - disableCancelFilterButton: defaults to false. If set to true then the 'x' button that cancels/clears the filter
7200 * will not be shown.
7202 * <pre>$scope.gridOptions.columnDefs = [
7207 * condition: uiGridConstants.filter.STARTS_WITH,
7208 * placeholder: 'starts with...',
7209 * ariaLabel: 'Starts with filter for field1',
7210 * flags: { caseSensitive: false },
7211 * type: uiGridConstants.filter.SELECT,
7212 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ],
7213 * disableCancelFilterButton: true
7228 condition: uiGridConstants.filter.CONTAINS,
7229 placeholder: 'my placeholder',
7230 ariaLabel: 'Starts with filter for field1',
7239 // Only set filter if this is a newly added column, if we're updating an existing
7240 // column then we don't want to put the default filter back if the user may have already
7242 // However, we do want to keep the settings if they change, just not the term
7244 self.setPropertyOrDefault(colDef, 'filter');
7245 self.setPropertyOrDefault(colDef, 'filters', defaultFilters);
7246 } else if ( self.filters.length === defaultFilters.length ) {
7247 self.filters.forEach( function( filter, index ){
7248 if (typeof(defaultFilters[index].placeholder) !== 'undefined') {
7249 filter.placeholder = defaultFilters[index].placeholder;
7251 if (typeof(defaultFilters[index].ariaLabel) !== 'undefined') {
7252 filter.ariaLabel = defaultFilters[index].ariaLabel;
7254 if (typeof(defaultFilters[index].flags) !== 'undefined') {
7255 filter.flags = defaultFilters[index].flags;
7257 if (typeof(defaultFilters[index].type) !== 'undefined') {
7258 filter.type = defaultFilters[index].type;
7260 if (typeof(defaultFilters[index].selectOptions) !== 'undefined') {
7261 filter.selectOptions = defaultFilters[index].selectOptions;
7270 * @methodOf ui.grid.class:GridColumn
7271 * @description Removes column from the grid sorting
7273 GridColumn.prototype.unsort = function () {
7275 this.grid.api.core.raise.sortChanged( this.grid, this.grid.getColumnSorting() );
7282 * @methodOf ui.grid.class:GridColumn
7283 * @description Returns the class name for the column
7284 * @param {bool} prefixDot if true, will return .className instead of className
7286 GridColumn.prototype.getColClass = function (prefixDot) {
7287 var cls = uiGridConstants.COL_CLASS_PREFIX + this.uid;
7289 return prefixDot ? '.' + cls : cls;
7294 * @name isPinnedLeft
7295 * @methodOf ui.grid.class:GridColumn
7296 * @description Returns true if column is in the left render container
7298 GridColumn.prototype.isPinnedLeft = function () {
7299 return this.renderContainer === 'left';
7304 * @name isPinnedRight
7305 * @methodOf ui.grid.class:GridColumn
7306 * @description Returns true if column is in the right render container
7308 GridColumn.prototype.isPinnedRight = function () {
7309 return this.renderContainer === 'right';
7315 * @name getColClassDefinition
7316 * @methodOf ui.grid.class:GridColumn
7317 * @description Returns the class definition for th column
7319 GridColumn.prototype.getColClassDefinition = function () {
7320 return ' .grid' + this.grid.id + ' ' + this.getColClass(true) + ' { min-width: ' + this.drawnWidth + 'px; max-width: ' + this.drawnWidth + 'px; }';
7325 * @name getRenderContainer
7326 * @methodOf ui.grid.class:GridColumn
7327 * @description Returns the render container object that this column belongs to.
7329 * Columns will be default be in the `body` render container if they aren't allocated to one specifically.
7331 GridColumn.prototype.getRenderContainer = function getRenderContainer() {
7334 var containerId = self.renderContainer;
7336 if (containerId === null || containerId === '' || containerId === undefined) {
7337 containerId = 'body';
7340 return self.grid.renderContainers[containerId];
7346 * @methodOf ui.grid.class:GridColumn
7347 * @description Makes the column visible by setting colDef.visible = true
7349 GridColumn.prototype.showColumn = function() {
7350 this.colDef.visible = true;
7356 * @name aggregationHideLabel
7357 * @propertyOf ui.grid.class:GridOptions.columnDef
7358 * @description defaults to false, if set to true hides the label text
7359 * in the aggregation footer, so only the value is displayed.
7364 * @name getAggregationText
7365 * @methodOf ui.grid.class:GridColumn
7366 * @description Gets the aggregation label from colDef.aggregationLabel if
7367 * specified or by using i18n, including deciding whether or not to display
7368 * based on colDef.aggregationHideLabel.
7370 * @param {string} label the i18n lookup value to use for the column label
7373 GridColumn.prototype.getAggregationText = function () {
7375 if ( self.colDef.aggregationHideLabel ){
7378 else if ( self.colDef.aggregationLabel ) {
7379 return self.colDef.aggregationLabel;
7382 switch ( self.colDef.aggregationType ){
7383 case uiGridConstants.aggregationTypes.count:
7384 return i18nService.getSafeText('aggregation.count');
7385 case uiGridConstants.aggregationTypes.sum:
7386 return i18nService.getSafeText('aggregation.sum');
7387 case uiGridConstants.aggregationTypes.avg:
7388 return i18nService.getSafeText('aggregation.avg');
7389 case uiGridConstants.aggregationTypes.min:
7390 return i18nService.getSafeText('aggregation.min');
7391 case uiGridConstants.aggregationTypes.max:
7392 return i18nService.getSafeText('aggregation.max');
7399 GridColumn.prototype.getCellTemplate = function () {
7402 return self.cellTemplatePromise;
7405 GridColumn.prototype.getCompiledElementFn = function () {
7408 return self.compiledElementFnDefer.promise;
7418 angular.module('ui.grid')
7419 .factory('GridOptions', ['gridUtil','uiGridConstants', function(gridUtil,uiGridConstants) {
7423 * @name ui.grid.class:GridOptions
7424 * @description Default GridOptions class. GridOptions are defined by the application developer and overlaid
7425 * over this object. Setting gridOptions within your controller is the most common method for an application
7426 * developer to configure the behaviour of their ui-grid
7428 * @example To define your gridOptions within your controller:
7429 * <pre>$scope.gridOptions = {
7430 * data: $scope.myData,
7432 * { name: 'field1', displayName: 'pretty display name' },
7433 * { name: 'field2', visible: false }
7437 * You can then use this within your html template, when you define your grid:
7438 * <pre><div ui-grid="gridOptions"></div></pre>
7440 * To provide default options for all of the grids within your application, use an angular
7441 * decorator to modify the GridOptions factory.
7443 * app.config(function($provide){
7444 * $provide.decorator('GridOptions',function($delegate){
7446 * gridOptions = angular.copy($delegate);
7447 * gridOptions.initialize = function(options) {
7449 * initOptions = $delegate.initialize(options);
7450 * initOptions.enableColumnMenus = false;
7451 * return initOptions;
7453 * return gridOptions;
7459 initialize: function( baseOptions ){
7462 * @name onRegisterApi
7463 * @propertyOf ui.grid.class:GridOptions
7464 * @description A callback that returns the gridApi once the grid is instantiated, which is
7465 * then used to interact with the grid programatically.
7467 * Note that the gridApi.core.renderingComplete event is identical to this
7468 * callback, but has the advantage that it can be called from multiple places
7473 * $scope.gridOptions.onRegisterApi = function ( gridApi ) {
7474 * $scope.gridApi = gridApi;
7475 * $scope.gridApi.selection.selectAllRows( $scope.gridApi.grid );
7480 baseOptions.onRegisterApi = baseOptions.onRegisterApi || angular.noop();
7485 * @propertyOf ui.grid.class:GridOptions
7486 * @description (mandatory) Array of data to be rendered into the grid, providing the data source or data binding for
7489 * Most commonly the data is an array of objects, where each object has a number of attributes.
7490 * Each attribute automatically becomes a column in your grid. This array could, for example, be sourced from
7491 * an angularJS $resource query request. The array can also contain complex objects, refer the binding tutorial
7492 * for examples of that.
7494 * The most flexible usage is to set your data on $scope:
7496 * `$scope.data = data;`
7498 * And then direct the grid to resolve whatever is in $scope.data:
7500 * `$scope.gridOptions.data = 'data';`
7502 * This is the most flexible approach as it allows you to replace $scope.data whenever you feel like it without
7503 * getting pointer issues.
7505 * Alternatively you can directly set the data array:
7507 * `$scope.gridOptions.data = [ ];`
7510 * `$http.get('/data/100.json')
7511 * .success(function(data) {
7512 * $scope.myData = data;
7513 * $scope.gridOptions.data = $scope.myData;
7516 * Where you do this, you need to take care in updating the data - you can't just update `$scope.myData` to some other
7517 * array, you need to update $scope.gridOptions.data to point to that new array as well.
7520 baseOptions.data = baseOptions.data || [];
7525 * @propertyOf ui.grid.class:GridOptions
7526 * @description Array of columnDef objects. Only required property is name.
7527 * The individual options available in columnDefs are documented in the
7528 * {@link ui.grid.class:GridOptions.columnDef columnDef} section
7529 * </br>_field property can be used in place of name for backwards compatibility with 2.x_
7532 * <pre>var columnDefs = [{name:'field1'}, {name:'field2'}];</pre>
7535 baseOptions.columnDefs = baseOptions.columnDefs || [];
7539 * @name ui.grid.class:GridOptions.columnDef
7540 * @description Definition / configuration of an individual column, which would typically be
7541 * one of many column definitions within the gridOptions.columnDefs array
7543 * <pre>{name:'field1', field: 'field1', filter: { term: 'xxx' }}</pre>
7550 * @name excludeProperties
7551 * @propertyOf ui.grid.class:GridOptions
7552 * @description Array of property names in data to ignore when auto-generating column names. Provides the
7553 * inverse of columnDefs - columnDefs is a list of columns to include, excludeProperties is a list of columns
7556 * If columnDefs is defined, this will be ignored.
7558 * Defaults to ['$$hashKey']
7561 baseOptions.excludeProperties = baseOptions.excludeProperties || ['$$hashKey'];
7565 * @name enableRowHashing
7566 * @propertyOf ui.grid.class:GridOptions
7567 * @description True by default. When enabled, this setting allows uiGrid to add
7568 * `$$hashKey`-type properties (similar to Angular) to elements in the `data` array. This allows
7569 * the grid to maintain state while vastly speeding up the process of altering `data` by adding/moving/removing rows.
7571 * Note that this DOES add properties to your data that you may not want, but they are stripped out when using `angular.toJson()`. IF
7572 * 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
7573 * and are altering the data set often.
7575 baseOptions.enableRowHashing = baseOptions.enableRowHashing !== false;
7580 * @methodOf ui.grid.class:GridOptions
7581 * @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).
7583 * By default it returns the `$$hashKey` property if it exists. If it doesn't it uses gridUtil.nextUid() to generate one
7585 baseOptions.rowIdentity = baseOptions.rowIdentity || function rowIdentity(row) {
7586 return gridUtil.hashKey(row);
7591 * @name getRowIdentity
7592 * @methodOf ui.grid.class:GridOptions
7593 * @description This function returns the identity value uniquely identifying this row, if one is not present it does not set it.
7595 * By default it returns the `$$hashKey` property but can be overridden to use any property or set of properties you want.
7597 baseOptions.getRowIdentity = baseOptions.getRowIdentity || function getRowIdentity(row) {
7598 return row.$$hashKey;
7603 * @name flatEntityAccess
7604 * @propertyOf ui.grid.class:GridOptions
7605 * @description Set to true if your columns are all related directly to fields in a flat object structure - i.e.
7606 * each of your columns associate directly with a property on each of the entities in your data array.
7608 * In that situation we can avoid all the logic associated with complex binding to functions or to properties of sub-objects,
7609 * which can provide a significant speed improvement with large data sets when filtering or sorting.
7613 baseOptions.flatEntityAccess = baseOptions.flatEntityAccess === true;
7618 * @propertyOf ui.grid.class:GridOptions
7619 * @description True by default. When set to false, this setting will replace the
7620 * standard header template with '<div></div>', resulting in no header being shown.
7622 baseOptions.showHeader = typeof(baseOptions.showHeader) !== "undefined" ? baseOptions.showHeader : true;
7624 /* (NOTE): Don't show this in the docs. We only use it internally
7626 * @name headerRowHeight
7627 * @propertyOf ui.grid.class:GridOptions
7628 * @description The height of the header in pixels, defaults to 30
7631 if (!baseOptions.showHeader) {
7632 baseOptions.headerRowHeight = 0;
7635 baseOptions.headerRowHeight = typeof(baseOptions.headerRowHeight) !== "undefined" ? baseOptions.headerRowHeight : 30;
7641 * @propertyOf ui.grid.class:GridOptions
7642 * @description The height of the row in pixels, defaults to 30
7645 baseOptions.rowHeight = baseOptions.rowHeight || 30;
7649 * @name minRowsToShow
7650 * @propertyOf ui.grid.class:GridOptions
7651 * @description Minimum number of rows to show when the grid doesn't have a defined height. Defaults to "10".
7653 baseOptions.minRowsToShow = typeof(baseOptions.minRowsToShow) !== "undefined" ? baseOptions.minRowsToShow : 10;
7657 * @name showGridFooter
7658 * @propertyOf ui.grid.class:GridOptions
7659 * @description Whether or not to show the footer, defaults to false
7660 * The footer display Total Rows and Visible Rows (filtered rows)
7662 baseOptions.showGridFooter = baseOptions.showGridFooter === true;
7666 * @name showColumnFooter
7667 * @propertyOf ui.grid.class:GridOptions
7668 * @description Whether or not to show the column footer, defaults to false
7669 * The column footer displays column aggregates
7671 baseOptions.showColumnFooter = baseOptions.showColumnFooter === true;
7675 * @name columnFooterHeight
7676 * @propertyOf ui.grid.class:GridOptions
7677 * @description The height of the footer rows (column footer and grid footer) in pixels
7680 baseOptions.columnFooterHeight = typeof(baseOptions.columnFooterHeight) !== "undefined" ? baseOptions.columnFooterHeight : 30;
7681 baseOptions.gridFooterHeight = typeof(baseOptions.gridFooterHeight) !== "undefined" ? baseOptions.gridFooterHeight : 30;
7683 baseOptions.columnWidth = typeof(baseOptions.columnWidth) !== "undefined" ? baseOptions.columnWidth : 50;
7687 * @name maxVisibleColumnCount
7688 * @propertyOf ui.grid.class:GridOptions
7689 * @description Defaults to 200
7692 baseOptions.maxVisibleColumnCount = typeof(baseOptions.maxVisibleColumnCount) !== "undefined" ? baseOptions.maxVisibleColumnCount : 200;
7696 * @name virtualizationThreshold
7697 * @propertyOf ui.grid.class:GridOptions
7698 * @description Turn virtualization on when number of data elements goes over this number, defaults to 20
7700 baseOptions.virtualizationThreshold = typeof(baseOptions.virtualizationThreshold) !== "undefined" ? baseOptions.virtualizationThreshold : 20;
7704 * @name columnVirtualizationThreshold
7705 * @propertyOf ui.grid.class:GridOptions
7706 * @description Turn virtualization on when number of columns goes over this number, defaults to 10
7708 baseOptions.columnVirtualizationThreshold = typeof(baseOptions.columnVirtualizationThreshold) !== "undefined" ? baseOptions.columnVirtualizationThreshold : 10;
7713 * @propertyOf ui.grid.class:GridOptions
7714 * @description Extra rows to to render outside of the viewport, which helps with smoothness of scrolling.
7717 baseOptions.excessRows = typeof(baseOptions.excessRows) !== "undefined" ? baseOptions.excessRows : 4;
7720 * @name scrollThreshold
7721 * @propertyOf ui.grid.class:GridOptions
7722 * @description Defaults to 4
7724 baseOptions.scrollThreshold = typeof(baseOptions.scrollThreshold) !== "undefined" ? baseOptions.scrollThreshold : 4;
7728 * @name excessColumns
7729 * @propertyOf ui.grid.class:GridOptions
7730 * @description Extra columns to to render outside of the viewport, which helps with smoothness of scrolling.
7733 baseOptions.excessColumns = typeof(baseOptions.excessColumns) !== "undefined" ? baseOptions.excessColumns : 4;
7736 * @name horizontalScrollThreshold
7737 * @propertyOf ui.grid.class:GridOptions
7738 * @description Defaults to 4
7740 baseOptions.horizontalScrollThreshold = typeof(baseOptions.horizontalScrollThreshold) !== "undefined" ? baseOptions.horizontalScrollThreshold : 2;
7745 * @name aggregationCalcThrottle
7746 * @propertyOf ui.grid.class:GridOptions
7747 * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 500ms
7749 baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 500;
7753 * @name wheelScrollThrottle
7754 * @propertyOf ui.grid.class:GridOptions
7755 * @description Default time in milliseconds to throttle scroll events to, defaults to 70ms
7757 baseOptions.wheelScrollThrottle = typeof(baseOptions.wheelScrollThrottle) !== "undefined" ? baseOptions.wheelScrollThrottle : 70;
7762 * @name scrollDebounce
7763 * @propertyOf ui.grid.class:GridOptions
7764 * @description Default time in milliseconds to debounce scroll events, defaults to 300ms
7766 baseOptions.scrollDebounce = typeof(baseOptions.scrollDebounce) !== "undefined" ? baseOptions.scrollDebounce : 300;
7770 * @name enableSorting
7771 * @propertyOf ui.grid.class:GridOptions
7772 * @description True by default. When enabled, this setting adds sort
7773 * widgets to the column headers, allowing sorting of the data for the entire grid.
7774 * Sorting can then be disabled on individual columns using the columnDefs.
7776 baseOptions.enableSorting = baseOptions.enableSorting !== false;
7780 * @name enableFiltering
7781 * @propertyOf ui.grid.class:GridOptions
7782 * @description False by default. When enabled, this setting adds filter
7783 * boxes to each column header, allowing filtering within the column for the entire grid.
7784 * Filtering can then be disabled on individual columns using the columnDefs.
7786 baseOptions.enableFiltering = baseOptions.enableFiltering === true;
7790 * @name enableColumnMenus
7791 * @propertyOf ui.grid.class:GridOptions
7792 * @description True by default. When enabled, this setting displays a column
7793 * menu within each column.
7795 baseOptions.enableColumnMenus = baseOptions.enableColumnMenus !== false;
7799 * @name enableVerticalScrollbar
7800 * @propertyOf ui.grid.class:GridOptions
7801 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the vertical scrollbar for the grid.
7802 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7804 baseOptions.enableVerticalScrollbar = typeof(baseOptions.enableVerticalScrollbar) !== "undefined" ? baseOptions.enableVerticalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7808 * @name enableHorizontalScrollbar
7809 * @propertyOf ui.grid.class:GridOptions
7810 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the horizontal scrollbar for the grid.
7811 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7813 baseOptions.enableHorizontalScrollbar = typeof(baseOptions.enableHorizontalScrollbar) !== "undefined" ? baseOptions.enableHorizontalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7817 * @name enableMinHeightCheck
7818 * @propertyOf ui.grid.class:GridOptions
7819 * @description True by default. When enabled, a newly initialized grid will check to see if it is tall enough to display
7820 * at least one row of data. If the grid is not tall enough, it will resize the DOM element to display minRowsToShow number
7823 baseOptions.enableMinHeightCheck = baseOptions.enableMinHeightCheck !== false;
7827 * @name minimumColumnSize
7828 * @propertyOf ui.grid.class:GridOptions
7829 * @description Columns can't be smaller than this, defaults to 10 pixels
7831 baseOptions.minimumColumnSize = typeof(baseOptions.minimumColumnSize) !== "undefined" ? baseOptions.minimumColumnSize : 10;
7836 * @methodOf ui.grid.class:GridOptions
7837 * @description By default, rows are compared using object equality. This option can be overridden
7838 * to compare on any data item property or function
7839 * @param {object} entityA First Data Item to compare
7840 * @param {object} entityB Second Data Item to compare
7842 baseOptions.rowEquality = baseOptions.rowEquality || function(entityA, entityB) {
7843 return entityA === entityB;
7848 * @name headerTemplate
7849 * @propertyOf ui.grid.class:GridOptions
7850 * @description Null by default. When provided, this setting uses a custom header
7851 * template, rather than the default template. Can be set to either the name of a template file:
7852 * <pre> $scope.gridOptions.headerTemplate = 'header_template.html';</pre>
7854 * <pre> $scope.gridOptions.headerTemplate = '<div class="ui-grid-top-panel" style="text-align: center">I am a Custom Grid Header</div>'</pre>
7855 * or the id of a precompiled template (TBD how to use this).
7856 * </br>Refer to the custom header tutorial for more information.
7857 * If you want no header at all, you can set to an empty div:
7858 * <pre> $scope.gridOptions.headerTemplate = '<div></div>';</pre>
7860 * If you want to only have a static header, then you can set to static content. If
7861 * you want to tailor the existing column headers, then you should look at the
7862 * current 'ui-grid-header.html' template in github as your starting point.
7865 baseOptions.headerTemplate = baseOptions.headerTemplate || null;
7869 * @name footerTemplate
7870 * @propertyOf ui.grid.class:GridOptions
7871 * @description (optional) ui-grid/ui-grid-footer by default. This footer shows the per-column
7872 * aggregation totals.
7873 * 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
7874 * <pre>'<div class="ui-grid-bottom-panel" style="text-align: center">I am a Custom Grid Footer</div>'</pre>, or the id
7875 * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information.
7877 baseOptions.footerTemplate = baseOptions.footerTemplate || 'ui-grid/ui-grid-footer';
7881 * @name gridFooterTemplate
7882 * @propertyOf ui.grid.class:GridOptions
7883 * @description (optional) ui-grid/ui-grid-grid-footer by default. This template by default shows the
7884 * total items at the bottom of the grid, and the selected items if selection is enabled.
7886 baseOptions.gridFooterTemplate = baseOptions.gridFooterTemplate || 'ui-grid/ui-grid-grid-footer';
7891 * @propertyOf ui.grid.class:GridOptions
7892 * @description 'ui-grid/ui-grid-row' by default. When provided, this setting uses a
7893 * custom row template. Can be set to either the name of a template file:
7894 * <pre> $scope.gridOptions.rowTemplate = 'row_template.html';</pre>
7896 * <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>
7897 * or the id of a precompiled template (TBD how to use this) can be provided.
7898 * </br>Refer to the custom row template tutorial for more information.
7900 baseOptions.rowTemplate = baseOptions.rowTemplate || 'ui-grid/ui-grid-row';
7904 * @name appScopeProvider
7905 * @propertyOf ui.grid.class:GridOptions
7906 * @description by default, the parent scope of the ui-grid element will be assigned to grid.appScope
7907 * this property allows you to assign any reference you want to grid.appScope
7909 baseOptions.appScopeProvider = baseOptions.appScopeProvider || null;
7922 angular.module('ui.grid')
7926 * @name ui.grid.class:GridRenderContainer
7927 * @description The grid has render containers, allowing the ability to have pinned columns. If the grid
7928 * is right-to-left then there may be a right render container, if left-to-right then there may
7929 * be a left render container. There is always a body render container.
7930 * @param {string} name The name of the render container ('body', 'left', or 'right')
7931 * @param {Grid} grid the grid the render container is in
7932 * @param {object} options the render container options
7934 .factory('GridRenderContainer', ['gridUtil', 'uiGridConstants', function(gridUtil, uiGridConstants) {
7935 function GridRenderContainer(name, grid, options) {
7938 // if (gridUtil.type(grid) !== 'Grid') {
7939 // throw new Error('Grid argument is not a Grid object');
7946 // self.rowCache = [];
7947 // self.columnCache = [];
7949 self.visibleRowCache = [];
7950 self.visibleColumnCache = [];
7952 self.renderedRows = [];
7953 self.renderedColumns = [];
7955 self.prevScrollTop = 0;
7956 self.prevScrolltopPercentage = 0;
7957 self.prevRowScrollIndex = 0;
7959 self.prevScrollLeft = 0;
7960 self.prevScrollleftPercentage = 0;
7961 self.prevColumnScrollIndex = 0;
7963 self.columnStyles = "";
7965 self.viewportAdjusters = [];
7969 * @name hasHScrollbar
7970 * @propertyOf ui.grid.class:GridRenderContainer
7971 * @description flag to signal that container has a horizontal scrollbar
7973 self.hasHScrollbar = false;
7977 * @name hasVScrollbar
7978 * @propertyOf ui.grid.class:GridRenderContainer
7979 * @description flag to signal that container has a vertical scrollbar
7981 self.hasVScrollbar = false;
7985 * @name canvasHeightShouldUpdate
7986 * @propertyOf ui.grid.class:GridRenderContainer
7987 * @description flag to signal that container should recalculate the canvas size
7989 self.canvasHeightShouldUpdate = true;
7993 * @name canvasHeight
7994 * @propertyOf ui.grid.class:GridRenderContainer
7995 * @description last calculated canvas height value
7997 self.$$canvasHeight = 0;
7999 if (options && angular.isObject(options)) {
8000 angular.extend(self, options);
8003 grid.registerStyleComputation({
8006 self.updateColumnWidths();
8007 return self.columnStyles;
8013 GridRenderContainer.prototype.reset = function reset() {
8014 // this.rowCache.length = 0;
8015 // this.columnCache.length = 0;
8017 this.visibleColumnCache.length = 0;
8018 this.visibleRowCache.length = 0;
8020 this.renderedRows.length = 0;
8021 this.renderedColumns.length = 0;
8024 // TODO(c0bra): calculate size?? Should this be in a stackable directive?
8027 GridRenderContainer.prototype.containsColumn = function (col) {
8028 return this.visibleColumnCache.indexOf(col) !== -1;
8031 GridRenderContainer.prototype.minRowsToRender = function minRowsToRender() {
8034 var rowAddedHeight = 0;
8035 var viewPortHeight = self.getViewportHeight();
8036 for (var i = self.visibleRowCache.length - 1; rowAddedHeight < viewPortHeight && i >= 0; i--) {
8037 rowAddedHeight += self.visibleRowCache[i].height;
8043 GridRenderContainer.prototype.minColumnsToRender = function minColumnsToRender() {
8045 var viewportWidth = this.getViewportWidth();
8049 // self.columns.forEach(function(col, i) {
8050 for (var i = 0; i < self.visibleColumnCache.length; i++) {
8051 var col = self.visibleColumnCache[i];
8053 if (totalWidth < viewportWidth) {
8054 totalWidth += col.drawnWidth ? col.drawnWidth : 0;
8059 for (var j = i; j >= i - min; j--) {
8060 currWidth += self.visibleColumnCache[j].drawnWidth ? self.visibleColumnCache[j].drawnWidth : 0;
8062 if (currWidth < viewportWidth) {
8071 GridRenderContainer.prototype.getVisibleRowCount = function getVisibleRowCount() {
8072 return this.visibleRowCache.length;
8077 * @name registerViewportAdjuster
8078 * @methodOf ui.grid.class:GridRenderContainer
8079 * @description Registers an adjuster to the render container's available width or height. Adjusters are used
8080 * to tell the render container that there is something else consuming space, and to adjust it's size
8082 * @param {function} func the adjuster function we want to register
8085 GridRenderContainer.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
8086 this.viewportAdjusters.push(func);
8091 * @name removeViewportAdjuster
8092 * @methodOf ui.grid.class:GridRenderContainer
8093 * @description Removes an adjuster, should be used when your element is destroyed
8094 * @param {function} func the adjuster function we want to remove
8096 GridRenderContainer.prototype.removeViewportAdjuster = function removeViewportAdjuster(func) {
8097 var idx = this.viewportAdjusters.indexOf(func);
8100 this.viewportAdjusters.splice(idx, 1);
8106 * @name getViewportAdjustment
8107 * @methodOf ui.grid.class:GridRenderContainer
8108 * @description Gets the adjustment based on the viewportAdjusters.
8109 * @returns {object} a hash of { height: x, width: y }. Usually the values will be negative
8111 GridRenderContainer.prototype.getViewportAdjustment = function getViewportAdjustment() {
8114 var adjustment = { height: 0, width: 0 };
8116 self.viewportAdjusters.forEach(function (func) {
8117 adjustment = func.call(this, adjustment);
8123 GridRenderContainer.prototype.getMargin = function getMargin(side) {
8128 self.viewportAdjusters.forEach(function (func) {
8129 var adjustment = func.call(this, { height: 0, width: 0 });
8131 if (adjustment.side && adjustment.side === side) {
8132 amount += adjustment.width * -1;
8139 GridRenderContainer.prototype.getViewportHeight = function getViewportHeight() {
8142 var headerHeight = (self.headerHeight) ? self.headerHeight : self.grid.headerHeight;
8144 var viewPortHeight = self.grid.gridHeight - headerHeight - self.grid.footerHeight;
8147 var adjustment = self.getViewportAdjustment();
8149 viewPortHeight = viewPortHeight + adjustment.height;
8151 return viewPortHeight;
8154 GridRenderContainer.prototype.getViewportWidth = function getViewportWidth() {
8157 var viewportWidth = self.grid.gridWidth;
8159 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8160 // viewPortWidth = viewPortWidth - self.grid.verticalScrollbarWidth;
8163 // var viewportWidth = 0;\
8164 // self.visibleColumnCache.forEach(function (column) {
8165 // viewportWidth += column.drawnWidth;
8168 var adjustment = self.getViewportAdjustment();
8170 viewportWidth = viewportWidth + adjustment.width;
8172 return viewportWidth;
8175 GridRenderContainer.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
8178 var viewportWidth = this.getViewportWidth();
8180 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8181 // viewPortWidth = viewPortWidth + self.grid.verticalScrollbarWidth;
8184 // var adjustment = self.getViewportAdjustment();
8185 // viewPortWidth = viewPortWidth + adjustment.width;
8187 return viewportWidth;
8193 * @name getCanvasHeight
8194 * @methodOf ui.grid.class:GridRenderContainer
8195 * @description Returns the total canvas height. Only recalculates if canvasHeightShouldUpdate = false
8196 * @returns {number} total height of all the visible rows in the container
8198 GridRenderContainer.prototype.getCanvasHeight = function getCanvasHeight() {
8201 if (!self.canvasHeightShouldUpdate) {
8202 return self.$$canvasHeight;
8205 var oldCanvasHeight = self.$$canvasHeight;
8207 self.$$canvasHeight = 0;
8209 self.visibleRowCache.forEach(function(row){
8210 self.$$canvasHeight += row.height;
8214 self.canvasHeightShouldUpdate = false;
8216 self.grid.api.core.raise.canvasHeightChanged(oldCanvasHeight, self.$$canvasHeight);
8218 return self.$$canvasHeight;
8221 GridRenderContainer.prototype.getVerticalScrollLength = function getVerticalScrollLength() {
8222 return this.getCanvasHeight() - this.getViewportHeight() + this.grid.scrollbarHeight;
8225 GridRenderContainer.prototype.getCanvasWidth = function getCanvasWidth() {
8228 var ret = self.canvasWidth;
8233 GridRenderContainer.prototype.setRenderedRows = function setRenderedRows(newRows) {
8234 this.renderedRows.length = newRows.length;
8235 for (var i = 0; i < newRows.length; i++) {
8236 this.renderedRows[i] = newRows[i];
8240 GridRenderContainer.prototype.setRenderedColumns = function setRenderedColumns(newColumns) {
8244 this.renderedColumns.length = newColumns.length;
8245 for (var i = 0; i < newColumns.length; i++) {
8246 this.renderedColumns[i] = newColumns[i];
8249 this.updateColumnOffset();
8252 GridRenderContainer.prototype.updateColumnOffset = function updateColumnOffset() {
8253 // Calculate the width of the columns on the left side that are no longer rendered.
8254 // That will be the offset for the columns as we scroll horizontally.
8255 var hiddenColumnsWidth = 0;
8256 for (var i = 0; i < this.currentFirstColumn; i++) {
8257 hiddenColumnsWidth += this.visibleColumnCache[i].drawnWidth;
8260 this.columnOffset = hiddenColumnsWidth;
8263 GridRenderContainer.prototype.scrollVertical = function (newScrollTop) {
8264 var vertScrollPercentage = -1;
8266 if (newScrollTop !== this.prevScrollTop) {
8267 var yDiff = newScrollTop - this.prevScrollTop;
8269 if (yDiff > 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; }
8270 if (yDiff < 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.UP; }
8272 var vertScrollLength = this.getVerticalScrollLength();
8274 vertScrollPercentage = newScrollTop / vertScrollLength;
8276 // console.log('vert', vertScrollPercentage, newScrollTop, vertScrollLength);
8278 if (vertScrollPercentage > 1) { vertScrollPercentage = 1; }
8279 if (vertScrollPercentage < 0) { vertScrollPercentage = 0; }
8281 this.adjustScrollVertical(newScrollTop, vertScrollPercentage);
8282 return vertScrollPercentage;
8286 GridRenderContainer.prototype.scrollHorizontal = function(newScrollLeft){
8287 var horizScrollPercentage = -1;
8291 if (newScrollLeft !== this.prevScrollLeft) {
8292 var xDiff = newScrollLeft - this.prevScrollLeft;
8294 if (xDiff > 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; }
8295 if (xDiff < 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; }
8297 var horizScrollLength = (this.canvasWidth - this.getViewportWidth());
8298 if (horizScrollLength !== 0) {
8299 horizScrollPercentage = newScrollLeft / horizScrollLength;
8302 horizScrollPercentage = 0;
8305 this.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage);
8306 return horizScrollPercentage;
8310 GridRenderContainer.prototype.adjustScrollVertical = function adjustScrollVertical(scrollTop, scrollPercentage, force) {
8311 if (this.prevScrollTop === scrollTop && !force) {
8315 if (typeof(scrollTop) === 'undefined' || scrollTop === undefined || scrollTop === null) {
8316 scrollTop = (this.getCanvasHeight() - this.getViewportHeight()) * scrollPercentage;
8319 this.adjustRows(scrollTop, scrollPercentage, false);
8321 this.prevScrollTop = scrollTop;
8322 this.prevScrolltopPercentage = scrollPercentage;
8324 this.grid.queueRefresh();
8327 GridRenderContainer.prototype.adjustScrollHorizontal = function adjustScrollHorizontal(scrollLeft, scrollPercentage, force) {
8328 if (this.prevScrollLeft === scrollLeft && !force) {
8332 if (typeof(scrollLeft) === 'undefined' || scrollLeft === undefined || scrollLeft === null) {
8333 scrollLeft = (this.getCanvasWidth() - this.getViewportWidth()) * scrollPercentage;
8336 this.adjustColumns(scrollLeft, scrollPercentage);
8338 this.prevScrollLeft = scrollLeft;
8339 this.prevScrollleftPercentage = scrollPercentage;
8341 this.grid.queueRefresh();
8344 GridRenderContainer.prototype.adjustRows = function adjustRows(scrollTop, scrollPercentage, postDataLoaded) {
8347 var minRows = self.minRowsToRender();
8349 var rowCache = self.visibleRowCache;
8351 var maxRowIndex = rowCache.length - minRows;
8353 // console.log('scroll%1', scrollPercentage);
8355 // Calculate the scroll percentage according to the scrollTop location, if no percentage was provided
8356 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollTop) {
8357 scrollPercentage = scrollTop / self.getVerticalScrollLength();
8360 var rowIndex = Math.ceil(Math.min(maxRowIndex, maxRowIndex * scrollPercentage));
8362 // console.log('maxRowIndex / scroll%', maxRowIndex, scrollPercentage, rowIndex);
8364 // Define a max row index that we can't scroll past
8365 if (rowIndex > maxRowIndex) {
8366 rowIndex = maxRowIndex;
8370 if (rowCache.length > self.grid.options.virtualizationThreshold) {
8371 if (!(typeof(scrollTop) === 'undefined' || scrollTop === null)) {
8372 // Have we hit the threshold going down?
8373 if ( !self.grid.suppressParentScrollDown && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8376 //Have we hit the threshold going up?
8377 if ( !self.grid.suppressParentScrollUp && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8381 var rangeStart = {};
8384 rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows);
8385 rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows);
8387 newRange = [rangeStart, rangeEnd];
8390 var maxLen = self.visibleRowCache.length;
8391 newRange = [0, Math.max(maxLen, minRows + self.grid.options.excessRows)];
8394 self.updateViewableRowRange(newRange);
8396 self.prevRowScrollIndex = rowIndex;
8399 GridRenderContainer.prototype.adjustColumns = function adjustColumns(scrollLeft, scrollPercentage) {
8402 var minCols = self.minColumnsToRender();
8404 var columnCache = self.visibleColumnCache;
8405 var maxColumnIndex = columnCache.length - minCols;
8407 // Calculate the scroll percentage according to the scrollLeft location, if no percentage was provided
8408 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollLeft) {
8409 var horizScrollLength = (self.getCanvasWidth() - self.getViewportWidth());
8410 scrollPercentage = scrollLeft / horizScrollLength;
8413 var colIndex = Math.ceil(Math.min(maxColumnIndex, maxColumnIndex * scrollPercentage));
8415 // Define a max row index that we can't scroll past
8416 if (colIndex > maxColumnIndex) {
8417 colIndex = maxColumnIndex;
8421 if (columnCache.length > self.grid.options.columnVirtualizationThreshold && self.getCanvasWidth() > self.getViewportWidth()) {
8422 /* Commented the following lines because otherwise the moved column wasn't visible immediately on the new position
8423 * in the case of many columns with horizontal scroll, one had to scroll left or right and then return in order to see it
8424 // Have we hit the threshold going down?
8425 if (self.prevScrollLeft < scrollLeft && colIndex < self.prevColumnScrollIndex + self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8428 //Have we hit the threshold going up?
8429 if (self.prevScrollLeft > scrollLeft && colIndex > self.prevColumnScrollIndex - self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8433 var rangeStart = Math.max(0, colIndex - self.grid.options.excessColumns);
8434 var rangeEnd = Math.min(columnCache.length, colIndex + minCols + self.grid.options.excessColumns);
8436 newRange = [rangeStart, rangeEnd];
8439 var maxLen = self.visibleColumnCache.length;
8441 newRange = [0, Math.max(maxLen, minCols + self.grid.options.excessColumns)];
8444 self.updateViewableColumnRange(newRange);
8446 self.prevColumnScrollIndex = colIndex;
8449 // Method for updating the visible rows
8450 GridRenderContainer.prototype.updateViewableRowRange = function updateViewableRowRange(renderedRange) {
8451 // Slice out the range of rows from the data
8452 // var rowArr = uiGridCtrl.grid.rows.slice(renderedRange[0], renderedRange[1]);
8453 var rowArr = this.visibleRowCache.slice(renderedRange[0], renderedRange[1]);
8455 // Define the top-most rendered row
8456 this.currentTopRow = renderedRange[0];
8458 this.setRenderedRows(rowArr);
8461 // Method for updating the visible columns
8462 GridRenderContainer.prototype.updateViewableColumnRange = function updateViewableColumnRange(renderedRange) {
8463 // Slice out the range of rows from the data
8464 // var columnArr = uiGridCtrl.grid.columns.slice(renderedRange[0], renderedRange[1]);
8465 var columnArr = this.visibleColumnCache.slice(renderedRange[0], renderedRange[1]);
8467 // Define the left-most rendered columns
8468 this.currentFirstColumn = renderedRange[0];
8470 this.setRenderedColumns(columnArr);
8473 GridRenderContainer.prototype.headerCellWrapperStyle = function () {
8476 if (self.currentFirstColumn !== 0) {
8477 var offset = self.columnOffset;
8479 if (self.grid.isRTL()) {
8480 return { 'margin-right': offset + 'px' };
8483 return { 'margin-left': offset + 'px' };
8492 * @name updateColumnWidths
8493 * @propertyOf ui.grid.class:GridRenderContainer
8494 * @description Determine the appropriate column width of each column across all render containers.
8496 * Column width is easy when each column has a specified width. When columns are variable width (i.e.
8497 * have an * or % of the viewport) then we try to calculate so that things fit in. The problem is that
8498 * we have multiple render containers, and we don't want one render container to just take the whole viewport
8499 * when it doesn't need to - we want things to balance out across the render containers.
8501 * To do this, we use this method to calculate all the renderContainers, recognising that in a given render
8502 * cycle it'll get called once per render container, so it needs to return the same values each time.
8504 * The constraints on this method are therefore:
8505 * - must return the same value when called multiple times, to do this it needs to rely on properties of the
8506 * columns, but not properties that change when this is called (so it shouldn't rely on drawnWidth)
8508 * The general logic of this method is:
8509 * - calculate our total available width
8510 * - look at all the columns across all render containers, and work out which have widths and which have
8511 * constraints such as % or * or something else
8512 * - for those with *, count the total number of * we see and add it onto a running total, add this column to an * array
8513 * - for those with a %, allocate the % as a percentage of the viewport, having consideration of min and max
8514 * - for those with manual width (in pixels) we set the drawnWidth to the specified width
8515 * - we end up with an asterisks array still to process
8516 * - we look at our remaining width. If it's greater than zero, we divide it up among the asterisk columns, then process
8517 * them for min and max width constraints
8518 * - if it's zero or less, we set the asterisk columns to their minimum widths
8519 * - we use parseInt quite a bit, as we try to make all our column widths integers
8521 GridRenderContainer.prototype.updateColumnWidths = function () {
8524 var asterisksArray = [],
8529 // Get the width of the viewport
8530 var availableWidth = self.grid.getViewportWidth() - self.grid.scrollbarWidth;
8532 // get all the columns across all render containers, we have to calculate them all or one render container
8533 // could consume the whole viewport
8534 var columnCache = [];
8535 angular.forEach(self.grid.renderContainers, function( container, name){
8536 columnCache = columnCache.concat(container.visibleColumnCache);
8539 // look at each column, process any manual values or %, put the * into an array to look at later
8540 columnCache.forEach(function(column, i) {
8542 // Skip hidden columns
8543 if (!column.visible) { return; }
8545 if (angular.isNumber(column.width)) {
8546 // pixel width, set to this value
8547 width = parseInt(column.width, 10);
8548 usedWidthSum = usedWidthSum + width;
8549 column.drawnWidth = width;
8551 } else if (gridUtil.endsWith(column.width, "%")) {
8552 // percentage width, set to percentage of the viewport
8553 width = parseInt(parseInt(column.width.replace(/%/g, ''), 10) / 100 * availableWidth);
8555 if ( width > column.maxWidth ){
8556 width = column.maxWidth;
8559 if ( width < column.minWidth ){
8560 width = column.minWidth;
8563 usedWidthSum = usedWidthSum + width;
8564 column.drawnWidth = width;
8565 } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) {
8566 // is an asterisk column, the gridColumn already checked the string consists only of '****'
8567 asteriskNum = asteriskNum + column.width.length;
8568 asterisksArray.push(column);
8572 // Get the remaining width (available width subtracted by the used widths sum)
8573 var remainingWidth = availableWidth - usedWidthSum;
8575 var i, column, colWidth;
8577 if (asterisksArray.length > 0) {
8578 // the width that each asterisk value would be assigned (this can be negative)
8579 var asteriskVal = remainingWidth / asteriskNum;
8581 asterisksArray.forEach(function( column ){
8582 var width = parseInt(column.width.length * asteriskVal, 10);
8584 if ( width > column.maxWidth ){
8585 width = column.maxWidth;
8588 if ( width < column.minWidth ){
8589 width = column.minWidth;
8592 usedWidthSum = usedWidthSum + width;
8593 column.drawnWidth = width;
8597 // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our
8598 // calculated widths would have the grid narrower than the available space,
8599 // dole the remainder out one by one to make everything fit
8600 var processColumnUpwards = function(column){
8601 if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) {
8602 column.drawnWidth++;
8605 columnsToChange = true;
8609 var leftoverWidth = availableWidth - usedWidthSum;
8610 var columnsToChange = true;
8612 while (leftoverWidth > 0 && columnsToChange) {
8613 columnsToChange = false;
8614 asterisksArray.forEach(processColumnUpwards);
8617 // We can end up with too much width even though some columns aren't at their max width, in this situation
8618 // we can trim the columns a little
8619 var processColumnDownwards = function(column){
8620 if ( column.drawnWidth > column.minWidth && excessWidth > 0) {
8621 column.drawnWidth--;
8624 columnsToChange = true;
8628 var excessWidth = usedWidthSum - availableWidth;
8629 columnsToChange = true;
8631 while (excessWidth > 0 && columnsToChange) {
8632 columnsToChange = false;
8633 asterisksArray.forEach(processColumnDownwards);
8637 // all that was across all the renderContainers, now we need to work out what that calculation decided for
8638 // our renderContainer
8639 var canvasWidth = 0;
8640 self.visibleColumnCache.forEach(function(column){
8641 if ( column.visible ){
8642 canvasWidth = canvasWidth + column.drawnWidth;
8647 columnCache.forEach(function (column) {
8648 ret = ret + column.getColClassDefinition();
8651 self.canvasWidth = canvasWidth;
8653 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
8656 // Set this render container's column styles so they can be used in style computation
8657 this.columnStyles = ret;
8660 GridRenderContainer.prototype.needsHScrollbarPlaceholder = function () {
8661 return this.grid.options.enableHorizontalScrollbar && !this.hasHScrollbar && !this.grid.disableScrolling;
8664 GridRenderContainer.prototype.getViewportStyle = function () {
8668 self.hasHScrollbar = false;
8669 self.hasVScrollbar = false;
8671 if (self.grid.disableScrolling) {
8672 styles['overflow-x'] = 'hidden';
8673 styles['overflow-y'] = 'hidden';
8677 if (self.name === 'body') {
8678 self.hasHScrollbar = self.grid.options.enableHorizontalScrollbar !== uiGridConstants.scrollbars.NEVER;
8679 if (!self.grid.isRTL()) {
8680 if (!self.grid.hasRightContainerColumns()) {
8681 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8685 if (!self.grid.hasLeftContainerColumns()) {
8686 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8690 else if (self.name === 'left') {
8691 self.hasVScrollbar = self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8694 self.hasVScrollbar = !self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8697 styles['overflow-x'] = self.hasHScrollbar ? 'scroll' : 'hidden';
8698 styles['overflow-y'] = self.hasVScrollbar ? 'scroll' : 'hidden';
8706 return GridRenderContainer;
8713 angular.module('ui.grid')
8714 .factory('GridRow', ['gridUtil', function(gridUtil) {
8718 * @name ui.grid.class:GridRow
8719 * @description GridRow is the viewModel for one logical row on the grid. A grid Row is not necessarily a one-to-one
8720 * relation to gridOptions.data.
8721 * @param {object} entity the array item from GridOptions.data
8722 * @param {number} index the current position of the row in the array
8723 * @param {Grid} reference to the parent grid
8725 function GridRow(entity, index, grid) {
8730 * @propertyOf ui.grid.class:GridRow
8731 * @description A reference back to the grid
8738 * @propertyOf ui.grid.class:GridRow
8739 * @description A reference to an item in gridOptions.data[]
8741 this.entity = entity;
8746 * @propertyOf ui.grid.class:GridRow
8747 * @description UniqueId of row
8749 this.uid = gridUtil.nextUid();
8754 * @propertyOf ui.grid.class:GridRow
8755 * @description If true, the row will be rendered
8758 this.visible = true;
8761 this.$$height = grid.options.rowHeight;
8768 * @propertyOf ui.grid.class:GridRow
8769 * @description height of each individual row. changing the height will flag all
8770 * row renderContainers to recalculate their canvas height
8772 Object.defineProperty(GridRow.prototype, 'height', {
8774 return this.$$height;
8776 set: function(height) {
8777 if (height !== this.$$height) {
8778 this.grid.updateCanvasHeight();
8779 this.$$height = height;
8786 * @name getQualifiedColField
8787 * @methodOf ui.grid.class:GridRow
8788 * @description returns the qualified field name as it exists on scope
8789 * ie: row.entity.fieldA
8790 * @param {GridCol} col column instance
8791 * @returns {string} resulting name that can be evaluated on scope
8793 GridRow.prototype.getQualifiedColField = function(col) {
8794 return 'row.' + this.getEntityQualifiedColField(col);
8799 * @name getEntityQualifiedColField
8800 * @methodOf ui.grid.class:GridRow
8801 * @description returns the qualified field name minus the row path
8803 * @param {GridCol} col column instance
8804 * @returns {string} resulting name that can be evaluated against a row
8806 GridRow.prototype.getEntityQualifiedColField = function(col) {
8807 return gridUtil.preEval('entity.' + col.field);
8813 * @name setRowInvisible
8814 * @methodOf ui.grid.class:GridRow
8815 * @description Sets an override on the row that forces it to always
8816 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8818 * This method can be called from the api, passing in the gridRow we want
8819 * altered. It should really work by calling gridRow.setRowInvisible, but that's
8820 * not the way I coded it, and too late to change now. Changed to just call
8821 * the internal function row.setThisRowInvisible().
8823 * @param {GridRow} row the row we want to set to invisible
8826 GridRow.prototype.setRowInvisible = function ( row ) {
8827 if (row && row.setThisRowInvisible){
8828 row.setThisRowInvisible( 'user' );
8835 * @name clearRowInvisible
8836 * @methodOf ui.grid.class:GridRow
8837 * @description Clears an override on the row that forces it to always
8838 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8840 * This method can be called from the api, passing in the gridRow we want
8841 * altered. It should really work by calling gridRow.clearRowInvisible, but that's
8842 * not the way I coded it, and too late to change now. Changed to just call
8843 * the internal function row.clearThisRowInvisible().
8845 * @param {GridRow} row the row we want to clear the invisible flag
8848 GridRow.prototype.clearRowInvisible = function ( row ) {
8849 if (row && row.clearThisRowInvisible){
8850 row.clearThisRowInvisible( 'user' );
8857 * @name setThisRowInvisible
8858 * @methodOf ui.grid.class:GridRow
8859 * @description Sets an override on the row that forces it to always
8860 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility
8862 * @param {string} reason the reason (usually the module) for the row to be invisible.
8863 * E.g. grouping, user, filter
8864 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8866 GridRow.prototype.setThisRowInvisible = function ( reason, fromRowsProcessor ) {
8867 if ( !this.invisibleReason ){
8868 this.invisibleReason = {};
8870 this.invisibleReason[reason] = true;
8871 this.evaluateRowVisibility( fromRowsProcessor);
8877 * @name clearRowInvisible
8878 * @methodOf ui.grid.class:GridRow
8879 * @description Clears any override on the row visibility, returning it
8880 * to normal visibility calculations. Emits the rowsVisibleChanged
8883 * @param {string} reason the reason (usually the module) for the row to be invisible.
8884 * E.g. grouping, user, filter
8885 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8887 GridRow.prototype.clearThisRowInvisible = function ( reason, fromRowsProcessor ) {
8888 if (typeof(this.invisibleReason) !== 'undefined' ) {
8889 delete this.invisibleReason[reason];
8891 this.evaluateRowVisibility( fromRowsProcessor );
8897 * @name evaluateRowVisibility
8898 * @methodOf ui.grid.class:GridRow
8899 * @description Determines whether the row should be visible based on invisibleReason,
8900 * and if it changes the row visibility, then emits the rowsVisibleChanged event.
8902 * Queues a grid refresh, but doesn't call it directly to avoid hitting lots of grid refreshes.
8903 * @param {boolean} fromRowProcessor if true, then it won't raise events or queue the refresh, the
8904 * row processor does that already
8906 GridRow.prototype.evaluateRowVisibility = function ( fromRowProcessor ) {
8907 var newVisibility = true;
8908 if ( typeof(this.invisibleReason) !== 'undefined' ){
8909 angular.forEach(this.invisibleReason, function( value, key ){
8911 newVisibility = false;
8916 if ( typeof(this.visible) === 'undefined' || this.visible !== newVisibility ){
8917 this.visible = newVisibility;
8918 if ( !fromRowProcessor ){
8919 this.grid.queueGridRefresh();
8920 this.grid.api.core.raise.rowsVisibleChanged(this);
8935 * @name ui.grid.class:GridRowColumn
8936 * @param {GridRow} row The row for this pair
8937 * @param {GridColumn} column The column for this pair
8938 * @description A row and column pair that represents the intersection of these two entities.
8939 * Must be instantiated as a constructor using the `new` keyword.
8941 angular.module('ui.grid')
8942 .factory('GridRowColumn', ['$parse', '$filter',
8943 function GridRowColumnFactory($parse, $filter){
8944 var GridRowColumn = function GridRowColumn(row, col) {
8945 if ( !(this instanceof GridRowColumn)){
8946 throw "Using GridRowColumn as a function insead of as a constructor. Must be called with `new` keyword";
8952 * @propertyOf ui.grid.class:GridRowColumn
8953 * @description {@link ui.grid.class:GridRow }
8959 * @propertyOf ui.grid.class:GridRowColumn
8960 * @description {@link ui.grid.class:GridColumn }
8967 * @name getIntersectionValueRaw
8968 * @methodOf ui.grid.class:GridRowColumn
8969 * @description Gets the intersection of where the row and column meet.
8970 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8971 * If the column has a cellFilter this will NOT return the filtered value.
8973 GridRowColumn.prototype.getIntersectionValueRaw = function(){
8974 var getter = $parse(this.row.getEntityQualifiedColField(this.col));
8975 var context = this.row;
8976 return getter(context);
8980 * @name getIntersectionValueFiltered
8981 * @methodOf ui.grid.class:GridRowColumn
8982 * @description Gets the intersection of where the row and column meet.
8983 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8984 * If the column has a cellFilter this will also apply the filter to it and return the value that the filter displays.
8986 GridRowColumn.prototype.getIntersectionValueFiltered = function(){
8987 var value = this.getIntersectionValueRaw();
8988 if (this.col.cellFilter && this.col.cellFilter !== ''){
8989 var getFilterIfExists = function(filterName){
8991 return $filter(filterName);
8996 var filter = getFilterIfExists(this.col.cellFilter);
8997 if (filter) { // Check if this is filter name or a filter string
8998 value = filter(value);
8999 } else { // We have the template version of a filter so we need to parse it apart
9000 // Get the filter params out using a regex
9001 // Test out this regex here https://regex101.com/r/rC5eR5/2
9002 var re = /([^:]*):([^:]*):?([\s\S]+)?/;
9004 if ((matches = re.exec(this.col.cellFilter)) !== null) {
9005 // View your result using the matches-variable.
9006 // eg matches[0] etc.
9007 value = $filter(matches[1])(value, matches[2], matches[3]);
9013 return GridRowColumn;
9019 angular.module('ui.grid')
9020 .factory('ScrollEvent', ['gridUtil', function (gridUtil) {
9024 * @name ui.grid.class:ScrollEvent
9025 * @description Model for all scrollEvents
9026 * @param {Grid} grid that owns the scroll event
9027 * @param {GridRenderContainer} sourceRowContainer that owns the scroll event. Can be null
9028 * @param {GridRenderContainer} sourceColContainer that owns the scroll event. Can be null
9029 * @param {string} source the source of the event - from uiGridConstants.scrollEventSources or a string value of directive/service/factory.functionName
9031 function ScrollEvent(grid, sourceRowContainer, sourceColContainer, source) {
9034 throw new Error("grid argument is required");
9040 * @propertyOf ui.grid.class:ScrollEvent
9041 * @description A reference back to the grid
9050 * @propertyOf ui.grid.class:ScrollEvent
9051 * @description the source of the scroll event. limited to values from uiGridConstants.scrollEventSources
9053 self.source = source;
9059 * @propertyOf ui.grid.class:ScrollEvent
9060 * @description most scroll events from the mouse or trackpad require delay to operate properly
9061 * set to false to eliminate delay. Useful for scroll events that the grid causes, such as scrolling to make a row visible.
9063 self.withDelay = true;
9065 self.sourceRowContainer = sourceRowContainer;
9066 self.sourceColContainer = sourceColContainer;
9068 self.newScrollLeft = null;
9069 self.newScrollTop = null;
9073 self.verticalScrollLength = -9999999;
9074 self.horizontalScrollLength = -999999;
9079 * @name fireThrottledScrollingEvent
9080 * @methodOf ui.grid.class:ScrollEvent
9081 * @description fires a throttled event using grid.api.core.raise.scrollEvent
9083 self.fireThrottledScrollingEvent = gridUtil.throttle(function(sourceContainerId) {
9084 self.grid.scrollContainers(sourceContainerId, self);
9085 }, self.grid.options.wheelScrollThrottle, {trailing: true});
9092 * @name getNewScrollLeft
9093 * @methodOf ui.grid.class:ScrollEvent
9094 * @description returns newScrollLeft property if available; calculates a new value if it isn't
9096 ScrollEvent.prototype.getNewScrollLeft = function(colContainer, viewport){
9099 if (!self.newScrollLeft){
9100 var scrollWidth = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
9102 var oldScrollLeft = gridUtil.normalizeScrollLeft(viewport, self.grid);
9104 var scrollXPercentage;
9105 if (typeof(self.x.percentage) !== 'undefined' && self.x.percentage !== undefined) {
9106 scrollXPercentage = self.x.percentage;
9108 else if (typeof(self.x.pixels) !== 'undefined' && self.x.pixels !== undefined) {
9109 scrollXPercentage = self.x.percentage = (oldScrollLeft + self.x.pixels) / scrollWidth;
9112 throw new Error("No percentage or pixel value provided for scroll event X axis");
9115 return Math.max(0, scrollXPercentage * scrollWidth);
9118 return self.newScrollLeft;
9124 * @name getNewScrollTop
9125 * @methodOf ui.grid.class:ScrollEvent
9126 * @description returns newScrollTop property if available; calculates a new value if it isn't
9128 ScrollEvent.prototype.getNewScrollTop = function(rowContainer, viewport){
9132 if (!self.newScrollTop){
9133 var scrollLength = rowContainer.getVerticalScrollLength();
9135 var oldScrollTop = viewport[0].scrollTop;
9137 var scrollYPercentage;
9138 if (typeof(self.y.percentage) !== 'undefined' && self.y.percentage !== undefined) {
9139 scrollYPercentage = self.y.percentage;
9141 else if (typeof(self.y.pixels) !== 'undefined' && self.y.pixels !== undefined) {
9142 scrollYPercentage = self.y.percentage = (oldScrollTop + self.y.pixels) / scrollLength;
9145 throw new Error("No percentage or pixel value provided for scroll event Y axis");
9148 return Math.max(0, scrollYPercentage * scrollLength);
9151 return self.newScrollTop;
9154 ScrollEvent.prototype.atTop = function(scrollTop) {
9155 return (this.y && (this.y.percentage === 0 || this.verticalScrollLength < 0) && scrollTop === 0);
9158 ScrollEvent.prototype.atBottom = function(scrollTop) {
9159 return (this.y && (this.y.percentage === 1 || this.verticalScrollLength === 0) && scrollTop > 0);
9162 ScrollEvent.prototype.atLeft = function(scrollLeft) {
9163 return (this.x && (this.x.percentage === 0 || this.horizontalScrollLength < 0) && scrollLeft === 0);
9166 ScrollEvent.prototype.atRight = function(scrollLeft) {
9167 return (this.x && (this.x.percentage === 1 || this.horizontalScrollLength ===0) && scrollLeft > 0);
9171 ScrollEvent.Sources = {
9172 ViewPortScroll: 'ViewPortScroll',
9173 RenderContainerMouseWheel: 'RenderContainerMouseWheel',
9174 RenderContainerTouchMove: 'RenderContainerTouchMove',
9189 * @name ui.grid.service:gridClassFactory
9191 * @description factory to return dom specific instances of a grid
9194 angular.module('ui.grid').service('gridClassFactory', ['gridUtil', '$q', '$compile', '$templateCache', 'uiGridConstants', 'Grid', 'GridColumn', 'GridRow',
9195 function (gridUtil, $q, $compile, $templateCache, uiGridConstants, Grid, GridColumn, GridRow) {
9201 * @methodOf ui.grid.service:gridClassFactory
9202 * @description Creates a new grid instance. Each instance will have a unique id
9203 * @param {object} options An object map of options to pass into the created grid instance.
9204 * @returns {Grid} grid
9206 createGrid : function(options) {
9207 options = (typeof(options) !== 'undefined') ? options : {};
9208 options.id = gridUtil.newId();
9209 var grid = new Grid(options);
9211 // NOTE/TODO: rowTemplate should always be defined...
9212 if (grid.options.rowTemplate) {
9213 var rowTemplateFnPromise = $q.defer();
9214 grid.getRowTemplateFn = rowTemplateFnPromise.promise;
9216 gridUtil.getTemplate(grid.options.rowTemplate)
9218 function (template) {
9219 var rowTemplateFn = $compile(template);
9220 rowTemplateFnPromise.resolve(rowTemplateFn);
9223 // Todo handle response error here?
9224 throw new Error("Couldn't fetch/use row template '" + grid.options.rowTemplate + "'");
9228 grid.registerColumnBuilder(service.defaultColumnBuilder);
9230 // Row builder for custom row templates
9231 grid.registerRowBuilder(service.rowTemplateAssigner);
9233 // Reset all rows to visible initially
9234 grid.registerRowsProcessor(function allRowsVisible(rows) {
9235 rows.forEach(function (row) {
9236 row.evaluateRowVisibility( true );
9242 grid.registerColumnsProcessor(function allColumnsVisible(columns) {
9243 columns.forEach(function (column) {
9244 column.visible = true;
9250 grid.registerColumnsProcessor(function(renderableColumns) {
9251 renderableColumns.forEach(function (column) {
9252 if (column.colDef.visible === false) {
9253 column.visible = false;
9257 return renderableColumns;
9261 grid.registerRowsProcessor(grid.searchRows, 100);
9263 // Register the default row processor, it sorts rows by selected columns
9264 if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) {
9265 grid.registerRowsProcessor(grid.options.externalSort, 200);
9268 grid.registerRowsProcessor(grid.sortByColumn, 200);
9276 * @name defaultColumnBuilder
9277 * @methodOf ui.grid.service:gridClassFactory
9278 * @description Processes designTime column definitions and applies them to col for the
9279 * core grid features
9280 * @param {object} colDef reference to column definition
9281 * @param {GridColumn} col reference to gridCol
9282 * @param {object} gridOptions reference to grid options
9284 defaultColumnBuilder: function (colDef, col, gridOptions) {
9286 var templateGetPromises = [];
9288 // Abstracts the standard template processing we do for every template type.
9289 var processTemplate = function( templateType, providedType, defaultTemplate, filterType, tooltipType ) {
9290 if ( !colDef[templateType] ){
9291 col[providedType] = defaultTemplate;
9293 col[providedType] = colDef[templateType];
9296 templateGetPromises.push(gridUtil.getTemplate(col[providedType])
9298 function (template) {
9299 if ( angular.isFunction(template) ) { template = template(); }
9300 var tooltipCall = ( tooltipType === 'cellTooltip' ) ? 'col.cellTooltip(row,col)' : 'col.headerTooltip(col)';
9301 if ( tooltipType && col[tooltipType] === false ){
9302 template = template.replace(uiGridConstants.TOOLTIP, '');
9303 } else if ( tooltipType && col[tooltipType] ){
9304 template = template.replace(uiGridConstants.TOOLTIP, 'title="{{' + tooltipCall + ' CUSTOM_FILTERS }}"');
9308 col[templateType] = template.replace(uiGridConstants.CUSTOM_FILTERS, function() {
9309 return col[filterType] ? "|" + col[filterType] : "";
9312 col[templateType] = template;
9316 throw new Error("Couldn't fetch/use colDef." + templateType + " '" + colDef[templateType] + "'");
9325 * @name cellTemplate
9326 * @propertyOf ui.grid.class:GridOptions.columnDef
9327 * @description a custom template for each cell in this column. The default
9328 * is ui-grid/uiGridCell. If you are using the cellNav feature, this template
9329 * must contain a div that can receive focus.
9332 processTemplate( 'cellTemplate', 'providedCellTemplate', 'ui-grid/uiGridCell', 'cellFilter', 'cellTooltip' );
9333 col.cellTemplatePromise = templateGetPromises[0];
9337 * @name headerCellTemplate
9338 * @propertyOf ui.grid.class:GridOptions.columnDef
9339 * @description a custom template for the header for this column. The default
9340 * is ui-grid/uiGridHeaderCell
9343 processTemplate( 'headerCellTemplate', 'providedHeaderCellTemplate', 'ui-grid/uiGridHeaderCell', 'headerCellFilter', 'headerTooltip' );
9347 * @name footerCellTemplate
9348 * @propertyOf ui.grid.class:GridOptions.columnDef
9349 * @description a custom template for the footer for this column. The default
9350 * is ui-grid/uiGridFooterCell
9353 processTemplate( 'footerCellTemplate', 'providedFooterCellTemplate', 'ui-grid/uiGridFooterCell', 'footerCellFilter' );
9357 * @name filterHeaderTemplate
9358 * @propertyOf ui.grid.class:GridOptions.columnDef
9359 * @description a custom template for the filter input. The default is ui-grid/ui-grid-filter
9362 processTemplate( 'filterHeaderTemplate', 'providedFilterHeaderTemplate', 'ui-grid/ui-grid-filter' );
9364 // Create a promise for the compiled element function
9365 col.compiledElementFnDefer = $q.defer();
9367 return $q.all(templateGetPromises);
9371 rowTemplateAssigner: function rowTemplateAssigner(row) {
9374 // Row has no template assigned to it
9375 if (!row.rowTemplate) {
9376 // Use the default row template from the grid
9377 row.rowTemplate = grid.options.rowTemplate;
9379 // Use the grid's function for fetching the compiled row template function
9380 row.getRowTemplateFn = grid.getRowTemplateFn;
9382 // Row has its own template assigned
9384 // Create a promise for the compiled row template function
9385 var perRowTemplateFnPromise = $q.defer();
9386 row.getRowTemplateFn = perRowTemplateFnPromise.promise;
9388 // Get the row template
9389 gridUtil.getTemplate(row.rowTemplate)
9390 .then(function (template) {
9391 // Compile the template
9392 var rowTemplateFn = $compile(template);
9394 // Resolve the compiled template function promise
9395 perRowTemplateFnPromise.resolve(rowTemplateFn);
9398 // Todo handle response error here?
9399 throw new Error("Couldn't fetch/use row template '" + row.rowTemplate + "'");
9403 return row.getRowTemplateFn;
9407 //class definitions (moved to separate factories)
9416 var module = angular.module('ui.grid');
9418 function escapeRegExp(str) {
9419 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
9425 * @name ui.grid.service:rowSearcher
9427 * @description Service for searching/filtering rows based on column value conditions.
9429 module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil, uiGridConstants) {
9430 var defaultCondition = uiGridConstants.filter.CONTAINS;
9432 var rowSearcher = {};
9437 * @methodOf ui.grid.service:rowSearcher
9438 * @description Get the term from a filter
9439 * Trims leading and trailing whitespace
9440 * @param {object} filter object to use
9441 * @returns {object} Parsed term
9443 rowSearcher.getTerm = function getTerm(filter) {
9444 if (typeof(filter.term) === 'undefined') { return filter.term; }
9446 var term = filter.term;
9448 // Strip leading and trailing whitespace if the term is a string
9449 if (typeof(term) === 'string') {
9459 * @methodOf ui.grid.service:rowSearcher
9460 * @description Remove leading and trailing asterisk (*) from the filter's term
9461 * @param {object} filter object to use
9462 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9464 rowSearcher.stripTerm = function stripTerm(filter) {
9465 var term = rowSearcher.getTerm(filter);
9467 if (typeof(term) === 'string') {
9468 return escapeRegExp(term.replace(/(^\*|\*$)/g, ''));
9478 * @name guessCondition
9479 * @methodOf ui.grid.service:rowSearcher
9480 * @description Guess the condition for a filter based on its term
9482 * Defaults to STARTS_WITH. Uses CONTAINS for strings beginning and ending with *s (*bob*).
9483 * Uses STARTS_WITH for strings ending with * (bo*). Uses ENDS_WITH for strings starting with * (*ob).
9484 * @param {object} filter object to use
9485 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9487 rowSearcher.guessCondition = function guessCondition(filter) {
9488 if (typeof(filter.term) === 'undefined' || !filter.term) {
9489 return defaultCondition;
9492 var term = rowSearcher.getTerm(filter);
9494 if (/\*/.test(term)) {
9495 var regexpFlags = '';
9496 if (!filter.flags || !filter.flags.caseSensitive) {
9500 var reText = term.replace(/(\\)?\*/g, function ($0, $1) { return $1 ? $0 : '[\\s\\S]*?'; });
9501 return new RegExp('^' + reText + '$', regexpFlags);
9503 // Otherwise default to default condition
9505 return defaultCondition;
9512 * @name setupFilters
9513 * @methodOf ui.grid.service:rowSearcher
9514 * @description For a given columns filters (either col.filters, or [col.filter] can be passed in),
9515 * do all the parsing and pre-processing and store that data into a new filters object. The object
9516 * has the condition, the flags, the stripped term, and a parsed reg exp if there was one.
9518 * We could use a forEach in here, since it's much less performance sensitive, but since we're using
9519 * for loops everywhere else in this module...
9521 * @param {array} filters the filters from the column (col.filters or [col.filter])
9522 * @returns {array} An array of parsed/preprocessed filters
9524 rowSearcher.setupFilters = function setupFilters( filters ){
9525 var newFilters = [];
9527 var filtersLength = filters.length;
9528 for ( var i = 0; i < filtersLength; i++ ){
9529 var filter = filters[i];
9531 if ( filter.noTerm || !gridUtil.isNullOrUndefined(filter.term) ){
9534 var regexpFlags = '';
9535 if (!filter.flags || !filter.flags.caseSensitive) {
9539 if ( !gridUtil.isNullOrUndefined(filter.term) ){
9540 // it is possible to have noTerm. We don't need to copy that across, it was just a flag to avoid
9541 // getting the filter ignored if the filter was a function that didn't use a term
9542 newFilter.term = rowSearcher.stripTerm(filter);
9545 if ( filter.condition ){
9546 newFilter.condition = filter.condition;
9548 newFilter.condition = rowSearcher.guessCondition(filter);
9551 newFilter.flags = angular.extend( { caseSensitive: false, date: false }, filter.flags );
9553 if (newFilter.condition === uiGridConstants.filter.STARTS_WITH) {
9554 newFilter.startswithRE = new RegExp('^' + newFilter.term, regexpFlags);
9557 if (newFilter.condition === uiGridConstants.filter.ENDS_WITH) {
9558 newFilter.endswithRE = new RegExp(newFilter.term + '$', regexpFlags);
9561 if (newFilter.condition === uiGridConstants.filter.CONTAINS) {
9562 newFilter.containsRE = new RegExp(newFilter.term, regexpFlags);
9565 if (newFilter.condition === uiGridConstants.filter.EXACT) {
9566 newFilter.exactRE = new RegExp('^' + newFilter.term + '$', regexpFlags);
9569 newFilters.push(newFilter);
9578 * @name runColumnFilter
9579 * @methodOf ui.grid.service:rowSearcher
9580 * @description Runs a single pre-parsed filter against a cell, returning true
9581 * if the cell matches that one filter.
9583 * @param {Grid} grid the grid we're working against
9584 * @param {GridRow} row the row we're matching against
9585 * @param {GridCol} column the column that we're working against
9586 * @param {object} filter the specific, preparsed, filter that we want to test
9587 * @returns {boolean} true if we match (row stays visible)
9589 rowSearcher.runColumnFilter = function runColumnFilter(grid, row, column, filter) {
9590 // Cache typeof condition
9591 var conditionType = typeof(filter.condition);
9593 // Term to search for.
9594 var term = filter.term;
9596 // Get the column value for this row
9598 if ( column.filterCellFiltered ){
9599 value = grid.getCellDisplayValue(row, column);
9601 value = grid.getCellValue(row, column);
9605 // If the filter's condition is a RegExp, then use it
9606 if (filter.condition instanceof RegExp) {
9607 return filter.condition.test(value);
9610 // If the filter's condition is a function, run it
9611 if (conditionType === 'function') {
9612 return filter.condition(term, value, row, column);
9615 if (filter.startswithRE) {
9616 return filter.startswithRE.test(value);
9619 if (filter.endswithRE) {
9620 return filter.endswithRE.test(value);
9623 if (filter.containsRE) {
9624 return filter.containsRE.test(value);
9627 if (filter.exactRE) {
9628 return filter.exactRE.test(value);
9631 if (filter.condition === uiGridConstants.filter.NOT_EQUAL) {
9632 var regex = new RegExp('^' + term + '$');
9633 return !regex.exec(value);
9636 if (typeof(value) === 'number' && typeof(term) === 'string' ){
9637 // if the term has a decimal in it, it comes through as '9\.4', we need to take out the \
9638 // the same for negative numbers
9639 // TODO: I suspect the right answer is to look at escapeRegExp at the top of this code file, maybe it's not needed?
9640 var tempFloat = parseFloat(term.replace(/\\\./,'.').replace(/\\\-/,'-'));
9641 if (!isNaN(tempFloat)) {
9646 if (filter.flags.date === true) {
9647 value = new Date(value);
9648 // If the term has a dash in it, it comes through as '\-' -- we need to take out the '\'.
9649 term = new Date(term.replace(/\\/g, ''));
9652 if (filter.condition === uiGridConstants.filter.GREATER_THAN) {
9653 return (value > term);
9656 if (filter.condition === uiGridConstants.filter.GREATER_THAN_OR_EQUAL) {
9657 return (value >= term);
9660 if (filter.condition === uiGridConstants.filter.LESS_THAN) {
9661 return (value < term);
9664 if (filter.condition === uiGridConstants.filter.LESS_THAN_OR_EQUAL) {
9665 return (value <= term);
9674 * @name useExternalFiltering
9675 * @propertyOf ui.grid.class:GridOptions
9676 * @description False by default. When enabled, this setting suppresses the internal filtering.
9677 * All UI logic will still operate, allowing filter conditions to be set and modified.
9679 * The external filter logic can listen for the `filterChange` event, which fires whenever
9680 * a filter has been adjusted.
9684 * @name searchColumn
9685 * @methodOf ui.grid.service:rowSearcher
9686 * @description Process provided filters on provided column against a given row. If the row meets
9687 * the conditions on all the filters, return true.
9688 * @param {Grid} grid Grid to search in
9689 * @param {GridRow} row Row to search on
9690 * @param {GridCol} column Column with the filters to use
9691 * @param {array} filters array of pre-parsed/preprocessed filters to apply
9692 * @returns {boolean} Whether the column matches or not.
9694 rowSearcher.searchColumn = function searchColumn(grid, row, column, filters) {
9695 if (grid.options.useExternalFiltering) {
9699 var filtersLength = filters.length;
9700 for (var i = 0; i < filtersLength; i++) {
9701 var filter = filters[i];
9703 var ret = rowSearcher.runColumnFilter(grid, row, column, filter);
9716 * @methodOf ui.grid.service:rowSearcher
9717 * @description Run a search across the given rows and columns, marking any rows that don't
9718 * match the stored col.filters or col.filter as invisible.
9719 * @param {Grid} grid Grid instance to search inside
9720 * @param {Array[GridRow]} rows GridRows to filter
9721 * @param {Array[GridColumn]} columns GridColumns with filters to process
9723 rowSearcher.search = function search(grid, rows, columns) {
9725 * Added performance optimisations into this code base, as this logic creates deeply nested
9726 * loops and is therefore very performance sensitive. In particular, avoiding forEach as
9727 * this impacts some browser optimisers (particularly Chrome), using iterators instead
9730 // Don't do anything if we weren't passed any rows
9735 // don't filter if filtering currently disabled
9736 if (!grid.options.enableFiltering){
9740 // Build list of filters to apply
9741 var filterData = [];
9743 var colsLength = columns.length;
9745 var hasTerm = function( filters ) {
9746 var hasTerm = false;
9748 filters.forEach( function (filter) {
9749 if ( !gridUtil.isNullOrUndefined(filter.term) && filter.term !== '' || filter.noTerm ){
9757 for (var i = 0; i < colsLength; i++) {
9758 var col = columns[i];
9760 if (typeof(col.filters) !== 'undefined' && hasTerm(col.filters) ) {
9761 filterData.push( { col: col, filters: rowSearcher.setupFilters(col.filters) } );
9765 if (filterData.length > 0) {
9766 // define functions outside the loop, performance optimisation
9767 var foreachRow = function(grid, row, col, filters){
9768 if ( row.visible && !rowSearcher.searchColumn(grid, row, col, filters) ) {
9769 row.visible = false;
9773 var foreachFilterCol = function(grid, filterData){
9774 var rowsLength = rows.length;
9775 for ( var i = 0; i < rowsLength; i++){
9776 foreachRow(grid, rows[i], filterData.col, filterData.filters);
9780 // nested loop itself - foreachFilterCol, which in turn calls foreachRow
9781 var filterDataLength = filterData.length;
9782 for ( var j = 0; j < filterDataLength; j++){
9783 foreachFilterCol( grid, filterData[j] );
9786 if (grid.api.core.raise.rowsVisibleChanged) {
9787 grid.api.core.raise.rowsVisibleChanged();
9790 // drop any invisible rows
9791 // 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
9792 // rows = rows.filter(function(row){ return row.visible; });
9806 var module = angular.module('ui.grid');
9810 * @name ui.grid.class:RowSorter
9811 * @description RowSorter provides the default sorting mechanisms,
9812 * including guessing column types and applying appropriate sort
9817 module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGridConstants) {
9818 var currencyRegexStr =
9820 uiGridConstants.CURRENCY_SYMBOLS
9821 .map(function (a) { return '\\' + a; }) // Escape all the currency symbols ($ at least will jack up this regex)
9822 .join('|') + // Join all the symbols together with |s
9825 // /^[-+]?[£$¤¥]?[\d,.]+%?$/
9826 var numberStrRegex = new RegExp('^[-+]?' + currencyRegexStr + '[\\d,.]+' + currencyRegexStr + '%?$');
9829 // Cache of sorting functions. Once we create them, we don't want to keep re-doing it
9830 // this takes a piece of data from the cell and tries to determine its type and what sorting
9831 // function to use for it
9838 * @methodOf ui.grid.class:RowSorter
9840 * @description Assigns a sort function to use based on the itemType in the column
9841 * @param {string} itemType one of 'number', 'boolean', 'string', 'date', 'object'. And
9842 * error will be thrown for any other type.
9843 * @returns {function} a sort function that will sort that type
9845 rowSorter.guessSortFn = function guessSortFn(itemType) {
9848 return rowSorter.sortNumber;
9850 return rowSorter.sortNumberStr;
9852 return rowSorter.sortBool;
9854 return rowSorter.sortAlpha;
9856 return rowSorter.sortDate;
9858 return rowSorter.basicSort;
9860 throw new Error('No sorting function found for type:' + itemType);
9867 * @methodOf ui.grid.class:RowSorter
9869 * @description Sorts nulls and undefined to the bottom (top when
9870 * descending). Called by each of the internal sorters before
9871 * attempting to sort. Note that this method is available on the core api
9872 * via gridApi.core.sortHandleNulls
9873 * @param {object} a sort value a
9874 * @param {object} b sort value b
9875 * @returns {number} null if there were no nulls/undefineds, otherwise returns
9876 * a sort value that should be passed back from the sort function
9878 rowSorter.handleNulls = function handleNulls(a, b) {
9879 // We want to allow zero values and false values to be evaluated in the sort function
9880 if ((!a && a !== 0 && a !== false) || (!b && b !== 0 && b !== false)) {
9881 // We want to force nulls and such to the bottom when we sort... which effectively is "greater than"
9882 if ((!a && a !== 0 && a !== false) && (!b && b !== 0 && b !== false)) {
9885 else if (!a && a !== 0 && a !== false) {
9888 else if (!b && b !== 0 && b !== false) {
9898 * @methodOf ui.grid.class:RowSorter
9900 * @description Sorts any values that provide the < method, including strings
9901 * or numbers. Handles nulls and undefined through calling handleNulls
9902 * @param {object} a sort value a
9903 * @param {object} b sort value b
9904 * @returns {number} normal sort function, returns -ve, 0, +ve
9906 rowSorter.basicSort = function basicSort(a, b) {
9907 var nulls = rowSorter.handleNulls(a, b);
9908 if ( nulls !== null ){
9924 * @methodOf ui.grid.class:RowSorter
9926 * @description Sorts numerical values. Handles nulls and undefined through calling handleNulls
9927 * @param {object} a sort value a
9928 * @param {object} b sort value b
9929 * @returns {number} normal sort function, returns -ve, 0, +ve
9931 rowSorter.sortNumber = function sortNumber(a, b) {
9932 var nulls = rowSorter.handleNulls(a, b);
9933 if ( nulls !== null ){
9943 * @methodOf ui.grid.class:RowSorter
9944 * @name sortNumberStr
9945 * @description Sorts numerical values that are stored in a string (i.e. parses them to numbers first).
9946 * Handles nulls and undefined through calling handleNulls
9947 * @param {object} a sort value a
9948 * @param {object} b sort value b
9949 * @returns {number} normal sort function, returns -ve, 0, +ve
9951 rowSorter.sortNumberStr = function sortNumberStr(a, b) {
9952 var nulls = rowSorter.handleNulls(a, b);
9953 if ( nulls !== null ){
9956 var numA, // The parsed number form of 'a'
9957 numB, // The parsed number form of 'b'
9961 // Try to parse 'a' to a float
9962 numA = parseFloat(a.replace(/[^0-9.-]/g, ''));
9964 // If 'a' couldn't be parsed to float, flag it as bad
9969 // Try to parse 'b' to a float
9970 numB = parseFloat(b.replace(/[^0-9.-]/g, ''));
9972 // If 'b' couldn't be parsed to float, flag it as bad
9977 // We want bad ones to get pushed to the bottom... which effectively is "greater than"
9997 * @methodOf ui.grid.class:RowSorter
9999 * @description Sorts string values. Handles nulls and undefined through calling handleNulls
10000 * @param {object} a sort value a
10001 * @param {object} b sort value b
10002 * @returns {number} normal sort function, returns -ve, 0, +ve
10004 rowSorter.sortAlpha = function sortAlpha(a, b) {
10005 var nulls = rowSorter.handleNulls(a, b);
10006 if ( nulls !== null ){
10009 var strA = a.toString().toLowerCase(),
10010 strB = b.toString().toLowerCase();
10012 return strA === strB ? 0 : strA.localeCompare(strB);
10019 * @methodOf ui.grid.class:RowSorter
10021 * @description Sorts date values. Handles nulls and undefined through calling handleNulls.
10022 * Handles date strings by converting to Date object if not already an instance of Date
10023 * @param {object} a sort value a
10024 * @param {object} b sort value b
10025 * @returns {number} normal sort function, returns -ve, 0, +ve
10027 rowSorter.sortDate = function sortDate(a, b) {
10028 var nulls = rowSorter.handleNulls(a, b);
10029 if ( nulls !== null ){
10032 if (!(a instanceof Date)) {
10035 if (!(b instanceof Date)){
10038 var timeA = a.getTime(),
10039 timeB = b.getTime();
10041 return timeA === timeB ? 0 : (timeA < timeB ? -1 : 1);
10048 * @methodOf ui.grid.class:RowSorter
10050 * @description Sorts boolean values, true is considered larger than false.
10051 * Handles nulls and undefined through calling handleNulls
10052 * @param {object} a sort value a
10053 * @param {object} b sort value b
10054 * @returns {number} normal sort function, returns -ve, 0, +ve
10056 rowSorter.sortBool = function sortBool(a, b) {
10057 var nulls = rowSorter.handleNulls(a, b);
10058 if ( nulls !== null ){
10077 * @methodOf ui.grid.class:RowSorter
10079 * @description Get the sort function for the column. Looks first in
10080 * rowSorter.colSortFnCache using the column name, failing that it
10081 * looks at col.sortingAlgorithm (and puts it in the cache), failing that
10082 * it guesses the sort algorithm based on the data type.
10084 * The cache currently seems a bit pointless, as none of the work we do is
10085 * processor intensive enough to need caching. Presumably in future we might
10086 * inspect the row data itself to guess the sort function, and in that case
10087 * it would make sense to have a cache, the infrastructure is in place to allow
10090 * @param {Grid} grid the grid to consider
10091 * @param {GridCol} col the column to find a function for
10092 * @param {array} rows an array of grid rows. Currently unused, but presumably in future
10093 * we might inspect the rows themselves to decide what sort of data might be there
10094 * @returns {function} the sort function chosen for the column
10096 rowSorter.getSortFn = function getSortFn(grid, col, rows) {
10099 // See if we already figured out what to use to sort the column and have it in the cache
10100 if (rowSorter.colSortFnCache[col.colDef.name]) {
10101 sortFn = rowSorter.colSortFnCache[col.colDef.name];
10103 // If the column has its OWN sorting algorithm, use that
10104 else if (col.sortingAlgorithm !== undefined) {
10105 sortFn = col.sortingAlgorithm;
10106 rowSorter.colSortFnCache[col.colDef.name] = col.sortingAlgorithm;
10108 // Always default to sortAlpha when sorting after a cellFilter
10109 else if ( col.sortCellFiltered && col.cellFilter ){
10110 sortFn = rowSorter.sortAlpha;
10111 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10113 // Try and guess what sort function to use
10115 // Guess the sort function
10116 sortFn = rowSorter.guessSortFn(col.colDef.type);
10118 // If we found a sort function, cache it
10120 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10123 // We assign the alpha sort because anything that is null/undefined will never get passed to
10124 // the actual sorting function. It will get caught in our null check and returned to be sorted
10125 // down to the bottom
10126 sortFn = rowSorter.sortAlpha;
10137 * @methodOf ui.grid.class:RowSorter
10138 * @name prioritySort
10139 * @description Used where multiple columns are present in the sort criteria,
10140 * we determine which column should take precedence in the sort by sorting
10141 * the columns based on their sort.priority
10143 * @param {gridColumn} a column a
10144 * @param {gridColumn} b column b
10145 * @returns {number} normal sort function, returns -ve, 0, +ve
10147 rowSorter.prioritySort = function (a, b) {
10148 // Both columns have a sort priority
10149 if (a.sort.priority !== undefined && b.sort.priority !== undefined) {
10150 // A is higher priority
10151 if (a.sort.priority < b.sort.priority) {
10155 else if (a.sort.priority === b.sort.priority) {
10163 // Only A has a priority
10164 else if (a.sort.priority || a.sort.priority === 0) {
10167 // Only B has a priority
10168 else if (b.sort.priority || b.sort.priority === 0) {
10171 // Neither has a priority
10180 * @name useExternalSorting
10181 * @propertyOf ui.grid.class:GridOptions
10182 * @description Prevents the internal sorting from executing. Events will
10183 * still be fired when the sort changes, and the sort information on
10184 * the columns will be updated, allowing an external sorter (for example,
10185 * server sorting) to be implemented. Defaults to false.
10190 * @methodOf ui.grid.class:RowSorter
10192 * @description sorts the grid
10193 * @param {Object} grid the grid itself
10194 * @param {array} rows the rows to be sorted
10195 * @param {array} columns the columns in which to look
10196 * for sort criteria
10197 * @returns {array} sorted rows
10199 rowSorter.sort = function rowSorterSort(grid, rows, columns) {
10200 // first make sure we are even supposed to do work
10205 if (grid.options.useExternalSorting){
10209 // Build the list of columns to sort by
10211 columns.forEach(function (col) {
10212 if (col.sort && !col.sort.ignoreSort && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
10213 sortCols.push(col);
10217 // Sort the "sort columns" by their sort priority
10218 sortCols = sortCols.sort(rowSorter.prioritySort);
10220 // Now rows to sort by, maintain original order
10221 if (sortCols.length === 0) {
10225 // Re-usable variables
10226 var col, direction;
10228 // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10229 var setIndex = function( row, idx ){
10230 row.entity.$$uiGridIndex = idx;
10232 rows.forEach(setIndex);
10234 // IE9-11 HACK.... the 'rows' variable would be empty where we call rowSorter.getSortFn(...) below. We have to use a separate reference
10235 // var d = data.slice(0);
10236 var r = rows.slice(0);
10238 // Now actually sort the data
10239 var rowSortFn = function (rowA, rowB) {
10244 while (tem === 0 && idx < sortCols.length) {
10245 // grab the metadata for the rest of the logic
10246 col = sortCols[idx];
10247 direction = sortCols[idx].sort.direction;
10249 sortFn = rowSorter.getSortFn(grid, col, r);
10253 if ( col.sortCellFiltered ){
10254 propA = grid.getCellDisplayValue(rowA, col);
10255 propB = grid.getCellDisplayValue(rowB, col);
10257 propA = grid.getCellValue(rowA, col);
10258 propB = grid.getCellValue(rowB, col);
10261 tem = sortFn(propA, propB, rowA, rowB, direction);
10266 // Chrome doesn't implement a stable sort function. If our sort returns 0
10267 // (i.e. the items are equal), and we're at the last sort column in the list,
10268 // then return the previous order using our custom
10271 return rowA.entity.$$uiGridIndex - rowB.entity.$$uiGridIndex;
10274 // Made it this far, we don't have to worry about null & undefined
10275 if (direction === uiGridConstants.ASC) {
10282 var newRows = rows.sort(rowSortFn);
10284 // remove the custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10285 var clearIndex = function( row, idx ){
10286 delete row.entity.$$uiGridIndex;
10288 rows.forEach(clearIndex);
10300 var module = angular.module('ui.grid');
10303 if (typeof Function.prototype.bind !== "function") {
10304 bindPolyfill = function() {
10305 var slice = Array.prototype.slice;
10306 return function(context) {
10308 args = slice.call(arguments, 1);
10310 return function() {
10311 return arguments.length ? fn.apply(context, args.concat(slice.call(arguments))) : fn.apply(context, args);
10314 return function() {
10315 return arguments.length ? fn.apply(context, arguments) : fn.call(context);
10321 function getStyles (elem) {
10323 if (typeof(e.length) !== 'undefined' && e.length) {
10327 return e.ownerDocument.defaultView.getComputedStyle(e, null);
10330 var rnumnonpx = new RegExp( "^(" + (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source + ")(?!px)[a-z%]+$", "i" ),
10331 // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
10332 // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
10333 rdisplayswap = /^(block|none|table(?!-c[ea]).+)/,
10334 cssShow = { position: "absolute", visibility: "hidden", display: "block" };
10336 function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
10337 var i = extra === ( isBorderBox ? 'border' : 'content' ) ?
10338 // If we already have the right measurement, avoid augmentation
10340 // Otherwise initialize for horizontal or vertical properties
10341 name === 'width' ? 1 : 0,
10345 var sides = ['Top', 'Right', 'Bottom', 'Left'];
10347 for ( ; i < 4; i += 2 ) {
10348 var side = sides[i];
10349 // dump('side', side);
10351 // both box models exclude margin, so add it if we want it
10352 if ( extra === 'margin' ) {
10353 var marg = parseFloat(styles[extra + side]);
10354 if (!isNaN(marg)) {
10358 // dump('val1', val);
10360 if ( isBorderBox ) {
10361 // border-box includes padding, so remove it if we want content
10362 if ( extra === 'content' ) {
10363 var padd = parseFloat(styles['padding' + side]);
10364 if (!isNaN(padd)) {
10366 // dump('val2', val);
10370 // at this point, extra isn't border nor margin, so remove border
10371 if ( extra !== 'margin' ) {
10372 var bordermarg = parseFloat(styles['border' + side + 'Width']);
10373 if (!isNaN(bordermarg)) {
10375 // dump('val3', val);
10380 // at this point, extra isn't content, so add padding
10381 var nocontentPad = parseFloat(styles['padding' + side]);
10382 if (!isNaN(nocontentPad)) {
10383 val += nocontentPad;
10384 // dump('val4', val);
10387 // at this point, extra isn't content nor padding, so add border
10388 if ( extra !== 'padding') {
10389 var nocontentnopad = parseFloat(styles['border' + side + 'Width']);
10390 if (!isNaN(nocontentnopad)) {
10391 val += nocontentnopad;
10392 // dump('val5', val);
10398 // dump('augVal', val);
10403 function getWidthOrHeight( elem, name, extra ) {
10404 // Start with offset property, which is equivalent to the border-box value
10405 var valueIsBorderBox = true,
10406 val, // = name === 'width' ? elem.offsetWidth : elem.offsetHeight,
10407 styles = getStyles(elem),
10408 isBorderBox = styles['boxSizing'] === 'border-box';
10410 // some non-html elements return undefined for offsetWidth, so check for null/undefined
10411 // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
10412 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
10413 if ( val <= 0 || val == null ) {
10414 // Fall back to computed then uncomputed css if necessary
10415 val = styles[name];
10416 if ( val < 0 || val == null ) {
10417 val = elem.style[ name ];
10420 // Computed unit is not pixels. Stop here and return.
10421 if ( rnumnonpx.test(val) ) {
10425 // we need the check for style in case a browser which returns unreliable values
10426 // for getComputedStyle silently falls back to the reliable elem.style
10427 valueIsBorderBox = isBorderBox &&
10428 ( true || val === elem.style[ name ] ); // use 'true' instead of 'support.boxSizingReliable()'
10430 // Normalize "", auto, and prepare for extra
10431 val = parseFloat( val ) || 0;
10434 // use the active box-sizing model to add/subtract irrelevant styles
10436 augmentWidthOrHeight(
10439 extra || ( isBorderBox ? "border" : "content" ),
10445 // dump('ret', ret, val);
10449 function getLineHeight(elm) {
10450 elm = angular.element(elm)[0];
10451 var parent = elm.parentElement;
10454 parent = document.getElementsByTagName('body')[0];
10457 return parseInt( getStyles(parent).fontSize ) || parseInt( getStyles(elm).fontSize ) || 16;
10460 var uid = ['0', '0', '0', '0'];
10461 var uidPrefix = 'uiGrid-';
10465 * @name ui.grid.service:GridUtil
10467 * @description Grid utility functions
10469 module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateCache', '$timeout', '$interval', '$injector', '$q', '$interpolate', 'uiGridConstants',
10470 function ($log, $window, $document, $http, $templateCache, $timeout, $interval, $injector, $q, $interpolate, uiGridConstants) {
10473 augmentWidthOrHeight: augmentWidthOrHeight,
10475 getStyles: getStyles,
10479 * @name createBoundedWrapper
10480 * @methodOf ui.grid.service:GridUtil
10482 * @param {object} Object to bind 'this' to
10483 * @param {method} Method to bind
10484 * @returns {Function} The wrapper that performs the binding
10487 * Binds given method to given object.
10489 * By means of a wrapper, ensures that ``method`` is always bound to
10490 * ``object`` regardless of its calling environment.
10491 * Iow, inside ``method``, ``this`` always points to ``object``.
10493 * See http://alistapart.com/article/getoutbindingsituations
10496 createBoundedWrapper: function(object, method) {
10497 return function() {
10498 return method.apply(object, arguments);
10505 * @name readableColumnName
10506 * @methodOf ui.grid.service:GridUtil
10508 * @param {string} columnName Column name as a string
10509 * @returns {string} Column name appropriately capitalized and split apart
10512 <example module="app">
10513 <file name="app.js">
10514 var app = angular.module('app', ['ui.grid']);
10516 app.controller('MainCtrl', ['$scope', 'gridUtil', function ($scope, gridUtil) {
10517 $scope.name = 'firstName';
10518 $scope.columnName = function(name) {
10519 return gridUtil.readableColumnName(name);
10523 <file name="index.html">
10524 <div ng-controller="MainCtrl">
10525 <strong>Column name:</strong> <input ng-model="name" />
10527 <strong>Output:</strong> <span ng-bind="columnName(name)"></span>
10532 readableColumnName: function (columnName) {
10533 // Convert underscores to spaces
10534 if (typeof(columnName) === 'undefined' || columnName === undefined || columnName === null) { return columnName; }
10536 if (typeof(columnName) !== 'string') {
10537 columnName = String(columnName);
10540 return columnName.replace(/_+/g, ' ')
10541 // Replace a completely all-capsed word with a first-letter-capitalized version
10542 .replace(/^[A-Z]+$/, function (match) {
10543 return angular.lowercase(angular.uppercase(match.charAt(0)) + match.slice(1));
10545 // Capitalize the first letter of words
10546 .replace(/([\w\u00C0-\u017F]+)/g, function (match) {
10547 return angular.uppercase(match.charAt(0)) + match.slice(1);
10549 // Put a space in between words that have partial capilizations (i.e. 'firstName' becomes 'First Name')
10550 // .replace(/([A-Z]|[A-Z]\w+)([A-Z])/g, "$1 $2");
10551 // .replace(/(\w+?|\w)([A-Z])/g, "$1 $2");
10552 .replace(/(\w+?(?=[A-Z]))/g, '$1 ');
10557 * @name getColumnsFromData
10558 * @methodOf ui.grid.service:GridUtil
10559 * @description Return a list of column names, given a data set
10561 * @param {string} data Data array for grid
10562 * @returns {Object} Column definitions with field accessor and column name
10567 { firstName: 'Bob', lastName: 'Jones' },
10568 { firstName: 'Frank', lastName: 'Smith' }
10571 var columnDefs = GridUtil.getColumnsFromData(data, excludeProperties);
10575 field: 'firstName',
10585 getColumnsFromData: function (data, excludeProperties) {
10586 var columnDefs = [];
10588 if (!data || typeof(data[0]) === 'undefined' || data[0] === undefined) { return []; }
10589 if (angular.isUndefined(excludeProperties)) { excludeProperties = []; }
10591 var item = data[0];
10593 angular.forEach(item,function (prop, propName) {
10594 if ( excludeProperties.indexOf(propName) === -1){
10607 * @methodOf ui.grid.service:GridUtil
10608 * @description Return a unique ID string
10610 * @returns {string} Unique string
10614 var id = GridUtil.newId();
10619 newId: (function() {
10620 var seedId = new Date().getTime();
10621 return function() {
10622 return seedId += 1;
10629 * @name getTemplate
10630 * @methodOf ui.grid.service:GridUtil
10631 * @description Get's template from cache / element / url
10633 * @param {string|element|promise} Either a string representing the template id, a string representing the template url,
10634 * an jQuery/Angualr element, or a promise that returns the template contents to use.
10635 * @returns {object} a promise resolving to template contents
10639 GridUtil.getTemplate(url).then(function (contents) {
10644 getTemplate: function (template) {
10645 // Try to fetch the template out of the templateCache
10646 if ($templateCache.get(template)) {
10647 return s.postProcessTemplate($templateCache.get(template));
10650 // See if the template is itself a promise
10651 if (template.hasOwnProperty('then')) {
10652 return template.then(s.postProcessTemplate);
10655 // If the template is an element, return the element
10657 if (angular.element(template).length > 0) {
10658 return $q.when(template).then(s.postProcessTemplate);
10662 //do nothing; not valid html
10665 s.logDebug('fetching url', template);
10667 // Default to trying to fetch the template as a url with $http
10668 return $http({ method: 'GET', url: template})
10670 function (result) {
10671 var templateHtml = result.data.trim();
10672 //put in templateCache for next call
10673 $templateCache.put(template, templateHtml);
10674 return templateHtml;
10677 throw new Error("Could not get template " + template + ": " + err);
10680 .then(s.postProcessTemplate);
10684 postProcessTemplate: function (template) {
10685 var startSym = $interpolate.startSymbol(),
10686 endSym = $interpolate.endSymbol();
10688 // If either of the interpolation symbols have been changed, we need to alter this template
10689 if (startSym !== '{{' || endSym !== '}}') {
10690 template = template.replace(/\{\{/g, startSym);
10691 template = template.replace(/\}\}/g, endSym);
10694 return $q.when(template);
10700 * @methodOf ui.grid.service:GridUtil
10701 * @description guesses the type of an argument
10703 * @param {string/number/bool/object} item variable to examine
10704 * @returns {string} one of the following
10711 guessType : function (item) {
10712 var itemType = typeof(item);
10714 // Check for numbers and booleans
10715 switch (itemType) {
10721 if (angular.isDate(item)) {
10731 * @name elementWidth
10732 * @methodOf ui.grid.service:GridUtil
10734 * @param {element} element DOM element
10735 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10737 * @returns {number} Element width in pixels, accounting for any borders, etc.
10739 elementWidth: function (elem) {
10745 * @name elementHeight
10746 * @methodOf ui.grid.service:GridUtil
10748 * @param {element} element DOM element
10749 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10751 * @returns {number} Element height in pixels, accounting for any borders, etc.
10753 elementHeight: function (elem) {
10757 // Thanks to http://stackoverflow.com/a/13382873/888165
10758 getScrollbarWidth: function() {
10759 var outer = document.createElement("div");
10760 outer.style.visibility = "hidden";
10761 outer.style.width = "100px";
10762 outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
10764 document.body.appendChild(outer);
10766 var widthNoScroll = outer.offsetWidth;
10767 // force scrollbars
10768 outer.style.overflow = "scroll";
10771 var inner = document.createElement("div");
10772 inner.style.width = "100%";
10773 outer.appendChild(inner);
10775 var widthWithScroll = inner.offsetWidth;
10778 outer.parentNode.removeChild(outer);
10780 return widthNoScroll - widthWithScroll;
10783 swap: function( elem, options, callback, args ) {
10787 // Remember the old values, and insert the new ones
10788 for ( name in options ) {
10789 old[ name ] = elem.style[ name ];
10790 elem.style[ name ] = options[ name ];
10793 ret = callback.apply( elem, args || [] );
10795 // Revert the old values
10796 for ( name in options ) {
10797 elem.style[ name ] = old[ name ];
10803 fakeElement: function( elem, options, callback, args ) {
10805 newElement = angular.element(elem).clone()[0];
10807 for ( name in options ) {
10808 newElement.style[ name ] = options[ name ];
10811 angular.element(document.body).append(newElement);
10813 ret = callback.call( newElement, newElement );
10815 angular.element(newElement).remove();
10822 * @name normalizeWheelEvent
10823 * @methodOf ui.grid.service:GridUtil
10825 * @param {event} event A mouse wheel event
10827 * @returns {event} A normalized event
10830 * Given an event from this list:
10832 * `wheel, mousewheel, DomMouseScroll, MozMousePixelScroll`
10835 * so that it stays consistent no matter what browser it comes from (i.e. scale it correctly and make sure the direction is right.)
10837 normalizeWheelEvent: function (event) {
10838 // var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];
10839 // var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
10840 var lowestDelta, lowestDeltaXY;
10842 var orgEvent = event || window.event,
10843 args = [].slice.call(arguments, 1),
10851 // event = $.event.fix(orgEvent);
10852 // event.type = 'mousewheel';
10854 // NOTE: jQuery masks the event and stores it in the event as originalEvent
10855 if (orgEvent.originalEvent) {
10856 orgEvent = orgEvent.originalEvent;
10859 // Old school scrollwheel delta
10860 if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; }
10861 if ( orgEvent.detail ) { delta = orgEvent.detail * -1; }
10863 // At a minimum, setup the deltaY to be delta
10866 // Firefox < 17 related to DOMMouseScroll event
10867 if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
10869 deltaX = delta * -1;
10872 // New school wheel delta (wheel event)
10873 if ( orgEvent.deltaY ) {
10874 deltaY = orgEvent.deltaY * -1;
10877 if ( orgEvent.deltaX ) {
10878 deltaX = orgEvent.deltaX;
10879 delta = deltaX * -1;
10883 if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; }
10884 if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX; }
10886 // Look for lowest delta to normalize the delta values
10887 absDelta = Math.abs(delta);
10888 if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; }
10889 absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX));
10890 if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; }
10892 // Get a whole value for the deltas
10893 fn = delta > 0 ? 'floor' : 'ceil';
10894 delta = Math[fn](delta / lowestDelta);
10895 deltaX = Math[fn](deltaX / lowestDeltaXY);
10896 deltaY = Math[fn](deltaY / lowestDeltaXY);
10905 // Stolen from Modernizr
10906 // TODO: make this, and everythign that flows from it, robust
10907 //http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
10908 isTouchEnabled: function() {
10911 if (('ontouchstart' in $window) || $window.DocumentTouch && $document instanceof DocumentTouch) {
10918 isNullOrUndefined: function(obj) {
10919 if (obj === undefined || obj === null) {
10925 endsWith: function(str, suffix) {
10926 if (!str || !suffix || typeof str !== "string") {
10929 return str.indexOf(suffix, str.length - suffix.length) !== -1;
10932 arrayContainsObjectWithProperty: function(array, propertyName, propertyValue) {
10934 angular.forEach(array, function (object) {
10935 if (object[propertyName] === propertyValue) {
10942 //// Shim requestAnimationFrame
10943 //requestAnimationFrame: $window.requestAnimationFrame && $window.requestAnimationFrame.bind($window) ||
10944 // $window.webkitRequestAnimationFrame && $window.webkitRequestAnimationFrame.bind($window) ||
10946 // return $timeout(fn, 10, false);
10949 numericAndNullSort: function (a, b) {
10950 if (a === null) { return 1; }
10951 if (b === null) { return -1; }
10952 if (a === null && b === null) { return 0; }
10956 // Disable ngAnimate animations on an element
10957 disableAnimations: function (element) {
10960 $animate = $injector.get('$animate');
10961 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10962 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10963 $animate.enabled(element, false);
10965 $animate.enabled(false, element);
10971 enableAnimations: function (element) {
10974 $animate = $injector.get('$animate');
10975 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10976 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10977 $animate.enabled(element, true);
10979 $animate.enabled(true, element);
10986 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
10987 nextUid: function nextUid() {
10988 var index = uid.length;
10993 digit = uid[index].charCodeAt(0);
10994 if (digit === 57 /*'9'*/) {
10996 return uidPrefix + uid.join('');
10998 if (digit === 90 /*'Z'*/) {
11001 uid[index] = String.fromCharCode(digit + 1);
11002 return uidPrefix + uid.join('');
11007 return uidPrefix + uid.join('');
11010 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
11011 hashKey: function hashKey(obj) {
11012 var objType = typeof obj,
11015 if (objType === 'object' && obj !== null) {
11016 if (typeof (key = obj.$$hashKey) === 'function') {
11017 // must invoke on object to keep the right this
11018 key = obj.$$hashKey();
11020 else if (typeof(obj.$$hashKey) !== 'undefined' && obj.$$hashKey) {
11021 key = obj.$$hashKey;
11023 else if (key === undefined) {
11024 key = obj.$$hashKey = s.nextUid();
11031 return objType + ':' + key;
11034 resetUids: function () {
11035 uid = ['0', '0', '0'];
11040 * @methodOf ui.grid.service:GridUtil
11042 * @description wraps the $log method, allowing us to choose different
11043 * treatment within ui-grid if we so desired. At present we only log
11044 * error messages if uiGridConstants.LOG_ERROR_MESSAGES is set to true
11045 * @param {string} logMessage message to be logged to the console
11048 logError: function( logMessage ){
11049 if ( uiGridConstants.LOG_ERROR_MESSAGES ){
11050 $log.error( logMessage );
11056 * @methodOf ui.grid.service:GridUtil
11058 * @description wraps the $log method, allowing us to choose different
11059 * treatment within ui-grid if we so desired. At present we only log
11060 * warning messages if uiGridConstants.LOG_WARN_MESSAGES is set to true
11061 * @param {string} logMessage message to be logged to the console
11064 logWarn: function( logMessage ){
11065 if ( uiGridConstants.LOG_WARN_MESSAGES ){
11066 $log.warn( logMessage );
11072 * @methodOf ui.grid.service:GridUtil
11074 * @description wraps the $log method, allowing us to choose different
11075 * treatment within ui-grid if we so desired. At present we only log
11076 * debug messages if uiGridConstants.LOG_DEBUG_MESSAGES is set to true
11079 logDebug: function() {
11080 if ( uiGridConstants.LOG_DEBUG_MESSAGES ){
11081 $log.debug.apply($log, arguments);
11090 * @propertyOf ui.grid.service:GridUtil
11091 * @description Provies a set of methods to set the document focus inside the grid.
11092 * See {@link ui.grid.service:GridUtil.focus} for more information.
11097 * @name ui.grid.service:GridUtil.focus
11098 * @description Provies a set of methods to set the document focus inside the grid.
11099 * Timeouts are utilized to ensure that the focus is invoked after any other event has been triggered.
11100 * e.g. click events that need to run before the focus or
11101 * inputs elements that are in a disabled state but are enabled when those events
11106 //http://stackoverflow.com/questions/25596399/set-element-focus-in-angular-way
11109 * @methodOf ui.grid.service:GridUtil.focus
11111 * @description Sets the focus of the document to the given id value.
11112 * If provided with the grid object it will automatically append the grid id.
11113 * This is done to encourage unique dom id's as it allows for multiple grids on a
11115 * @param {String} id the id of the dom element to set the focus on
11116 * @param {Object=} Grid the grid object for this grid instance. See: {@link ui.grid.class:Grid}
11117 * @param {Number} Grid.id the unique id for this grid. Already set on an initialized grid object.
11118 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11119 * then the promise will fail with the `'canceled'` reason.
11121 byId: function (id, Grid) {
11122 this._purgeQueue();
11123 var promise = $timeout(function() {
11124 var elementID = (Grid && Grid.id ? Grid.id + '-' : '') + id;
11125 var element = $window.document.getElementById(elementID);
11129 s.logWarn('[focus.byId] Element id ' + elementID + ' was not found.');
11132 this.queue.push(promise);
11138 * @methodOf ui.grid.service:GridUtil.focus
11140 * @description Sets the focus of the document to the given dom element.
11141 * @param {(element|angular.element)} element the DOM element to set the focus on
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 byElement: function(element){
11146 if (!angular.isElement(element)){
11147 s.logWarn("Trying to focus on an element that isn\'t an element.");
11148 return $q.reject('not-element');
11150 element = angular.element(element);
11151 this._purgeQueue();
11152 var promise = $timeout(function(){
11154 element[0].focus();
11157 this.queue.push(promise);
11162 * @methodOf ui.grid.service:GridUtil.focus
11164 * @description Sets the focus of the document to the given dom element.
11165 * @param {(element|angular.element)} parentElement the parent/ancestor of the dom element that you are selecting using the query selector
11166 * @param {String} querySelector finds the dom element using the {@link http://www.w3schools.com/jsref/met_document_queryselector.asp querySelector}
11167 * @param {boolean} [aSync=false] If true then the selector will be querried inside of a timeout. Otherwise the selector will be querried imidately
11168 * then the focus will be called.
11169 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11170 * then the promise will fail with the `'canceled'` reason.
11172 bySelector: function(parentElement, querySelector, aSync){
11174 if (!angular.isElement(parentElement)){
11175 throw new Error("The parent element is not an element.");
11177 // Ensure that this is an angular element.
11178 // It is fine if this is already an angular element.
11179 parentElement = angular.element(parentElement);
11180 var focusBySelector = function(){
11181 var element = parentElement[0].querySelector(querySelector);
11182 return self.byElement(element);
11184 this._purgeQueue();
11185 if (aSync){ //Do this asynchronysly
11186 var promise = $timeout(focusBySelector);
11187 this.queue.push($timeout(focusBySelector));
11190 return focusBySelector();
11193 _purgeQueue: function(){
11194 this.queue.forEach(function(element){
11195 $timeout.cancel(element);
11202 ['width', 'height'].forEach(function (name) {
11203 var capsName = angular.uppercase(name.charAt(0)) + name.substr(1);
11204 s['element' + capsName] = function (elem, extra) {
11206 if (e && typeof(e.length) !== 'undefined' && e.length) {
11211 var styles = getStyles(e);
11212 return e.offsetWidth === 0 && rdisplayswap.test(styles.display) ?
11213 s.swap(e, cssShow, function() {
11214 return getWidthOrHeight(e, name, extra );
11216 getWidthOrHeight( e, name, extra );
11223 s['outerElement' + capsName] = function (elem, margin) {
11224 return elem ? s['element' + capsName].call(this, elem, margin ? 'margin' : 'border') : null;
11228 // http://stackoverflow.com/a/24107550/888165
11229 s.closestElm = function closestElm(el, selector) {
11230 if (typeof(el.length) !== 'undefined' && el.length) {
11236 // find vendor prefix
11237 ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) {
11238 if (typeof document.body[fn] === 'function') {
11245 // traverse parents
11247 while (el !== null) {
11248 parent = el.parentElement;
11249 if (parent !== null && parent[matchesFn](selector)) {
11258 s.type = function (obj) {
11259 var text = Function.prototype.toString.call(obj.constructor);
11260 return text.match(/function (.*?)\(/)[1];
11263 s.getBorderSize = function getBorderSize(elem, borderType) {
11264 if (typeof(elem.length) !== 'undefined' && elem.length) {
11268 var styles = getStyles(elem);
11270 // If a specific border is supplied, like 'top', read the 'borderTop' style property
11272 borderType = 'border' + borderType.charAt(0).toUpperCase() + borderType.slice(1);
11275 borderType = 'border';
11278 borderType += 'Width';
11280 var val = parseInt(styles[borderType], 10);
11290 // http://stackoverflow.com/a/22948274/888165
11291 // TODO: Opera? Mobile?
11292 s.detectBrowser = function detectBrowser() {
11293 var userAgent = $window.navigator.userAgent;
11295 var browsers = {chrome: /chrome/i, safari: /safari/i, firefox: /firefox/i, ie: /internet explorer|trident\//i};
11297 for (var key in browsers) {
11298 if (browsers[key].test(userAgent)) {
11306 // Borrowed from https://github.com/othree/jquery.rtl-scroll-type
11307 // Determine the scroll "type" this browser is using for RTL
11308 s.rtlScrollType = function rtlScrollType() {
11309 if (rtlScrollType.type) {
11310 return rtlScrollType.type;
11313 var definer = angular.element('<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>')[0],
11316 document.body.appendChild(definer);
11318 if (definer.scrollLeft > 0) {
11322 definer.scrollLeft = 1;
11323 if (definer.scrollLeft === 0) {
11328 angular.element(definer).remove();
11329 rtlScrollType.type = type;
11336 * @name normalizeScrollLeft
11337 * @methodOf ui.grid.service:GridUtil
11339 * @param {element} element The element to get the `scrollLeft` from.
11340 * @param {grid} grid - grid used to normalize (uses the rtl property)
11342 * @returns {number} A normalized scrollLeft value for the current browser.
11345 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method normalizes them
11347 s.normalizeScrollLeft = function normalizeScrollLeft(element, grid) {
11348 if (typeof(element.length) !== 'undefined' && element.length) {
11349 element = element[0];
11352 var scrollLeft = element.scrollLeft;
11354 if (grid.isRTL()) {
11355 switch (s.rtlScrollType()) {
11357 return element.scrollWidth - scrollLeft - element.clientWidth;
11359 return Math.abs(scrollLeft);
11370 * @name denormalizeScrollLeft
11371 * @methodOf ui.grid.service:GridUtil
11373 * @param {element} element The element to normalize the `scrollLeft` value for
11374 * @param {number} scrollLeft The `scrollLeft` value to denormalize.
11375 * @param {grid} grid The grid that owns the scroll event.
11377 * @returns {number} A normalized scrollLeft value for the current browser.
11380 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method denormalizes a value for the current browser.
11382 s.denormalizeScrollLeft = function denormalizeScrollLeft(element, scrollLeft, grid) {
11383 if (typeof(element.length) !== 'undefined' && element.length) {
11384 element = element[0];
11387 if (grid.isRTL()) {
11388 switch (s.rtlScrollType()) {
11390 // Get the max scroll for the element
11391 var maxScrollLeft = element.scrollWidth - element.clientWidth;
11393 // Subtract the current scroll amount from the max scroll
11394 return maxScrollLeft - scrollLeft;
11396 return scrollLeft * -1;
11408 * @methodOf ui.grid.service:GridUtil
11410 * @param {string} path Path to evaluate
11412 * @returns {string} A path that is normalized.
11415 * Takes a field path and converts it to bracket notation to allow for special characters in path
11418 * gridUtil.preEval('property') == 'property'
11419 * gridUtil.preEval('nested.deep.prop-erty') = "nested['deep']['prop-erty']"
11422 s.preEval = function (path) {
11423 var m = uiGridConstants.BRACKET_REGEXP.exec(path);
11425 return (m[1] ? s.preEval(m[1]) : m[1]) + m[2] + (m[3] ? s.preEval(m[3]) : m[3]);
11427 path = path.replace(uiGridConstants.APOS_REGEXP, '\\\'');
11428 var parts = path.split(uiGridConstants.DOT_REGEXP);
11429 var preparsed = [parts.shift()]; // first item must be var notation, thus skip
11430 angular.forEach(parts, function (part) {
11431 preparsed.push(part.replace(uiGridConstants.FUNC_REGEXP, '\']$1'));
11433 return preparsed.join('[\'');
11440 * @methodOf ui.grid.service:GridUtil
11442 * @param {function} func function to debounce
11443 * @param {number} wait milliseconds to delay
11444 * @param {boolean} immediate execute before delay
11446 * @returns {function} A function that can be executed as debounced function
11449 * Copied from https://github.com/shahata/angular-debounce
11450 * Takes a function, decorates it to execute only 1 time after multiple calls, and returns the decorated function
11453 * var debouncedFunc = gridUtil.debounce(function(){alert('debounced');}, 500);
11459 s.debounce = function (func, wait, immediate) {
11460 var timeout, args, context, result;
11461 function debounce() {
11462 /* jshint validthis:true */
11465 var later = function () {
11468 result = func.apply(context, args);
11471 var callNow = immediate && !timeout;
11473 $timeout.cancel(timeout);
11475 timeout = $timeout(later, wait);
11477 result = func.apply(context, args);
11481 debounce.cancel = function () {
11482 $timeout.cancel(timeout);
11491 * @methodOf ui.grid.service:GridUtil
11493 * @param {function} func function to throttle
11494 * @param {number} wait milliseconds to delay after first trigger
11495 * @param {Object} params to use in throttle.
11497 * @returns {function} A function that can be executed as throttled function
11500 * Adapted from debounce function (above)
11501 * Potential keys for Params Object are:
11502 * trailing (bool) - whether to trigger after throttle time ends if called multiple times
11503 * Updated to use $interval rather than $timeout, as protractor (e2e tests) is able to work with $interval,
11504 * but not with $timeout
11506 * Note that when using throttle, you need to use throttle to create a new function upfront, then use the function
11507 * return from that call each time you need to call throttle. If you call throttle itself repeatedly, the lastCall
11508 * variable will get overwritten and the throttling won't work
11512 * var throttledFunc = gridUtil.throttle(function(){console.log('throttled');}, 500, {trailing: true});
11513 * throttledFunc(); //=> logs throttled
11514 * throttledFunc(); //=> queues attempt to log throttled for ~500ms (since trailing param is truthy)
11515 * throttledFunc(); //=> updates arguments to keep most-recent request, but does not do anything else.
11518 s.throttle = function(func, wait, options){
11519 options = options || {};
11520 var lastCall = 0, queued = null, context, args;
11522 function runFunc(endDate){
11523 lastCall = +new Date();
11524 func.apply(context, args);
11525 $interval(function(){ queued = null; }, 0, 1);
11529 /* jshint validthis:true */
11532 if (queued === null){
11533 var sinceLast = +new Date() - lastCall;
11534 if (sinceLast > wait){
11537 else if (options.trailing){
11538 queued = $interval(runFunc, wait - sinceLast, 1);
11548 s.addOff = function (eventName) {
11549 s.off[eventName] = function (elm, fn) {
11550 var idx = s._events[eventName].indexOf(fn);
11552 s._events[eventName].removeAt(idx);
11557 var mouseWheeltoBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
11558 nullLowestDeltaTimeout,
11561 s.on.mousewheel = function (elm, fn) {
11562 if (!elm || !fn) { return; }
11564 var $elm = angular.element(elm);
11566 // Store the line height and page height for this particular element
11567 $elm.data('mousewheel-line-height', getLineHeight($elm));
11568 $elm.data('mousewheel-page-height', s.elementHeight($elm));
11569 if (!$elm.data('mousewheel-callbacks')) { $elm.data('mousewheel-callbacks', {}); }
11571 var cbs = $elm.data('mousewheel-callbacks');
11572 cbs[fn] = (Function.prototype.bind || bindPolyfill).call(mousewheelHandler, $elm[0], fn);
11574 // Bind all the mousew heel events
11575 for ( var i = mouseWheeltoBind.length; i; ) {
11576 $elm.on(mouseWheeltoBind[--i], cbs[fn]);
11579 s.off.mousewheel = function (elm, fn) {
11580 var $elm = angular.element(elm);
11582 var cbs = $elm.data('mousewheel-callbacks');
11583 var handler = cbs[fn];
11586 for ( var i = mouseWheeltoBind.length; i; ) {
11587 $elm.off(mouseWheeltoBind[--i], handler);
11593 if (Object.keys(cbs).length === 0) {
11594 $elm.removeData('mousewheel-line-height');
11595 $elm.removeData('mousewheel-page-height');
11596 $elm.removeData('mousewheel-callbacks');
11600 function mousewheelHandler(fn, event) {
11601 var $elm = angular.element(this);
11610 // jQuery masks events
11611 if (event.originalEvent) { event = event.originalEvent; }
11613 if ( 'detail' in event ) { deltaY = event.detail * -1; }
11614 if ( 'wheelDelta' in event ) { deltaY = event.wheelDelta; }
11615 if ( 'wheelDeltaY' in event ) { deltaY = event.wheelDeltaY; }
11616 if ( 'wheelDeltaX' in event ) { deltaX = event.wheelDeltaX * -1; }
11618 // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
11619 if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
11620 deltaX = deltaY * -1;
11624 // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
11625 delta = deltaY === 0 ? deltaX : deltaY;
11627 // New school wheel delta (wheel event)
11628 if ( 'deltaY' in event ) {
11629 deltaY = event.deltaY * -1;
11632 if ( 'deltaX' in event ) {
11633 deltaX = event.deltaX;
11634 if ( deltaY === 0 ) { delta = deltaX * -1; }
11637 // No change actually happened, no reason to go any further
11638 if ( deltaY === 0 && deltaX === 0 ) { return; }
11640 // Need to convert lines and pages to pixels if we aren't already in pixels
11641 // There are three delta modes:
11642 // * deltaMode 0 is by pixels, nothing to do
11643 // * deltaMode 1 is by lines
11644 // * deltaMode 2 is by pages
11645 if ( event.deltaMode === 1 ) {
11646 var lineHeight = $elm.data('mousewheel-line-height');
11647 delta *= lineHeight;
11648 deltaY *= lineHeight;
11649 deltaX *= lineHeight;
11651 else if ( event.deltaMode === 2 ) {
11652 var pageHeight = $elm.data('mousewheel-page-height');
11653 delta *= pageHeight;
11654 deltaY *= pageHeight;
11655 deltaX *= pageHeight;
11658 // Store lowest absolute delta to normalize the delta values
11659 absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );
11661 if ( !lowestDelta || absDelta < lowestDelta ) {
11662 lowestDelta = absDelta;
11664 // Adjust older deltas if necessary
11665 if ( shouldAdjustOldDeltas(event, absDelta) ) {
11670 // Get a whole, normalized value for the deltas
11671 delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta);
11672 deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
11673 deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);
11675 event.deltaMode = 0;
11677 // Normalise offsetX and offsetY properties
11678 // if ($elm[0].getBoundingClientRect ) {
11679 // var boundingRect = $(elm)[0].getBoundingClientRect();
11680 // offsetX = event.clientX - boundingRect.left;
11681 // offsetY = event.clientY - boundingRect.top;
11684 // event.deltaX = deltaX;
11685 // event.deltaY = deltaY;
11686 // event.deltaFactor = lowestDelta;
11689 originalEvent: event,
11692 deltaFactor: lowestDelta,
11693 preventDefault: function () { event.preventDefault(); },
11694 stopPropagation: function () { event.stopPropagation(); }
11697 // Clearout lowestDelta after sometime to better
11698 // handle multiple device types that give
11699 // a different lowestDelta
11700 // Ex: trackpad = 3 and mouse wheel = 120
11701 if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
11702 nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);
11704 fn.call($elm[0], newEvent);
11707 function nullLowestDelta() {
11708 lowestDelta = null;
11711 function shouldAdjustOldDeltas(orgEvent, absDelta) {
11712 // If this is an older event and the delta is divisable by 120,
11713 // then we are assuming that the browser is treating this as an
11714 // older mouse wheel event and that we should divide the deltas
11715 // by 40 to try and get a more usable deltaFactor.
11716 // Side note, this actually impacts the reported scroll distance
11717 // in older browsers and can cause scrolling to be slower than native.
11718 // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
11719 return orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
11725 // Add 'px' to the end of a number string if it doesn't have it already
11726 module.filter('px', function() {
11727 return function(str) {
11728 if (str.match(/^[\d\.]+$/)) {
11740 angular.module('ui.grid').config(['$provide', function($provide) {
11741 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11747 description: 'Přesuňte záhlaví zde pro vytvoření skupiny dle sloupce.'
11750 placeholder: 'Hledat...',
11751 showingItems: 'Zobrazuji položky:',
11752 selectedItems: 'Vybrané položky:',
11753 totalItems: 'Celkem položek:',
11754 size: 'Velikost strany:',
11755 first: 'První strana',
11756 next: 'Další strana',
11757 previous: 'Předchozí strana',
11758 last: 'Poslední strana'
11761 text: 'Vyberte sloupec:'
11764 ascending: 'Seřadit od A-Z',
11765 descending: 'Seřadit od Z-A',
11766 remove: 'Odebrat seřazení'
11769 hide: 'Schovat sloupec'
11772 count: 'celkem řádků: ',
11779 pinLeft: 'Zamknout vlevo',
11780 pinRight: 'Zamknout vpravo',
11784 columns: 'Sloupce:',
11785 importerTitle: 'Importovat soubor',
11786 exporterAllAsCsv: 'Exportovat všechna data do csv',
11787 exporterVisibleAsCsv: 'Exportovat viditelná data do csv',
11788 exporterSelectedAsCsv: 'Exportovat vybraná data do csv',
11789 exporterAllAsPdf: 'Exportovat všechna data do pdf',
11790 exporterVisibleAsPdf: 'Exportovat viditelná data do pdf',
11791 exporterSelectedAsPdf: 'Exportovat vybraná data do pdf',
11792 clearAllFilters: 'Odstranit všechny filtry'
11795 noHeaders: 'Názvy sloupců se nepodařilo získat, obsahuje soubor záhlaví?',
11796 noObjects: 'Data se nepodařilo zpracovat, obsahuje soubor řádky mimo záhlaví?',
11797 invalidCsv: 'Soubor nelze zpracovat, jedná se o CSV?',
11798 invalidJson: 'Soubor nelze zpracovat, je to JSON?',
11799 jsonNotArray: 'Soubor musí obsahovat json. Ukončuji..'
11802 sizes: 'položek na stránku',
11803 totalItems: 'položek'
11807 ungroup: 'Odebrat seskupení',
11808 aggregate_count: 'Agregace: Count',
11809 aggregate_sum: 'Agregace: Sum',
11810 aggregate_max: 'Agregace: Max',
11811 aggregate_min: 'Agregace: Min',
11812 aggregate_avg: 'Agregace: Avg',
11813 aggregate_remove: 'Agregace: Odebrat'
11817 // support varianty of different czech keys.
11818 $delegate.add('cs', lang);
11819 $delegate.add('cz', lang);
11820 $delegate.add('cs-cz', lang);
11821 $delegate.add('cs-CZ', lang);
11828 angular.module('ui.grid').config(['$provide', function($provide) {
11829 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11830 $delegate.add('da', {
11835 description: 'Grupér rækker udfra en kolonne ved at trække dens overskift hertil.'
11838 placeholder: 'Søg...',
11839 showingItems: 'Viste rækker:',
11840 selectedItems: 'Valgte rækker:',
11841 totalItems: 'Rækker totalt:',
11842 size: 'Side størrelse:',
11843 first: 'Første side',
11844 next: 'Næste side',
11845 previous: 'Forrige side',
11846 last: 'Sidste side'
11849 text: 'Vælg kolonner:'
11852 ascending: 'Sorter stigende',
11853 descending: 'Sorter faldende',
11854 none: 'Sorter ingen',
11855 remove: 'Fjern sortering'
11858 hide: 'Skjul kolonne'
11861 count: 'antal rækker: ',
11868 columns: 'Columns:',
11869 importerTitle: 'Import file',
11870 exporterAllAsCsv: 'Export all data as csv',
11871 exporterVisibleAsCsv: 'Export visible data as csv',
11872 exporterSelectedAsCsv: 'Export selected data as csv',
11873 exporterAllAsPdf: 'Export all data as pdf',
11874 exporterVisibleAsPdf: 'Export visible data as pdf',
11875 exporterSelectedAsPdf: 'Export selected data as pdf',
11876 clearAllFilters: 'Clear all filters'
11879 noHeaders: 'Column names were unable to be derived, does the file have a header?',
11880 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
11881 invalidCsv: 'File was unable to be processed, is it valid CSV?',
11882 invalidJson: 'File was unable to be processed, is it valid Json?',
11883 jsonNotArray: 'Imported json file must contain an array, aborting.'
11892 angular.module('ui.grid').config(['$provide', function ($provide) {
11893 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
11894 $delegate.add('de', {
11897 defaultFilterLabel: 'Filter für Spalte',
11898 removeFilter: 'Filter löschen',
11899 columnMenuButtonLabel: 'Spaltenmenü'
11901 priority: 'Priorität:',
11902 filterLabel: "Filter für Spalte: "
11908 description: 'Ziehen Sie eine Spaltenüberschrift hierhin, um nach dieser Spalte zu gruppieren.'
11911 placeholder: 'Suche...',
11912 showingItems: 'Zeige Einträge:',
11913 selectedItems: 'Ausgewählte Einträge:',
11914 totalItems: 'Einträge gesamt:',
11915 size: 'Einträge pro Seite:',
11916 first: 'Erste Seite',
11917 next: 'Nächste Seite',
11918 previous: 'Vorherige Seite',
11919 last: 'Letzte Seite'
11922 text: 'Spalten auswählen:'
11925 ascending: 'aufsteigend sortieren',
11926 descending: 'absteigend sortieren',
11927 none: 'keine Sortierung',
11928 remove: 'Sortierung zurücksetzen'
11931 hide: 'Spalte ausblenden'
11934 count: 'Zeilen insgesamt: ',
11936 avg: 'Durchschnitt: ',
11941 pinLeft: 'Links anheften',
11942 pinRight: 'Rechts anheften',
11950 buttonLabel: 'Tabellenmenü'
11952 columns: 'Spalten:',
11953 importerTitle: 'Datei importieren',
11954 exporterAllAsCsv: 'Alle Daten als CSV exportieren',
11955 exporterVisibleAsCsv: 'sichtbare Daten als CSV exportieren',
11956 exporterSelectedAsCsv: 'markierte Daten als CSV exportieren',
11957 exporterAllAsPdf: 'Alle Daten als PDF exportieren',
11958 exporterVisibleAsPdf: 'sichtbare Daten als PDF exportieren',
11959 exporterSelectedAsPdf: 'markierte Daten als CSV exportieren',
11960 clearAllFilters: 'Alle Filter zurücksetzen'
11963 noHeaders: 'Es konnten keine Spaltennamen ermittelt werden. Sind in der Datei Spaltendefinitionen enthalten?',
11964 noObjects: 'Es konnten keine Zeileninformationen gelesen werden, Sind in der Datei außer den Spaltendefinitionen auch Daten enthalten?',
11965 invalidCsv: 'Die Datei konnte nicht eingelesen werden, ist es eine gültige CSV-Datei?',
11966 invalidJson: 'Die Datei konnte nicht eingelesen werden. Enthält sie gültiges JSON?',
11967 jsonNotArray: 'Die importierte JSON-Datei muß ein Array enthalten. Breche Import ab.'
11971 pageToFirst: 'Zum Anfang',
11972 pageBack: 'Seite zurück',
11973 pageSelected: 'Ausgwählte Seite',
11974 pageForward: 'Seite vor',
11975 pageToLast: 'Zum Ende'
11977 sizes: 'Einträge pro Seite',
11978 totalItems: 'Einträge',
11983 group: 'Gruppieren',
11984 ungroup: 'Gruppierung aufheben',
11985 aggregate_count: 'Agg: Anzahl',
11986 aggregate_sum: 'Agg: Summe',
11987 aggregate_max: 'Agg: Maximum',
11988 aggregate_min: 'Agg: Minimum',
11989 aggregate_avg: 'Agg: Mittelwert',
11990 aggregate_remove: 'Aggregation entfernen'
11999 angular.module('ui.grid').config(['$provide', function($provide) {
12000 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12001 $delegate.add('en', {
12004 defaultFilterLabel: 'Filter for column',
12005 removeFilter: 'Remove Filter',
12006 columnMenuButtonLabel: 'Column Menu'
12008 priority: 'Priority:',
12009 filterLabel: "Filter for column: "
12015 description: 'Drag a column header here and drop it to group by that column.'
12018 placeholder: 'Search...',
12019 showingItems: 'Showing Items:',
12020 selectedItems: 'Selected Items:',
12021 totalItems: 'Total Items:',
12022 size: 'Page Size:',
12023 first: 'First Page',
12025 previous: 'Previous Page',
12029 text: 'Choose Columns:'
12032 ascending: 'Sort Ascending',
12033 descending: 'Sort Descending',
12035 remove: 'Remove Sort'
12038 hide: 'Hide Column'
12041 count: 'total rows: ',
12048 pinLeft: 'Pin Left',
12049 pinRight: 'Pin Right',
12057 buttonLabel: 'Grid Menu'
12059 columns: 'Columns:',
12060 importerTitle: 'Import file',
12061 exporterAllAsCsv: 'Export all data as csv',
12062 exporterVisibleAsCsv: 'Export visible data as csv',
12063 exporterSelectedAsCsv: 'Export selected data as csv',
12064 exporterAllAsPdf: 'Export all data as pdf',
12065 exporterVisibleAsPdf: 'Export visible data as pdf',
12066 exporterSelectedAsPdf: 'Export selected data as pdf',
12067 clearAllFilters: 'Clear all filters'
12070 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12071 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12072 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12073 invalidJson: 'File was unable to be processed, is it valid Json?',
12074 jsonNotArray: 'Imported json file must contain an array, aborting.'
12078 pageToFirst: 'Page to first',
12079 pageBack: 'Page back',
12080 pageSelected: 'Selected page',
12081 pageForward: 'Page forward',
12082 pageToLast: 'Page to last'
12084 sizes: 'items per page',
12085 totalItems: 'items',
12086 through: 'through',
12091 ungroup: 'Ungroup',
12092 aggregate_count: 'Agg: Count',
12093 aggregate_sum: 'Agg: Sum',
12094 aggregate_max: 'Agg: Max',
12095 aggregate_min: 'Agg: Min',
12096 aggregate_avg: 'Agg: Avg',
12097 aggregate_remove: 'Agg: Remove'
12101 minLength: 'Value should be at least THRESHOLD characters long.',
12102 maxLength: 'Value should be at most THRESHOLD characters long.',
12103 required: 'A value is needed.'
12112 angular.module('ui.grid').config(['$provide', function($provide) {
12113 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12114 $delegate.add('es', {
12119 description: 'Arrastre un encabezado de columna aquí y suéltelo para agrupar por esa columna.'
12122 placeholder: 'Buscar...',
12123 showingItems: 'Artículos Mostrados:',
12124 selectedItems: 'Artículos Seleccionados:',
12125 totalItems: 'Artículos Totales:',
12126 size: 'Tamaño de Página:',
12127 first: 'Primera Página',
12128 next: 'Página Siguiente',
12129 previous: 'Página Anterior',
12130 last: 'Última Página'
12133 text: 'Elegir columnas:'
12136 ascending: 'Orden Ascendente',
12137 descending: 'Orden Descendente',
12138 remove: 'Sin Ordenar'
12141 hide: 'Ocultar la columna'
12144 count: 'filas totales: ',
12151 pinLeft: 'Fijar a la Izquierda',
12152 pinRight: 'Fijar a la Derecha',
12153 unpin: 'Quitar Fijación'
12156 columns: 'Columnas:',
12157 importerTitle: 'Importar archivo',
12158 exporterAllAsCsv: 'Exportar todo como csv',
12159 exporterVisibleAsCsv: 'Exportar vista como csv',
12160 exporterSelectedAsCsv: 'Exportar selección como csv',
12161 exporterAllAsPdf: 'Exportar todo como pdf',
12162 exporterVisibleAsPdf: 'Exportar vista como pdf',
12163 exporterSelectedAsPdf: 'Exportar selección como pdf',
12164 clearAllFilters: 'Limpiar todos los filtros'
12167 noHeaders: 'No fue posible derivar los nombres de las columnas, ¿tiene encabezados el archivo?',
12168 noObjects: 'No fue posible obtener registros, ¿contiene datos el archivo, aparte de los encabezados?',
12169 invalidCsv: 'No fue posible procesar el archivo, ¿es un CSV válido?',
12170 invalidJson: 'No fue posible procesar el archivo, ¿es un Json válido?',
12171 jsonNotArray: 'El archivo json importado debe contener un array, abortando.'
12174 sizes: 'registros por página',
12175 totalItems: 'registros',
12180 ungroup: 'Desagrupar',
12181 aggregate_count: 'Agr: Cont',
12182 aggregate_sum: 'Agr: Sum',
12183 aggregate_max: 'Agr: Máx',
12184 aggregate_min: 'Agr: Min',
12185 aggregate_avg: 'Agr: Prom',
12186 aggregate_remove: 'Agr: Quitar'
12195 * Translated by: R. Salarmehr
12197 * Using Vajje.com online dictionary.
12200 angular.module('ui.grid').config(['$provide', function ($provide) {
12201 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12202 $delegate.add('fa', {
12207 description: 'عنوان یک ستون را بگیر و به گروهی از آن ستون رها کن.'
12210 placeholder: 'جستجو...',
12211 showingItems: 'نمایش اقلام:',
12212 selectedItems: 'قلم\u200cهای انتخاب شده:',
12213 totalItems: 'مجموع اقلام:',
12214 size: 'اندازه\u200cی صفحه:',
12215 first: 'اولین صفحه',
12216 next: 'صفحه\u200cی\u200cبعدی',
12217 previous: 'صفحه\u200cی\u200c قبلی',
12221 text: 'ستون\u200cهای انتخابی:'
12224 ascending: 'ترتیب صعودی',
12225 descending: 'ترتیب نزولی',
12226 remove: 'حذف مرتب کردن'
12229 hide: 'پنهان\u200cکردن ستون'
12239 pinLeft: 'پین کردن سمت چپ',
12240 pinRight: 'پین کردن سمت راست',
12244 columns: 'ستون\u200cها:',
12245 importerTitle: 'وارد کردن فایل',
12246 exporterAllAsCsv: 'خروجی تمام داده\u200cها در فایل csv',
12247 exporterVisibleAsCsv: 'خروجی داده\u200cهای قابل مشاهده در فایل csv',
12248 exporterSelectedAsCsv: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل csv',
12249 exporterAllAsPdf: 'خروجی تمام داده\u200cها در فایل pdf',
12250 exporterVisibleAsPdf: 'خروجی داده\u200cهای قابل مشاهده در فایل pdf',
12251 exporterSelectedAsPdf: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل pdf',
12252 clearAllFilters: 'پاک کردن تمام فیلتر'
12255 noHeaders: 'نام ستون قابل استخراج نیست. آیا فایل عنوان دارد؟',
12256 noObjects: 'اشیا قابل استخراج نیستند. آیا به جز عنوان\u200cها در فایل داده وجود دارد؟',
12257 invalidCsv: 'فایل قابل پردازش نیست. آیا فرمت csv معتبر است؟',
12258 invalidJson: 'فایل قابل پردازش نیست. آیا فرمت json معتبر است؟',
12259 jsonNotArray: 'فایل json وارد شده باید حاوی آرایه باشد. عملیات ساقط شد.'
12262 sizes: 'اقلام در هر صفحه',
12263 totalItems: 'اقلام',
12267 group: 'گروه\u200cبندی',
12268 ungroup: 'حذف گروه\u200cبندی',
12269 aggregate_count: 'Agg: تعداد',
12270 aggregate_sum: 'Agg: جمع',
12271 aggregate_max: 'Agg: بیشینه',
12272 aggregate_min: 'Agg: کمینه',
12273 aggregate_avg: 'Agg: میانگین',
12274 aggregate_remove: 'Agg: حذف'
12283 angular.module('ui.grid').config(['$provide', function($provide) {
12284 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12285 $delegate.add('fi', {
12290 description: 'Raahaa ja pudota otsikko tähän ryhmittääksesi sarakkeen mukaan.'
12293 placeholder: 'Hae...',
12294 showingItems: 'Näytetään rivejä:',
12295 selectedItems: 'Valitut rivit:',
12296 totalItems: 'Rivejä yht.:',
12298 first: 'Ensimmäinen sivu',
12299 next: 'Seuraava sivu',
12300 previous: 'Edellinen sivu',
12301 last: 'Viimeinen sivu'
12304 text: 'Valitse sarakkeet:'
12307 ascending: 'Järjestä nouseva',
12308 descending: 'Järjestä laskeva',
12309 remove: 'Poista järjestys'
12312 hide: 'Piilota sarake'
12315 count: 'Rivejä yht.: ',
12322 pinLeft: 'Lukitse vasemmalle',
12323 pinRight: 'Lukitse oikealle',
12324 unpin: 'Poista lukitus'
12327 columns: 'Sarakkeet:',
12328 importerTitle: 'Tuo tiedosto',
12329 exporterAllAsCsv: 'Vie tiedot csv-muodossa',
12330 exporterVisibleAsCsv: 'Vie näkyvä tieto csv-muodossa',
12331 exporterSelectedAsCsv: 'Vie valittu tieto csv-muodossa',
12332 exporterAllAsPdf: 'Vie tiedot pdf-muodossa',
12333 exporterVisibleAsPdf: 'Vie näkyvä tieto pdf-muodossa',
12334 exporterSelectedAsPdf: 'Vie valittu tieto pdf-muodossa',
12335 clearAllFilters: 'Puhdista kaikki suodattimet'
12338 noHeaders: 'Sarakkeen nimiä ei voitu päätellä, onko tiedostossa otsikkoriviä?',
12339 noObjects: 'Tietoja ei voitu lukea, onko tiedostossa muuta kuin otsikkot?',
12340 invalidCsv: 'Tiedostoa ei voitu käsitellä, oliko se CSV-muodossa?',
12341 invalidJson: 'Tiedostoa ei voitu käsitellä, oliko se JSON-muodossa?',
12342 jsonNotArray: 'Tiedosto ei sisältänyt taulukkoa, lopetetaan.'
12351 angular.module('ui.grid').config(['$provide', function($provide) {
12352 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12353 $delegate.add('fr', {
12358 description: 'Faites glisser une en-tête de colonne ici pour créer un groupe de colonnes.'
12361 placeholder: 'Recherche...',
12362 showingItems: 'Affichage des éléments :',
12363 selectedItems: 'Éléments sélectionnés :',
12364 totalItems: 'Nombre total d\'éléments:',
12365 size: 'Taille de page:',
12366 first: 'Première page',
12367 next: 'Page Suivante',
12368 previous: 'Page précédente',
12369 last: 'Dernière page'
12372 text: 'Choisir des colonnes :'
12375 ascending: 'Trier par ordre croissant',
12376 descending: 'Trier par ordre décroissant',
12377 remove: 'Enlever le tri'
12380 hide: 'Cacher la colonne'
12383 count: 'lignes totales: ',
12390 pinLeft: 'Épingler à gauche',
12391 pinRight: 'Épingler à droite',
12395 columns: 'Colonnes:',
12396 importerTitle: 'Importer un fichier',
12397 exporterAllAsCsv: 'Exporter toutes les données en CSV',
12398 exporterVisibleAsCsv: 'Exporter les données visibles en CSV',
12399 exporterSelectedAsCsv: 'Exporter les données sélectionnées en CSV',
12400 exporterAllAsPdf: 'Exporter toutes les données en PDF',
12401 exporterVisibleAsPdf: 'Exporter les données visibles en PDF',
12402 exporterSelectedAsPdf: 'Exporter les données sélectionnées en PDF',
12403 clearAllFilters: 'Nettoyez tous les filtres'
12406 noHeaders: 'Impossible de déterminer le nom des colonnes, le fichier possède-t-il une en-tête ?',
12407 noObjects: 'Aucun objet trouvé, le fichier possède-t-il des données autres que l\'en-tête ?',
12408 invalidCsv: 'Le fichier n\'a pas pu être traité, le CSV est-il valide ?',
12409 invalidJson: 'Le fichier n\'a pas pu être traité, le JSON est-il valide ?',
12410 jsonNotArray: 'Le fichier JSON importé doit contenir un tableau, abandon.'
12413 sizes: 'éléments par page',
12414 totalItems: 'éléments',
12419 ungroup: 'Dégrouper',
12420 aggregate_count: 'Agg: Compte',
12421 aggregate_sum: 'Agg: Somme',
12422 aggregate_max: 'Agg: Max',
12423 aggregate_min: 'Agg: Min',
12424 aggregate_avg: 'Agg: Moy',
12425 aggregate_remove: 'Agg: Retirer'
12434 angular.module('ui.grid').config(['$provide', function ($provide) {
12435 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12436 $delegate.add('he', {
12441 description: 'גרור עמודה לכאן ושחרר בכדי לקבץ עמודה זו.'
12444 placeholder: 'חפש...',
12445 showingItems: 'מציג:',
12446 selectedItems: 'סה"כ נבחרו:',
12447 totalItems: 'סה"כ רשומות:',
12448 size: 'תוצאות בדף:',
12451 previous: 'דף קודם',
12455 text: 'בחר עמודות:'
12458 ascending: 'סדר עולה',
12459 descending: 'סדר יורד',
12466 count: 'total rows: ',
12473 columns: 'Columns:',
12474 importerTitle: 'Import file',
12475 exporterAllAsCsv: 'Export all data as csv',
12476 exporterVisibleAsCsv: 'Export visible data as csv',
12477 exporterSelectedAsCsv: 'Export selected data as csv',
12478 exporterAllAsPdf: 'Export all data as pdf',
12479 exporterVisibleAsPdf: 'Export visible data as pdf',
12480 exporterSelectedAsPdf: 'Export selected data as pdf',
12481 clearAllFilters: 'Clean all filters'
12484 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12485 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12486 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12487 invalidJson: 'File was unable to be processed, is it valid Json?',
12488 jsonNotArray: 'Imported json file must contain an array, aborting.'
12497 angular.module('ui.grid').config(['$provide', function($provide) {
12498 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12499 $delegate.add('hy', {
12504 description: 'Ըստ սյան խմբավորելու համար քաշեք և գցեք վերնագիրն այստեղ։'
12507 placeholder: 'Փնտրում...',
12508 showingItems: 'Ցուցադրված տվյալներ՝',
12509 selectedItems: 'Ընտրված:',
12510 totalItems: 'Ընդամենը՝',
12511 size: 'Տողերի քանակը էջում՝',
12512 first: 'Առաջին էջ',
12514 previous: 'Նախորդ էջ',
12518 text: 'Ընտրել սյուները:'
12521 ascending: 'Աճման կարգով',
12522 descending: 'Նվազման կարգով',
12526 hide: 'Թաքցնել սյունը'
12529 count: 'ընդամենը տող՝ ',
12536 pinLeft: 'Կպցնել ձախ կողմում',
12537 pinRight: 'Կպցնել աջ կողմում',
12541 columns: 'Սյուներ:',
12542 importerTitle: 'Ներմուծել ֆայլ',
12543 exporterAllAsCsv: 'Արտահանել ամբողջը CSV',
12544 exporterVisibleAsCsv: 'Արտահանել երևացող տվյալները CSV',
12545 exporterSelectedAsCsv: 'Արտահանել ընտրված տվյալները CSV',
12546 exporterAllAsPdf: 'Արտահանել PDF',
12547 exporterVisibleAsPdf: 'Արտահանել երևացող տվյալները PDF',
12548 exporterSelectedAsPdf: 'Արտահանել ընտրված տվյալները PDF',
12549 clearAllFilters: 'Մաքրել բոլոր ֆիլտրերը'
12552 noHeaders: 'Հնարավոր չեղավ որոշել սյան վերնագրերը։ Արդյո՞ք ֆայլը ունի վերնագրեր։',
12553 noObjects: 'Հնարավոր չեղավ կարդալ տվյալները։ Արդյո՞ք ֆայլում կան տվյալներ։',
12554 invalidCsv: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր CSV է։',
12555 invalidJson: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր Json է։',
12556 jsonNotArray: 'Ներմուծված json ֆայլը պետք է պարունակի զանգված, կասեցվում է։'
12565 angular.module('ui.grid').config(['$provide', function($provide) {
12566 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12567 $delegate.add('it', {
12572 description: 'Trascina un\'intestazione all\'interno del gruppo della colonna.'
12575 placeholder: 'Ricerca...',
12576 showingItems: 'Mostra:',
12577 selectedItems: 'Selezionati:',
12578 totalItems: 'Totali:',
12579 size: 'Tot Pagine:',
12582 previous: 'Precedente',
12586 text: 'Scegli le colonne:'
12590 descending: 'Desc.',
12591 remove: 'Annulla ordinamento'
12597 count: 'righe totali: ',
12604 pinLeft: 'Blocca a sx',
12605 pinRight: 'Blocca a dx',
12606 unpin: 'Blocca in alto'
12609 columns: 'Colonne:',
12610 importerTitle: 'Importa',
12611 exporterAllAsCsv: 'Esporta tutti i dati in CSV',
12612 exporterVisibleAsCsv: 'Esporta i dati visibili in CSV',
12613 exporterSelectedAsCsv: 'Esporta i dati selezionati in CSV',
12614 exporterAllAsPdf: 'Esporta tutti i dati in PDF',
12615 exporterVisibleAsPdf: 'Esporta i dati visibili in PDF',
12616 exporterSelectedAsPdf: 'Esporta i dati selezionati in PDF',
12617 clearAllFilters: 'Pulire tutti i filtri'
12620 noHeaders: 'Impossibile reperire i nomi delle colonne, sicuro che siano indicati all\'interno del file?',
12621 noObjects: 'Impossibile reperire gli oggetti, sicuro che siano indicati all\'interno del file?',
12622 invalidCsv: 'Impossibile elaborare il file, sicuro che sia un CSV?',
12623 invalidJson: 'Impossibile elaborare il file, sicuro che sia un JSON valido?',
12624 jsonNotArray: 'Errore! Il file JSON da importare deve contenere un array.'
12627 group: 'Raggruppa',
12629 aggregate_count: 'Agg: N. Elem.',
12630 aggregate_sum: 'Agg: Somma',
12631 aggregate_max: 'Agg: Massimo',
12632 aggregate_min: 'Agg: Minimo',
12633 aggregate_avg: 'Agg: Media',
12634 aggregate_remove: 'Agg: Rimuovi'
12638 minLength: 'Lunghezza minima pari a THRESHOLD caratteri.',
12639 maxLength: 'Lunghezza massima pari a THRESHOLD caratteri.',
12640 required: 'Necessario inserire un valore.'
12649 angular.module('ui.grid').config(['$provide', function($provide) {
12650 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12651 $delegate.add('ja', {
12656 description: 'ここに列ヘッダをドラッグアンドドロップして、その列でグループ化します。'
12659 placeholder: '検索...',
12660 showingItems: '表示中の項目:',
12661 selectedItems: '選択した項目:',
12662 totalItems: '項目の総数:',
12673 ascending: '昇順に並べ替え',
12674 descending: '降順に並べ替え',
12694 importerTitle: 'ファイルのインポート',
12695 exporterAllAsCsv: 'すべてのデータをCSV形式でエクスポート',
12696 exporterVisibleAsCsv: '表示中のデータをCSV形式でエクスポート',
12697 exporterSelectedAsCsv: '選択したデータをCSV形式でエクスポート',
12698 exporterAllAsPdf: 'すべてのデータをPDF形式でエクスポート',
12699 exporterVisibleAsPdf: '表示中のデータをPDF形式でエクスポート',
12700 exporterSelectedAsPdf: '選択したデータをPDF形式でエクスポート',
12701 clearAllFilters: 'すべてのフィルタを清掃してください'
12704 noHeaders: '列名を取得できません。ファイルにヘッダが含まれていることを確認してください。',
12705 noObjects: 'オブジェクトを取得できません。ファイルにヘッダ以外のデータが含まれていることを確認してください。',
12706 invalidCsv: 'ファイルを処理できません。ファイルが有効なCSV形式であることを確認してください。',
12707 invalidJson: 'ファイルを処理できません。ファイルが有効なJSON形式であることを確認してください。',
12708 jsonNotArray: 'インポートしたJSONファイルには配列が含まれている必要があります。処理を中止します。'
12721 angular.module('ui.grid').config(['$provide', function($provide) {
12722 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12723 $delegate.add('ko', {
12728 description: '컬럼으로 그룹핑하기 위해서는 컬럼 헤더를 끌어 떨어뜨려 주세요.'
12731 placeholder: '검색...',
12732 showingItems: '항목 보여주기:',
12733 selectedItems: '선택 항목:',
12734 totalItems: '전체 항목:',
12738 previous: '이전 페이지',
12745 ascending: '오름차순 정렬',
12746 descending: '내림차순 정렬',
12766 importerTitle: '파일 가져오기',
12767 exporterAllAsCsv: 'csv로 모든 데이터 내보내기',
12768 exporterVisibleAsCsv: 'csv로 보이는 데이터 내보내기',
12769 exporterSelectedAsCsv: 'csv로 선택된 데이터 내보내기',
12770 exporterAllAsPdf: 'pdf로 모든 데이터 내보내기',
12771 exporterVisibleAsPdf: 'pdf로 보이는 데이터 내보내기',
12772 exporterSelectedAsPdf: 'pdf로 선택 데이터 내보내기',
12773 clearAllFilters: '모든 필터를 청소'
12776 noHeaders: '컬럼명이 지정되어 있지 않습니다. 파일에 헤더가 명시되어 있는지 확인해 주세요.',
12777 noObjects: '데이터가 지정되어 있지 않습니다. 데이터가 파일에 있는지 확인해 주세요.',
12778 invalidCsv: '파일을 처리할 수 없습니다. 올바른 csv인지 확인해 주세요.',
12779 invalidJson: '파일을 처리할 수 없습니다. 올바른 json인지 확인해 주세요.',
12780 jsonNotArray: 'json 파일은 배열을 포함해야 합니다.'
12784 totalItems: '전체 항목'
12793 angular.module('ui.grid').config(['$provide', function($provide) {
12794 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12795 $delegate.add('nl', {
12800 description: 'Sleep hier een kolomnaam heen om op te groeperen.'
12803 placeholder: 'Zoeken...',
12804 showingItems: 'Getoonde items:',
12805 selectedItems: 'Geselecteerde items:',
12806 totalItems: 'Totaal aantal items:',
12807 size: 'Items per pagina:',
12808 first: 'Eerste pagina',
12809 next: 'Volgende pagina',
12810 previous: 'Vorige pagina',
12811 last: 'Laatste pagina'
12814 text: 'Kies kolommen:'
12817 ascending: 'Sorteer oplopend',
12818 descending: 'Sorteer aflopend',
12819 remove: 'Verwijder sortering'
12822 hide: 'Verberg kolom'
12825 count: 'Aantal rijen: ',
12827 avg: 'Gemiddelde: ',
12832 pinLeft: 'Zet links vast',
12833 pinRight: 'Zet rechts vast',
12837 columns: 'Kolommen:',
12838 importerTitle: 'Importeer bestand',
12839 exporterAllAsCsv: 'Exporteer alle data als csv',
12840 exporterVisibleAsCsv: 'Exporteer zichtbare data als csv',
12841 exporterSelectedAsCsv: 'Exporteer geselecteerde data als csv',
12842 exporterAllAsPdf: 'Exporteer alle data als pdf',
12843 exporterVisibleAsPdf: 'Exporteer zichtbare data als pdf',
12844 exporterSelectedAsPdf: 'Exporteer geselecteerde data als pdf',
12845 clearAllFilters: 'Reinig alle filters'
12848 noHeaders: 'Kolomnamen kunnen niet worden afgeleid. Heeft het bestand een header?',
12849 noObjects: 'Objecten kunnen niet worden afgeleid. Bevat het bestand data naast de headers?',
12850 invalidCsv: 'Het bestand kan niet verwerkt worden. Is het een valide csv bestand?',
12851 invalidJson: 'Het bestand kan niet verwerkt worden. Is het valide json?',
12852 jsonNotArray: 'Het json bestand moet een array bevatten. De actie wordt geannuleerd.'
12855 sizes: 'items per pagina',
12856 totalItems: 'items',
12861 ungroup: 'Groepering opheffen',
12862 aggregate_count: 'Agg: Aantal',
12863 aggregate_sum: 'Agg: Som',
12864 aggregate_max: 'Agg: Max',
12865 aggregate_min: 'Agg: Min',
12866 aggregate_avg: 'Agg: Gem',
12867 aggregate_remove: 'Agg: Verwijder'
12876 angular.module('ui.grid').config(['$provide', function($provide) {
12877 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12878 $delegate.add('pl', {
12881 defaultFilterLabel: 'Filter dla kolumny',
12882 removeFilter: 'Usuń filter',
12883 columnMenuButtonLabel: 'Menu kolumny'
12885 priority: 'Prioritet:',
12886 filterLabel: "Filtr dla kolumny: "
12892 description: 'Przeciągnij nagłówek kolumny tutaj, aby pogrupować według niej.'
12895 placeholder: 'Szukaj...',
12896 showingItems: 'Widoczne pozycje:',
12897 selectedItems: 'Zaznaczone pozycje:',
12898 totalItems: 'Wszystkich pozycji:',
12899 size: 'Rozmiar strony:',
12900 first: 'Pierwsza strona',
12901 next: 'Następna strona',
12902 previous: 'Poprzednia strona',
12903 last: 'Ostatnia strona'
12906 text: 'Wybierz kolumny:'
12909 ascending: 'Sortuj rosnąco',
12910 descending: 'Sortuj malejąco',
12911 none: 'Brak sortowania',
12912 remove: 'Wyłącz sortowanie'
12915 hide: 'Ukryj kolumne'
12918 count: 'Razem pozycji: ',
12925 pinLeft: 'Przypnij do lewej',
12926 pinRight: 'Przypnij do prawej',
12934 buttonLabel: 'Menu Grida'
12936 columns: 'Kolumny:',
12937 importerTitle: 'Importuj plik',
12938 exporterAllAsCsv: 'Eksportuj wszystkie dane do csv',
12939 exporterVisibleAsCsv: 'Eksportuj widoczne dane do csv',
12940 exporterSelectedAsCsv: 'Eksportuj zaznaczone dane do csv',
12941 exporterAllAsPdf: 'Eksportuj wszystkie dane do pdf',
12942 exporterVisibleAsPdf: 'Eksportuj widoczne dane do pdf',
12943 exporterSelectedAsPdf: 'Eksportuj zaznaczone dane do pdf',
12944 clearAllFilters: 'Wyczyść filtry'
12947 noHeaders: 'Nie udało się wczytać nazw kolumn. Czy plik posiada nagłówek?',
12948 noObjects: 'Nie udalo się wczytać pozycji. Czy plik zawiera dane??',
12949 invalidCsv: 'Nie udało się przetworzyć pliku, jest to prawidlowy plik CSV??',
12950 invalidJson: 'Nie udało się przetworzyć pliku, jest to prawidlowy plik Json?',
12951 jsonNotArray: 'Importowany plik json musi zawierać tablicę, importowanie przerwane.'
12955 pageToFirst: 'Pierwsza strona',
12956 pageBack: 'Poprzednia strona',
12957 pageSelected: 'Wybrana strona',
12958 pageForward: 'Następna strona',
12959 pageToLast: 'Ostatnia strona'
12961 sizes: 'pozycji na stronę',
12962 totalItems: 'pozycji',
12968 ungroup: 'Rozgrupuj',
12969 aggregate_count: 'Zbiorczo: Razem',
12970 aggregate_sum: 'Zbiorczo: Suma',
12971 aggregate_max: 'Zbiorczo: Max',
12972 aggregate_min: 'Zbiorczo: Min',
12973 aggregate_avg: 'Zbiorczo: Średnia',
12974 aggregate_remove: 'Zbiorczo: Usuń'
12983 angular.module('ui.grid').config(['$provide', function($provide) {
12984 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12985 $delegate.add('pt-br', {
12988 defaultFilterLabel: 'Filtro por coluna',
12989 removeFilter: 'Remover filtro',
12990 columnMenuButtonLabel: 'Menu coluna'
12992 priority: 'Prioridade:',
12993 filterLabel: "Filtro por coluna: "
12999 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
13002 placeholder: 'Procurar...',
13003 showingItems: 'Mostrando os Itens:',
13004 selectedItems: 'Items Selecionados:',
13005 totalItems: 'Total de Itens:',
13006 size: 'Tamanho da Página:',
13007 first: 'Primeira Página',
13008 next: 'Próxima Página',
13009 previous: 'Página Anterior',
13010 last: 'Última Página'
13013 text: 'Selecione as colunas:'
13016 ascending: 'Ordenar Ascendente',
13017 descending: 'Ordenar Descendente',
13018 none: 'Nenhuma Ordem',
13019 remove: 'Remover Ordenação'
13022 hide: 'Esconder coluna'
13025 count: 'total de linhas: ',
13032 pinLeft: 'Fixar Esquerda',
13033 pinRight: 'Fixar Direita',
13034 unpin: 'Desprender'
13041 buttonLabel: 'Menu Grid'
13043 columns: 'Colunas:',
13044 importerTitle: 'Importar arquivo',
13045 exporterAllAsCsv: 'Exportar todos os dados como csv',
13046 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
13047 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
13048 exporterAllAsPdf: 'Exportar todos os dados como pdf',
13049 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
13050 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
13051 clearAllFilters: 'Limpar todos os filtros'
13054 noHeaders: 'Nomes de colunas não puderam ser derivados. O arquivo tem um cabeçalho?',
13055 noObjects: 'Objetos não puderam ser derivados. Havia dados no arquivo, além dos cabeçalhos?',
13056 invalidCsv: 'Arquivo não pode ser processado. É um CSV válido?',
13057 invalidJson: 'Arquivo não pode ser processado. É um Json válido?',
13058 jsonNotArray: 'Arquivo json importado tem que conter um array. Abortando.'
13062 pageToFirst: 'Primeira página',
13063 pageBack: 'Página anterior',
13064 pageSelected: 'Página Selecionada',
13065 pageForward: 'Proxima',
13066 pageToLast: 'Anterior'
13068 sizes: 'itens por página',
13069 totalItems: 'itens',
13070 through: 'através dos',
13075 ungroup: 'Desagrupar',
13076 aggregate_count: 'Agr: Contar',
13077 aggregate_sum: 'Agr: Soma',
13078 aggregate_max: 'Agr: Max',
13079 aggregate_min: 'Agr: Min',
13080 aggregate_avg: 'Agr: Med',
13081 aggregate_remove: 'Agr: Remover'
13090 angular.module('ui.grid').config(['$provide', function($provide) {
13091 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13092 $delegate.add('pt', {
13095 defaultFilterLabel: 'Filtro por coluna',
13096 removeFilter: 'Remover filtro',
13097 columnMenuButtonLabel: 'Menu coluna'
13099 priority: 'Prioridade:',
13100 filterLabel: "Filtro por coluna: "
13106 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
13109 placeholder: 'Procurar...',
13110 showingItems: 'Mostrando os Itens:',
13111 selectedItems: 'Itens Selecionados:',
13112 totalItems: 'Total de Itens:',
13113 size: 'Tamanho da Página:',
13114 first: 'Primeira Página',
13115 next: 'Próxima Página',
13116 previous: 'Página Anterior',
13117 last: 'Última Página'
13120 text: 'Selecione as colunas:'
13123 ascending: 'Ordenar Ascendente',
13124 descending: 'Ordenar Descendente',
13125 none: 'Nenhuma Ordem',
13126 remove: 'Remover Ordenação'
13129 hide: 'Esconder coluna'
13132 count: 'total de linhas: ',
13139 pinLeft: 'Fixar Esquerda',
13140 pinRight: 'Fixar Direita',
13141 unpin: 'Desprender'
13148 buttonLabel: 'Menu Grid'
13150 columns: 'Colunas:',
13151 importerTitle: 'Importar ficheiro',
13152 exporterAllAsCsv: 'Exportar todos os dados como csv',
13153 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
13154 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
13155 exporterAllAsPdf: 'Exportar todos os dados como pdf',
13156 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
13157 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
13158 clearAllFilters: 'Limpar todos os filtros'
13161 noHeaders: 'Nomes de colunas não puderam ser derivados. O ficheiro tem um cabeçalho?',
13162 noObjects: 'Objetos não puderam ser derivados. Havia dados no ficheiro, além dos cabeçalhos?',
13163 invalidCsv: 'Ficheiro não pode ser processado. É um CSV válido?',
13164 invalidJson: 'Ficheiro não pode ser processado. É um Json válido?',
13165 jsonNotArray: 'Ficheiro json importado tem que conter um array. Interrompendo.'
13169 pageToFirst: 'Primeira página',
13170 pageBack: 'Página anterior',
13171 pageSelected: 'Página Selecionada',
13172 pageForward: 'Próxima',
13173 pageToLast: 'Anterior'
13175 sizes: 'itens por página',
13176 totalItems: 'itens',
13177 through: 'através dos',
13182 ungroup: 'Desagrupar',
13183 aggregate_count: 'Agr: Contar',
13184 aggregate_sum: 'Agr: Soma',
13185 aggregate_max: 'Agr: Max',
13186 aggregate_min: 'Agr: Min',
13187 aggregate_avg: 'Agr: Med',
13188 aggregate_remove: 'Agr: Remover'
13197 angular.module('ui.grid').config(['$provide', function($provide) {
13198 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13199 $delegate.add('ro', {
13202 defaultFilterLabel: 'Filtru pentru coloana',
13203 removeFilter: 'Sterge filtru',
13204 columnMenuButtonLabel: 'Column Menu'
13206 priority: 'Prioritate:',
13207 filterLabel: "Filtru pentru coloana:"
13213 description: 'Trage un cap de coloana aici pentru a grupa elementele dupa coloana respectiva'
13216 placeholder: 'Cauta...',
13217 showingItems: 'Arata elementele:',
13218 selectedItems: 'Elementele selectate:',
13219 totalItems: 'Total elemente:',
13220 size: 'Marime pagina:',
13221 first: 'Prima pagina',
13222 next: 'Pagina urmatoare',
13223 previous: 'Pagina anterioara',
13224 last: 'Ultima pagina'
13227 text: 'Alege coloane:'
13230 ascending: 'Ordoneaza crescator',
13231 descending: 'Ordoneaza descrescator',
13232 none: 'Fara ordonare',
13233 remove: 'Sterge ordonarea'
13236 hide: 'Ascunde coloana'
13239 count: 'total linii: ',
13246 pinLeft: 'Pin la stanga',
13247 pinRight: 'Pin la dreapta',
13248 unpin: 'Sterge pinul'
13255 buttonLabel: 'Grid Menu'
13257 columns: 'Coloane:',
13258 importerTitle: 'Incarca fisier',
13259 exporterAllAsCsv: 'Exporta toate datele ca csv',
13260 exporterVisibleAsCsv: 'Exporta datele vizibile ca csv',
13261 exporterSelectedAsCsv: 'Exporta datele selectate ca csv',
13262 exporterAllAsPdf: 'Exporta toate datele ca pdf',
13263 exporterVisibleAsPdf: 'Exporta datele vizibile ca pdf',
13264 exporterSelectedAsPdf: 'Exporta datele selectate ca csv pdf',
13265 clearAllFilters: 'Sterge toate filtrele'
13268 noHeaders: 'Numele coloanelor nu a putut fi incarcat, acest fisier are un header?',
13269 noObjects: 'Datele nu au putut fi incarcate, exista date in fisier in afara numelor de coloane?',
13270 invalidCsv: 'Fisierul nu a putut fi procesat, ati incarcat un CSV valid ?',
13271 invalidJson: 'Fisierul nu a putut fi procesat, ati incarcat un Json valid?',
13272 jsonNotArray: 'Json-ul incarcat trebuie sa contina un array, inchidere.'
13276 pageToFirst: 'Prima pagina',
13277 pageBack: 'O pagina inapoi',
13278 pageSelected: 'Pagina selectata',
13279 pageForward: 'O pagina inainte',
13280 pageToLast: 'Ultima pagina'
13282 sizes: 'Elemente per pagina',
13283 totalItems: 'elemente',
13289 ungroup: 'Opreste gruparea',
13290 aggregate_count: 'Agg: Count',
13291 aggregate_sum: 'Agg: Sum',
13292 aggregate_max: 'Agg: Max',
13293 aggregate_min: 'Agg: Min',
13294 aggregate_avg: 'Agg: Avg',
13295 aggregate_remove: 'Agg: Remove'
13304 angular.module('ui.grid').config(['$provide', function($provide) {
13305 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13306 $delegate.add('ru', {
13309 defaultFilterLabel: 'Фильтр столбца',
13310 removeFilter: 'Удалить фильтр',
13311 columnMenuButtonLabel: 'Меню столбца'
13313 priority: 'Приоритет:',
13314 filterLabel: "Фильтр столбца: "
13320 description: 'Для группировки по столбцу перетащите сюда его название.'
13323 placeholder: 'Поиск...',
13324 showingItems: 'Показать элементы:',
13325 selectedItems: 'Выбранные элементы:',
13326 totalItems: 'Всего элементов:',
13327 size: 'Размер страницы:',
13328 first: 'Первая страница',
13329 next: 'Следующая страница',
13330 previous: 'Предыдущая страница',
13331 last: 'Последняя страница'
13334 text: 'Выбрать столбцы:'
13337 ascending: 'По возрастанию',
13338 descending: 'По убыванию',
13339 none: 'Без сортировки',
13340 remove: 'Убрать сортировку'
13343 hide: 'Спрятать столбец'
13346 count: 'всего строк: ',
13353 pinLeft: 'Закрепить слева',
13354 pinRight: 'Закрепить справа',
13362 buttonLabel: 'Меню'
13364 columns: 'Столбцы:',
13365 importerTitle: 'Импортировать файл',
13366 exporterAllAsCsv: 'Экспортировать всё в CSV',
13367 exporterVisibleAsCsv: 'Экспортировать видимые данные в CSV',
13368 exporterSelectedAsCsv: 'Экспортировать выбранные данные в CSV',
13369 exporterAllAsPdf: 'Экспортировать всё в PDF',
13370 exporterVisibleAsPdf: 'Экспортировать видимые данные в PDF',
13371 exporterSelectedAsPdf: 'Экспортировать выбранные данные в PDF',
13372 clearAllFilters: 'Очистите все фильтры'
13375 noHeaders: 'Не удалось получить названия столбцов, есть ли в файле заголовок?',
13376 noObjects: 'Не удалось получить данные, есть ли в файле строки кроме заголовка?',
13377 invalidCsv: 'Не удалось обработать файл, это правильный CSV-файл?',
13378 invalidJson: 'Не удалось обработать файл, это правильный JSON?',
13379 jsonNotArray: 'Импортируемый JSON-файл должен содержать массив, операция отменена.'
13383 pageToFirst: 'Первая страница',
13384 pageBack: 'Предыдущая страница',
13385 pageSelected: 'Выбранная страница',
13386 pageForward: 'Следующая страница',
13387 pageToLast: 'Последняя страница'
13389 sizes: 'строк на страницу',
13390 totalItems: 'строк',
13395 group: 'Группировать',
13396 ungroup: 'Разгруппировать',
13397 aggregate_count: 'Группировать: Count',
13398 aggregate_sum: 'Для группы: Сумма',
13399 aggregate_max: 'Для группы: Максимум',
13400 aggregate_min: 'Для группы: Минимум',
13401 aggregate_avg: 'Для группы: Среднее',
13402 aggregate_remove: 'Для группы: Пусто'
13411 angular.module('ui.grid').config(['$provide', function($provide) {
13412 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13413 $delegate.add('sk', {
13418 description: 'Pretiahni sem názov stĺpca pre zoskupenie podľa toho stĺpca.'
13421 placeholder: 'Hľadaj...',
13422 showingItems: 'Zobrazujem položky:',
13423 selectedItems: 'Vybraté položky:',
13424 totalItems: 'Počet položiek:',
13426 first: 'Prvá strana',
13427 next: 'Ďalšia strana',
13428 previous: 'Predchádzajúca strana',
13429 last: 'Posledná strana'
13432 text: 'Vyberte stĺpce:'
13435 ascending: 'Zotriediť vzostupne',
13436 descending: 'Zotriediť zostupne',
13437 remove: 'Vymazať triedenie'
13440 count: 'total rows: ',
13447 columns: 'Columns:',
13448 importerTitle: 'Import file',
13449 exporterAllAsCsv: 'Export all data as csv',
13450 exporterVisibleAsCsv: 'Export visible data as csv',
13451 exporterSelectedAsCsv: 'Export selected data as csv',
13452 exporterAllAsPdf: 'Export all data as pdf',
13453 exporterVisibleAsPdf: 'Export visible data as pdf',
13454 exporterSelectedAsPdf: 'Export selected data as pdf',
13455 clearAllFilters: 'Clear all filters'
13458 noHeaders: 'Column names were unable to be derived, does the file have a header?',
13459 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
13460 invalidCsv: 'File was unable to be processed, is it valid CSV?',
13461 invalidJson: 'File was unable to be processed, is it valid Json?',
13462 jsonNotArray: 'Imported json file must contain an array, aborting.'
13471 angular.module('ui.grid').config(['$provide', function($provide) {
13472 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13473 $delegate.add('sv', {
13478 description: 'Dra en kolumnrubrik hit och släpp den för att gruppera efter den kolumnen.'
13481 placeholder: 'Sök...',
13482 showingItems: 'Visar artiklar:',
13483 selectedItems: 'Valda artiklar:',
13484 totalItems: 'Antal artiklar:',
13485 size: 'Sidstorlek:',
13486 first: 'Första sidan',
13487 next: 'Nästa sida',
13488 previous: 'Föregående sida',
13489 last: 'Sista sidan'
13492 text: 'Välj kolumner:'
13495 ascending: 'Sortera stigande',
13496 descending: 'Sortera fallande',
13497 remove: 'Inaktivera sortering'
13503 count: 'Antal rader: ',
13505 avg: 'Genomsnitt: ',
13510 pinLeft: 'Fäst vänster',
13511 pinRight: 'Fäst höger',
13515 columns: 'Kolumner:',
13516 importerTitle: 'Importera fil',
13517 exporterAllAsCsv: 'Exportera all data som CSV',
13518 exporterVisibleAsCsv: 'Exportera synlig data som CSV',
13519 exporterSelectedAsCsv: 'Exportera markerad data som CSV',
13520 exporterAllAsPdf: 'Exportera all data som PDF',
13521 exporterVisibleAsPdf: 'Exportera synlig data som PDF',
13522 exporterSelectedAsPdf: 'Exportera markerad data som PDF',
13523 clearAllFilters: 'Rengör alla filter'
13526 noHeaders: 'Kolumnnamn kunde inte härledas. Har filen ett sidhuvud?',
13527 noObjects: 'Objekt kunde inte härledas. Har filen data undantaget sidhuvud?',
13528 invalidCsv: 'Filen kunde inte behandlas, är den en giltig CSV?',
13529 invalidJson: 'Filen kunde inte behandlas, är den en giltig JSON?',
13530 jsonNotArray: 'Importerad JSON-fil måste innehålla ett fält. Import avbruten.'
13533 sizes: 'Artiklar per sida',
13534 totalItems: 'Artiklar'
13543 angular.module('ui.grid').config(['$provide', function($provide) {
13544 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13545 $delegate.add('ta', {
13547 label: 'உருப்படிகள்'
13550 description: 'ஒரு பத்தியை குழுவாக அமைக்க அப்பத்தியின் தலைப்பை இங்கே இழுத்து வரவும் '
13553 placeholder: 'தேடல் ...',
13554 showingItems: 'உருப்படிகளை காண்பித்தல்:',
13555 selectedItems: 'தேர்ந்தெடுக்கப்பட்ட உருப்படிகள்:',
13556 totalItems: 'மொத்த உருப்படிகள்:',
13557 size: 'பக்க அளவு: ',
13558 first: 'முதல் பக்கம்',
13559 next: 'அடுத்த பக்கம்',
13560 previous: 'முந்தைய பக்கம் ',
13561 last: 'இறுதி பக்கம்'
13564 text: 'பத்திகளை தேர்ந்தெடு:'
13567 ascending: 'மேலிருந்து கீழாக',
13568 descending: 'கீழிருந்து மேலாக',
13569 remove: 'வரிசையை நீக்கு'
13572 hide: 'பத்தியை மறைத்து வை '
13575 count: 'மொத்த வரிகள்:',
13578 min: 'குறைந்தபட்ச: ',
13582 pinLeft: 'இடதுபுறமாக தைக்க ',
13583 pinRight: 'வலதுபுறமாக தைக்க',
13587 columns: 'பத்திகள்:',
13588 importerTitle: 'கோப்பு : படித்தல்',
13589 exporterAllAsCsv: 'எல்லா தரவுகளையும் கோப்பாக்கு: csv',
13590 exporterVisibleAsCsv: 'இருக்கும் தரவுகளை கோப்பாக்கு: csv',
13591 exporterSelectedAsCsv: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: csv',
13592 exporterAllAsPdf: 'எல்லா தரவுகளையும் கோப்பாக்கு: pdf',
13593 exporterVisibleAsPdf: 'இருக்கும் தரவுகளை கோப்பாக்கு: pdf',
13594 exporterSelectedAsPdf: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: pdf',
13595 clearAllFilters: 'Clear all filters'
13598 noHeaders: 'பத்தியின் தலைப்புகளை பெற இயலவில்லை, கோப்பிற்கு தலைப்பு உள்ளதா?',
13599 noObjects: 'இலக்குகளை உருவாக்க முடியவில்லை, கோப்பில் தலைப்புகளை தவிர தரவு ஏதேனும் உள்ளதா? ',
13600 invalidCsv: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - csv',
13601 invalidJson: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - json',
13602 jsonNotArray: 'படித்த கோப்பில் வரிசைகள் உள்ளது, நடைமுறை ரத்து செய் : json'
13605 sizes : 'உருப்படிகள் / பக்கம்',
13606 totalItems : 'உருப்படிகள் '
13611 aggregate_count : 'மதிப்பீட்டு : எண்ணு',
13612 aggregate_sum : 'மதிப்பீட்டு : கூட்டல்',
13613 aggregate_max : 'மதிப்பீட்டு : அதிகபட்சம்',
13614 aggregate_min : 'மதிப்பீட்டு : குறைந்தபட்சம்',
13615 aggregate_avg : 'மதிப்பீட்டு : சராசரி',
13616 aggregate_remove : 'மதிப்பீட்டு : நீக்கு'
13625 angular.module('ui.grid').config(['$provide', function($provide) {
13626 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13627 $delegate.add('tr', {
13630 defaultFilterLabel: 'Sütun için filtre',
13631 removeFilter: 'Filtreyi Kaldır',
13632 columnMenuButtonLabel: 'Sütun Menüsü'
13634 priority: 'Öncelik:',
13635 filterLabel: "Sütun için filtre: "
13641 description: 'Sütuna göre gruplamak için sütun başlığını buraya sürükleyin ve bırakın.'
13644 placeholder: 'Arama...',
13645 showingItems: 'Gösterilen Kayıt:',
13646 selectedItems: 'Seçili Kayıt:',
13647 totalItems: 'Toplam Kayıt:',
13648 size: 'Sayfa Boyutu:',
13649 first: 'İlk Sayfa',
13650 next: 'Sonraki Sayfa',
13651 previous: 'Önceki Sayfa',
13655 text: 'Sütunları Seç:'
13658 ascending: 'Artan Sırada Sırala',
13659 descending: 'Azalan Sırada Sırala',
13660 none: 'Sıralama Yapma',
13661 remove: 'Sıralamayı Kaldır'
13664 hide: 'Sütunu Gizle'
13667 count: 'toplam satır: ',
13674 pinLeft: 'Sola Sabitle',
13675 pinRight: 'Sağa Sabitle',
13676 unpin: 'Sabitlemeyi Kaldır'
13683 buttonLabel: 'Tablo Menü'
13685 columns: 'Sütunlar:',
13686 importerTitle: 'Dosya içeri aktar',
13687 exporterAllAsCsv: 'Bütün veriyi CSV olarak dışarı aktar',
13688 exporterVisibleAsCsv: 'Görünen veriyi CSV olarak dışarı aktar',
13689 exporterSelectedAsCsv: 'Seçili veriyi CSV olarak dışarı aktar',
13690 exporterAllAsPdf: 'Bütün veriyi PDF olarak dışarı aktar',
13691 exporterVisibleAsPdf: 'Görünen veriyi PDF olarak dışarı aktar',
13692 exporterSelectedAsPdf: 'Seçili veriyi PDF olarak dışarı aktar',
13693 clearAllFilters: 'Bütün filtreleri kaldır'
13696 noHeaders: 'Sütun isimleri üretilemiyor, dosyanın bir başlığı var mı?',
13697 noObjects: 'Nesneler üretilemiyor, dosyada başlıktan başka bir veri var mı?',
13698 invalidCsv: 'Dosya işlenemedi, geçerli bir CSV dosyası mı?',
13699 invalidJson: 'Dosya işlenemedi, geçerli bir Json dosyası mı?',
13700 jsonNotArray: 'Alınan Json dosyasında bir dizi bulunmalıdır, işlem iptal ediliyor.'
13704 pageToFirst: 'İlk sayfaya',
13705 pageBack: 'Geri git',
13706 pageSelected: 'Seçili sayfa',
13707 pageForward: 'İleri git',
13708 pageToLast: 'Sona git'
13710 sizes: 'Sayfadaki nesne sayısı',
13711 totalItems: 'kayıtlar',
13712 through: '', //note(fsw) : turkish dont have this preposition
13713 of: '' //note(fsw) : turkish dont have this preposition
13717 ungroup: 'Gruplama',
13718 aggregate_count: 'Yekun: Sayı',
13719 aggregate_sum: 'Yekun: Toplam',
13720 aggregate_max: 'Yekun: Maks',
13721 aggregate_min: 'Yekun: Min',
13722 aggregate_avg: 'Yekun: Ort',
13723 aggregate_remove: 'Yekun: Sil'
13732 * @name ui.grid.i18n
13736 * This module provides i18n functions to ui.grid and any application that wants to use it
13739 * <div doc-module-components="ui.grid.i18n"></div>
13743 var DIRECTIVE_ALIASES = ['uiT', 'uiTranslate'];
13744 var FILTER_ALIASES = ['t', 'uiTranslate'];
13746 var module = angular.module('ui.grid.i18n');
13751 * @name ui.grid.i18n.constant:i18nConstants
13753 * @description constants available in i18n module
13755 module.constant('i18nConstants', {
13756 MISSING: '[MISSING]',
13757 UPDATE_EVENT: '$uiI18n',
13759 LOCALE_DIRECTIVE_ALIAS: 'uiI18n',
13760 // default to english
13764 // module.config(['$provide', function($provide) {
13765 // $provide.decorator('i18nService', ['$delegate', function($delegate) {}])}]);
13769 * @name ui.grid.i18n.service:i18nService
13771 * @description Services for i18n
13773 module.service('i18nService', ['$log', 'i18nConstants', '$rootScope',
13774 function ($log, i18nConstants, $rootScope) {
13779 get: function (lang) {
13780 return this._langs[lang.toLowerCase()];
13782 add: function (lang, strings) {
13783 var lower = lang.toLowerCase();
13784 if (!this._langs[lower]) {
13785 this._langs[lower] = {};
13787 angular.extend(this._langs[lower], strings);
13789 getAllLangs: function () {
13791 if (!this._langs) {
13795 for (var key in this._langs) {
13801 setCurrent: function (lang) {
13802 this.current = lang.toLowerCase();
13804 getCurrentLang: function () {
13805 return this.current;
13814 * @methodOf ui.grid.i18n.service:i18nService
13815 * @description Adds the languages and strings to the cache. Decorate this service to
13816 * add more translation strings
13817 * @param {string} lang language to add
13818 * @param {object} stringMaps of strings to add grouped by property names
13821 * i18nService.add('en', {
13824 * label2: 'some more items'
13828 * description: 'Drag a column header here and drop it to group by that column.'
13833 add: function (langs, stringMaps) {
13834 if (typeof(langs) === 'object') {
13835 angular.forEach(langs, function (lang) {
13837 langCache.add(lang, stringMaps);
13841 langCache.add(langs, stringMaps);
13847 * @name getAllLangs
13848 * @methodOf ui.grid.i18n.service:i18nService
13849 * @description return all currently loaded languages
13850 * @returns {array} string
13852 getAllLangs: function () {
13853 return langCache.getAllLangs();
13859 * @methodOf ui.grid.i18n.service:i18nService
13860 * @description return all currently loaded languages
13861 * @param {string} lang to return. If not specified, returns current language
13862 * @returns {object} the translation string maps for the language
13864 get: function (lang) {
13865 var language = lang ? lang : service.getCurrentLang();
13866 return langCache.get(language);
13871 * @name getSafeText
13872 * @methodOf ui.grid.i18n.service:i18nService
13873 * @description returns the text specified in the path or a Missing text if text is not found
13874 * @param {string} path property path to use for retrieving text from string map
13875 * @param {string} lang to return. If not specified, returns current language
13876 * @returns {object} the translation for the path
13879 * i18nService.getSafeText('sort.ascending')
13882 getSafeText: function (path, lang) {
13883 var language = lang ? lang : service.getCurrentLang();
13884 var trans = langCache.get(language);
13887 return i18nConstants.MISSING;
13890 var paths = path.split('.');
13891 var current = trans;
13893 for (var i = 0; i < paths.length; ++i) {
13894 if (current[paths[i]] === undefined || current[paths[i]] === null) {
13895 return i18nConstants.MISSING;
13897 current = current[paths[i]];
13907 * @name setCurrentLang
13908 * @methodOf ui.grid.i18n.service:i18nService
13909 * @description sets the current language to use in the application
13910 * $broadcasts the Update_Event on the $rootScope
13911 * @param {string} lang to set
13914 * i18nService.setCurrentLang('fr');
13918 setCurrentLang: function (lang) {
13920 langCache.setCurrent(lang);
13921 $rootScope.$broadcast(i18nConstants.UPDATE_EVENT);
13927 * @name getCurrentLang
13928 * @methodOf ui.grid.i18n.service:i18nService
13929 * @description returns the current language used in the application
13931 getCurrentLang: function () {
13932 var lang = langCache.getCurrentLang();
13934 lang = i18nConstants.DEFAULT_LANG;
13935 langCache.setCurrent(lang);
13946 var localeDirective = function (i18nService, i18nConstants) {
13948 compile: function () {
13950 pre: function ($scope, $elm, $attrs) {
13951 var alias = i18nConstants.LOCALE_DIRECTIVE_ALIAS;
13952 // check for watchable property
13953 var lang = $scope.$eval($attrs[alias]);
13955 $scope.$watch($attrs[alias], function () {
13956 i18nService.setCurrentLang(lang);
13958 } else if ($attrs.$$observers) {
13959 $attrs.$observe(alias, function () {
13960 i18nService.setCurrentLang($attrs[alias] || i18nConstants.DEFAULT_LANG);
13969 module.directive('uiI18n', ['i18nService', 'i18nConstants', localeDirective]);
13971 // directive syntax
13972 var uitDirective = function ($parse, i18nService, i18nConstants) {
13975 compile: function () {
13977 pre: function ($scope, $elm, $attrs) {
13978 var alias1 = DIRECTIVE_ALIASES[0],
13979 alias2 = DIRECTIVE_ALIASES[1];
13980 var token = $attrs[alias1] || $attrs[alias2] || $elm.html();
13981 var missing = i18nConstants.MISSING + token;
13983 if ($attrs.$$observers) {
13984 var prop = $attrs[alias1] ? alias1 : alias2;
13985 observer = $attrs.$observe(prop, function (result) {
13987 $elm.html($parse(result)(i18nService.getCurrentLang()) || missing);
13991 var getter = $parse(token);
13992 var listener = $scope.$on(i18nConstants.UPDATE_EVENT, function (evt) {
13994 observer($attrs[alias1] || $attrs[alias2]);
13996 // set text based on i18n current language
13997 $elm.html(getter(i18nService.get()) || missing);
14000 $scope.$on('$destroy', listener);
14002 $elm.html(getter(i18nService.get()) || missing);
14009 angular.forEach( DIRECTIVE_ALIASES, function ( alias ) {
14010 module.directive( alias, ['$parse', 'i18nService', 'i18nConstants', uitDirective] );
14013 // optional filter syntax
14014 var uitFilter = function ($parse, i18nService, i18nConstants) {
14015 return function (data) {
14016 var getter = $parse(data);
14017 // set text based on i18n current language
14018 return getter(i18nService.get()) || i18nConstants.MISSING + data;
14022 angular.forEach( FILTER_ALIASES, function ( alias ) {
14023 module.filter( alias, ['$parse', 'i18nService', 'i18nConstants', uitFilter] );
14029 angular.module('ui.grid').config(['$provide', function($provide) {
14030 $provide.decorator('i18nService', ['$delegate', function($delegate) {
14031 $delegate.add('zh-cn', {
14034 defaultFilterLabel: '列过滤器',
14035 removeFilter: '移除过滤器',
14036 columnMenuButtonLabel: '列菜单'
14039 filterLabel: "列过滤器: "
14045 description: '拖曳表头到此处进行分组'
14049 showingItems: '已显示行数:',
14050 selectedItems: '已选择行数:',
14051 totalItems: '总行数:',
14087 buttonLabel: '表格菜单'
14090 importerTitle: '导入文件',
14091 exporterAllAsCsv: '导出全部数据到CSV',
14092 exporterVisibleAsCsv: '导出可见数据到CSV',
14093 exporterSelectedAsCsv: '导出已选数据到CSV',
14094 exporterAllAsPdf: '导出全部数据到PDF',
14095 exporterVisibleAsPdf: '导出可见数据到PDF',
14096 exporterSelectedAsPdf: '导出已选数据到PDF',
14097 clearAllFilters: '清除所有过滤器'
14100 noHeaders: '无法获取列名,确定文件包含表头?',
14101 noObjects: '无法获取数据,确定文件包含数据?',
14102 invalidCsv: '无法处理文件,确定是合法的CSV文件?',
14103 invalidJson: '无法处理文件,确定是合法的JSON文件?',
14104 jsonNotArray: '导入的文件不是JSON数组!'
14108 pageToFirst: '第一页',
14110 pageSelected: '当前页',
14111 pageForward: '下一页',
14122 aggregate_count: '合计: 计数',
14123 aggregate_sum: '合计: 求和',
14124 aggregate_max: '合计: 最大',
14125 aggregate_min: '合计: 最小',
14126 aggregate_avg: '合计: 平均',
14127 aggregate_remove: '合计: 移除'
14136 angular.module('ui.grid').config(['$provide', function($provide) {
14137 $provide.decorator('i18nService', ['$delegate', function($delegate) {
14138 $delegate.add('zh-tw', {
14143 description: '拖曳表頭到此處進行分組'
14147 showingItems: '已顯示行數:',
14148 selectedItems: '已選擇行數:',
14149 totalItems: '總行數:',
14181 importerTitle: '導入文件',
14182 exporterAllAsCsv: '導出全部數據到CSV',
14183 exporterVisibleAsCsv: '導出可見數據到CSV',
14184 exporterSelectedAsCsv: '導出已選數據到CSV',
14185 exporterAllAsPdf: '導出全部數據到PDF',
14186 exporterVisibleAsPdf: '導出可見數據到PDF',
14187 exporterSelectedAsPdf: '導出已選數據到PDF',
14188 clearAllFilters: '清除所有过滤器'
14191 noHeaders: '無法獲取列名,確定文件包含表頭?',
14192 noObjects: '無法獲取數據,確定文件包含數據?',
14193 invalidCsv: '無法處理文件,確定是合法的CSV文件?',
14194 invalidJson: '無法處理文件,確定是合法的JSON文件?',
14195 jsonNotArray: '導入的文件不是JSON數組!'
14211 * @name ui.grid.autoResize
14215 * #ui.grid.autoResize
14217 * <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>
14219 * This module provides auto-resizing functionality to UI-Grid.
14221 var module = angular.module('ui.grid.autoResize', ['ui.grid']);
14224 module.directive('uiGridAutoResize', ['$timeout', 'gridUtil', function ($timeout, gridUtil) {
14228 link: function ($scope, $elm, $attrs, uiGridCtrl) {
14229 var prevGridWidth, prevGridHeight;
14231 function getDimensions() {
14232 prevGridHeight = gridUtil.elementHeight($elm);
14233 prevGridWidth = gridUtil.elementWidth($elm);
14236 // Initialize the dimensions
14239 var resizeTimeoutId;
14240 function startTimeout() {
14241 clearTimeout(resizeTimeoutId);
14243 resizeTimeoutId = setTimeout(function () {
14244 var newGridHeight = gridUtil.elementHeight($elm);
14245 var newGridWidth = gridUtil.elementWidth($elm);
14247 if (newGridHeight !== prevGridHeight || newGridWidth !== prevGridWidth) {
14248 uiGridCtrl.grid.gridHeight = newGridHeight;
14249 uiGridCtrl.grid.gridWidth = newGridWidth;
14251 $scope.$apply(function () {
14252 uiGridCtrl.grid.refresh()
14253 .then(function () {
14268 $scope.$on('$destroy', function() {
14269 clearTimeout(resizeTimeoutId);
14281 * @name ui.grid.cellNav
14287 <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>
14289 This module provides auto-resizing functionality to UI-Grid.
14291 var module = angular.module('ui.grid.cellNav', ['ui.grid']);
14295 * @name ui.grid.cellNav.constant:uiGridCellNavConstants
14297 * @description constants available in cellNav
14299 module.constant('uiGridCellNavConstants', {
14300 FEATURE_NAME: 'gridCellNav',
14301 CELL_NAV_EVENT: 'cellNav',
14302 direction: {LEFT: 0, RIGHT: 1, UP: 2, DOWN: 3, PG_UP: 4, PG_DOWN: 5},
14311 module.factory('uiGridCellNavFactory', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', 'GridRowColumn', '$q',
14312 function (gridUtil, uiGridConstants, uiGridCellNavConstants, GridRowColumn, $q) {
14315 * @name ui.grid.cellNav.object:CellNav
14316 * @description returns a CellNav prototype function
14317 * @param {object} rowContainer container for rows
14318 * @param {object} colContainer parent column container
14319 * @param {object} leftColContainer column container to the left of parent
14320 * @param {object} rightColContainer column container to the right of parent
14322 var UiGridCellNav = function UiGridCellNav(rowContainer, colContainer, leftColContainer, rightColContainer) {
14323 this.rows = rowContainer.visibleRowCache;
14324 this.columns = colContainer.visibleColumnCache;
14325 this.leftColumns = leftColContainer ? leftColContainer.visibleColumnCache : [];
14326 this.rightColumns = rightColContainer ? rightColContainer.visibleColumnCache : [];
14327 this.bodyContainer = rowContainer;
14330 /** returns focusable columns of all containers */
14331 UiGridCellNav.prototype.getFocusableCols = function () {
14332 var allColumns = this.leftColumns.concat(this.columns, this.rightColumns);
14334 return allColumns.filter(function (col) {
14335 return col.colDef.allowCellFocus;
14341 * @name ui.grid.cellNav.api:GridRow
14343 * @description GridRow settings for cellNav feature, these are available to be
14344 * set only internally (for example, by other features)
14349 * @name allowCellFocus
14350 * @propertyOf ui.grid.cellNav.api:GridRow
14351 * @description Enable focus on a cell within this row. If set to false then no cells
14352 * in this row can be focused - group header rows as an example would set this to false.
14353 * <br/>Defaults to true
14355 /** returns focusable rows */
14356 UiGridCellNav.prototype.getFocusableRows = function () {
14357 return this.rows.filter(function(row) {
14358 return row.allowCellFocus !== false;
14362 UiGridCellNav.prototype.getNextRowCol = function (direction, curRow, curCol) {
14363 switch (direction) {
14364 case uiGridCellNavConstants.direction.LEFT:
14365 return this.getRowColLeft(curRow, curCol);
14366 case uiGridCellNavConstants.direction.RIGHT:
14367 return this.getRowColRight(curRow, curCol);
14368 case uiGridCellNavConstants.direction.UP:
14369 return this.getRowColUp(curRow, curCol);
14370 case uiGridCellNavConstants.direction.DOWN:
14371 return this.getRowColDown(curRow, curCol);
14372 case uiGridCellNavConstants.direction.PG_UP:
14373 return this.getRowColPageUp(curRow, curCol);
14374 case uiGridCellNavConstants.direction.PG_DOWN:
14375 return this.getRowColPageDown(curRow, curCol);
14380 UiGridCellNav.prototype.initializeSelection = function () {
14381 var focusableCols = this.getFocusableCols();
14382 var focusableRows = this.getFocusableRows();
14383 if (focusableCols.length === 0 || focusableRows.length === 0) {
14387 var curRowIndex = 0;
14388 var curColIndex = 0;
14389 return new GridRowColumn(focusableRows[0], focusableCols[0]); //return same row
14392 UiGridCellNav.prototype.getRowColLeft = function (curRow, curCol) {
14393 var focusableCols = this.getFocusableCols();
14394 var focusableRows = this.getFocusableRows();
14395 var curColIndex = focusableCols.indexOf(curCol);
14396 var curRowIndex = focusableRows.indexOf(curRow);
14398 //could not find column in focusable Columns so set it to 1
14399 if (curColIndex === -1) {
14403 var nextColIndex = curColIndex === 0 ? focusableCols.length - 1 : curColIndex - 1;
14405 //get column to left
14406 if (nextColIndex > curColIndex) {
14407 // On the first row
14408 // if (curRowIndex === 0 && curColIndex === 0) {
14411 if (curRowIndex === 0) {
14412 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
14415 //up one row and far right column
14416 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[nextColIndex]);
14420 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
14426 UiGridCellNav.prototype.getRowColRight = function (curRow, curCol) {
14427 var focusableCols = this.getFocusableCols();
14428 var focusableRows = this.getFocusableRows();
14429 var curColIndex = focusableCols.indexOf(curCol);
14430 var curRowIndex = focusableRows.indexOf(curRow);
14432 //could not find column in focusable Columns so set it to 0
14433 if (curColIndex === -1) {
14436 var nextColIndex = curColIndex === focusableCols.length - 1 ? 0 : curColIndex + 1;
14438 if (nextColIndex < curColIndex) {
14439 if (curRowIndex === focusableRows.length - 1) {
14440 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
14443 //down one row and far left column
14444 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[nextColIndex]);
14448 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
14452 UiGridCellNav.prototype.getRowColDown = function (curRow, curCol) {
14453 var focusableCols = this.getFocusableCols();
14454 var focusableRows = this.getFocusableRows();
14455 var curColIndex = focusableCols.indexOf(curCol);
14456 var curRowIndex = focusableRows.indexOf(curRow);
14458 //could not find column in focusable Columns so set it to 0
14459 if (curColIndex === -1) {
14463 if (curRowIndex === focusableRows.length - 1) {
14464 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
14468 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[curColIndex]);
14472 UiGridCellNav.prototype.getRowColPageDown = function (curRow, curCol) {
14473 var focusableCols = this.getFocusableCols();
14474 var focusableRows = this.getFocusableRows();
14475 var curColIndex = focusableCols.indexOf(curCol);
14476 var curRowIndex = focusableRows.indexOf(curRow);
14478 //could not find column in focusable Columns so set it to 0
14479 if (curColIndex === -1) {
14483 var pageSize = this.bodyContainer.minRowsToRender();
14484 if (curRowIndex >= focusableRows.length - pageSize) {
14485 return new GridRowColumn(focusableRows[focusableRows.length - 1], focusableCols[curColIndex]); //return last row
14489 return new GridRowColumn(focusableRows[curRowIndex + pageSize], focusableCols[curColIndex]);
14493 UiGridCellNav.prototype.getRowColUp = function (curRow, curCol) {
14494 var focusableCols = this.getFocusableCols();
14495 var focusableRows = this.getFocusableRows();
14496 var curColIndex = focusableCols.indexOf(curCol);
14497 var curRowIndex = focusableRows.indexOf(curRow);
14499 //could not find column in focusable Columns so set it to 0
14500 if (curColIndex === -1) {
14504 if (curRowIndex === 0) {
14505 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
14509 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[curColIndex]);
14513 UiGridCellNav.prototype.getRowColPageUp = function (curRow, curCol) {
14514 var focusableCols = this.getFocusableCols();
14515 var focusableRows = this.getFocusableRows();
14516 var curColIndex = focusableCols.indexOf(curCol);
14517 var curRowIndex = focusableRows.indexOf(curRow);
14519 //could not find column in focusable Columns so set it to 0
14520 if (curColIndex === -1) {
14524 var pageSize = this.bodyContainer.minRowsToRender();
14525 if (curRowIndex - pageSize < 0) {
14526 return new GridRowColumn(focusableRows[0], focusableCols[curColIndex]); //return first row
14530 return new GridRowColumn(focusableRows[curRowIndex - pageSize], focusableCols[curColIndex]);
14533 return UiGridCellNav;
14538 * @name ui.grid.cellNav.service:uiGridCellNavService
14540 * @description Services for cell navigation features. If you don't like the key maps we use,
14541 * or the direction cells navigation, override with a service decorator (see angular docs)
14543 module.service('uiGridCellNavService', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q', 'uiGridCellNavFactory', 'GridRowColumn', 'ScrollEvent',
14544 function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q, UiGridCellNav, GridRowColumn, ScrollEvent) {
14548 initializeGrid: function (grid) {
14549 grid.registerColumnBuilder(service.cellNavColumnBuilder);
14554 * @name ui.grid.cellNav:Grid.cellNav
14555 * @description cellNav properties added to grid class
14558 grid.cellNav.lastRowCol = null;
14559 grid.cellNav.focusedCells = [];
14561 service.defaultGridOptions(grid.options);
14565 * @name ui.grid.cellNav.api:PublicApi
14567 * @description Public Api for cellNav feature
14575 * @eventOf ui.grid.cellNav.api:PublicApi
14576 * @description raised when the active cell is changed
14578 * gridApi.cellNav.on.navigate(scope,function(newRowcol, oldRowCol){})
14580 * @param {object} newRowCol new position
14581 * @param {object} oldRowCol old position
14583 navigate: function (newRowCol, oldRowCol) {},
14586 * @name viewPortKeyDown
14587 * @eventOf ui.grid.cellNav.api:PublicApi
14588 * @description is raised when the viewPort receives a keyDown event. Cells never get focus in uiGrid
14589 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14590 * event whenever you need a keydown event on a cell
14592 * @param {object} event keydown event
14593 * @param {object} rowCol current rowCol position
14595 viewPortKeyDown: function (event, rowCol) {},
14599 * @name viewPortKeyPress
14600 * @eventOf ui.grid.cellNav.api:PublicApi
14601 * @description is raised when the viewPort receives a keyPress event. Cells never get focus in uiGrid
14602 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14603 * event whenever you need a keypress event on a cell
14605 * @param {object} event keypress event
14606 * @param {object} rowCol current rowCol position
14608 viewPortKeyPress: function (event, rowCol) {}
14615 * @name scrollToFocus
14616 * @methodOf ui.grid.cellNav.api:PublicApi
14617 * @description brings the specified row and column into view, and sets focus
14619 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus
14620 * @param {object} colDef to make visible and set focus
14621 * @returns {promise} a promise that is resolved after any scrolling is finished
14623 scrollToFocus: function (rowEntity, colDef) {
14624 return service.scrollToFocus(grid, rowEntity, colDef);
14629 * @name getFocusedCell
14630 * @methodOf ui.grid.cellNav.api:PublicApi
14631 * @description returns the current (or last if Grid does not have focus) focused row and column
14632 * <br> value is null if no selection has occurred
14634 getFocusedCell: function () {
14635 return grid.cellNav.lastRowCol;
14640 * @name getCurrentSelection
14641 * @methodOf ui.grid.cellNav.api:PublicApi
14642 * @description returns an array containing the current selection
14643 * <br> array is empty if no selection has occurred
14645 getCurrentSelection: function () {
14646 return grid.cellNav.focusedCells;
14651 * @name rowColSelectIndex
14652 * @methodOf ui.grid.cellNav.api:PublicApi
14653 * @description returns the index in the order in which the GridRowColumn was selected, returns -1 if the GridRowColumn
14655 * @param {object} rowCol the rowCol to evaluate
14657 rowColSelectIndex: function (rowCol) {
14658 //return gridUtil.arrayContainsObjectWithProperty(grid.cellNav.focusedCells, 'col.uid', rowCol.col.uid) &&
14660 for (var i = 0; i < grid.cellNav.focusedCells.length; i++) {
14661 if (grid.cellNav.focusedCells[i].col.uid === rowCol.col.uid &&
14662 grid.cellNav.focusedCells[i].row.uid === rowCol.row.uid) {
14673 grid.api.registerEventsFromObject(publicApi.events);
14675 grid.api.registerMethodsFromObject(publicApi.methods);
14679 defaultGridOptions: function (gridOptions) {
14682 * @name ui.grid.cellNav.api:GridOptions
14684 * @description GridOptions for cellNav feature, these are available to be
14685 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
14690 * @name modifierKeysToMultiSelectCells
14691 * @propertyOf ui.grid.cellNav.api:GridOptions
14692 * @description Enable multiple cell selection only when using the ctrlKey or shiftKey.
14693 * <br/>Defaults to false
14695 gridOptions.modifierKeysToMultiSelectCells = gridOptions.modifierKeysToMultiSelectCells === true;
14701 * @name decorateRenderContainers
14702 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14703 * @description decorates grid renderContainers with cellNav functions
14705 decorateRenderContainers: function (grid) {
14707 var rightContainer = grid.hasRightContainer() ? grid.renderContainers.right : null;
14708 var leftContainer = grid.hasLeftContainer() ? grid.renderContainers.left : null;
14710 if (leftContainer !== null) {
14711 grid.renderContainers.left.cellNav = new UiGridCellNav(grid.renderContainers.body, leftContainer, rightContainer, grid.renderContainers.body);
14713 if (rightContainer !== null) {
14714 grid.renderContainers.right.cellNav = new UiGridCellNav(grid.renderContainers.body, rightContainer, grid.renderContainers.body, leftContainer);
14717 grid.renderContainers.body.cellNav = new UiGridCellNav(grid.renderContainers.body, grid.renderContainers.body, leftContainer, rightContainer);
14722 * @name getDirection
14723 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14724 * @description determines which direction to for a given keyDown event
14725 * @returns {uiGridCellNavConstants.direction} direction
14727 getDirection: function (evt) {
14728 if (evt.keyCode === uiGridConstants.keymap.LEFT ||
14729 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey)) {
14730 return uiGridCellNavConstants.direction.LEFT;
14732 if (evt.keyCode === uiGridConstants.keymap.RIGHT ||
14733 evt.keyCode === uiGridConstants.keymap.TAB) {
14734 return uiGridCellNavConstants.direction.RIGHT;
14737 if (evt.keyCode === uiGridConstants.keymap.UP ||
14738 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ) {
14739 return uiGridCellNavConstants.direction.UP;
14742 if (evt.keyCode === uiGridConstants.keymap.PG_UP){
14743 return uiGridCellNavConstants.direction.PG_UP;
14746 if (evt.keyCode === uiGridConstants.keymap.DOWN ||
14747 evt.keyCode === uiGridConstants.keymap.ENTER && !(evt.ctrlKey || evt.altKey)) {
14748 return uiGridCellNavConstants.direction.DOWN;
14751 if (evt.keyCode === uiGridConstants.keymap.PG_DOWN){
14752 return uiGridCellNavConstants.direction.PG_DOWN;
14760 * @name cellNavColumnBuilder
14761 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14762 * @description columnBuilder function that adds cell navigation properties to grid column
14763 * @returns {promise} promise that will load any needed templates when resolved
14765 cellNavColumnBuilder: function (colDef, col, gridOptions) {
14770 * @name ui.grid.cellNav.api:ColumnDef
14772 * @description Column Definitions for cellNav feature, these are available to be
14773 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
14778 * @name allowCellFocus
14779 * @propertyOf ui.grid.cellNav.api:ColumnDef
14780 * @description Enable focus on a cell within this column.
14781 * <br/>Defaults to true
14783 colDef.allowCellFocus = colDef.allowCellFocus === undefined ? true : colDef.allowCellFocus;
14785 return $q.all(promises);
14790 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14791 * @name scrollToFocus
14792 * @description Scroll the grid such that the specified
14793 * row and column is in view, and set focus to the cell in that row and column
14794 * @param {Grid} grid the grid you'd like to act upon, usually available
14795 * from gridApi.grid
14796 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus to
14797 * @param {object} colDef to make visible and set focus to
14798 * @returns {promise} a promise that is resolved after any scrolling is finished
14800 scrollToFocus: function (grid, rowEntity, colDef) {
14801 var gridRow = null, gridCol = null;
14803 if (typeof(rowEntity) !== 'undefined' && rowEntity !== null) {
14804 gridRow = grid.getRow(rowEntity);
14807 if (typeof(colDef) !== 'undefined' && colDef !== null) {
14808 gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field);
14810 return grid.api.core.scrollToIfNecessary(gridRow, gridCol).then(function () {
14811 var rowCol = { row: gridRow, col: gridCol };
14813 // Broadcast the navigation
14814 if (gridRow !== null && gridCol !== null) {
14815 grid.cellNav.broadcastCellNav(rowCol);
14826 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14827 * @name getLeftWidth
14828 * @description Get the current drawn width of the columns in the
14829 * grid up to the numbered column, and add an apportionment for the
14830 * column that we're on. So if we are on column 0, we want to scroll
14831 * 0% (i.e. exclude this column from calc). If we're on the last column
14832 * we want to scroll to 100% (i.e. include this column in the calc). So
14833 * we include (thisColIndex / totalNumberCols) % of this column width
14834 * @param {Grid} grid the grid you'd like to act upon, usually available
14835 * from gridApi.grid
14836 * @param {gridCol} upToCol the column to total up to and including
14838 getLeftWidth: function (grid, upToCol) {
14845 var lastIndex = grid.renderContainers.body.visibleColumnCache.indexOf( upToCol );
14847 // total column widths up-to but not including the passed in column
14848 grid.renderContainers.body.visibleColumnCache.forEach( function( col, index ) {
14849 if ( index < lastIndex ){
14850 width += col.drawnWidth;
14854 // pro-rata the final column based on % of total columns.
14855 var percentage = lastIndex === 0 ? 0 : (lastIndex + 1) / grid.renderContainers.body.visibleColumnCache.length;
14856 width += upToCol.drawnWidth * percentage;
14867 * @name ui.grid.cellNav.directive:uiCellNav
14871 * @description Adds cell navigation features to the grid columns
14874 <example module="app">
14875 <file name="app.js">
14876 var app = angular.module('app', ['ui.grid', 'ui.grid.cellNav']);
14878 app.controller('MainCtrl', ['$scope', function ($scope) {
14880 { name: 'Bob', title: 'CEO' },
14881 { name: 'Frank', title: 'Lowly Developer' }
14884 $scope.columnDefs = [
14890 <file name="index.html">
14891 <div ng-controller="MainCtrl">
14892 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-cellnav></div>
14897 module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn', '$timeout', '$compile',
14898 function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, GridRowColumn, $timeout, $compile) {
14902 require: '^uiGrid',
14904 controller: function () {},
14905 compile: function () {
14907 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
14908 var _scope = $scope;
14910 var grid = uiGridCtrl.grid;
14911 uiGridCellNavService.initializeGrid(grid);
14913 uiGridCtrl.cellNav = {};
14915 //Ensure that the object has all of the methods we expect it to
14916 uiGridCtrl.cellNav.makeRowCol = function (obj) {
14917 if (!(obj instanceof GridRowColumn)) {
14918 obj = new GridRowColumn(obj.row, obj.col);
14923 uiGridCtrl.cellNav.getActiveCell = function () {
14924 var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus');
14925 if (elms.length > 0){
14932 uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown, originEvt) {
14933 modifierDown = !(modifierDown === undefined || !modifierDown);
14935 newRowCol = uiGridCtrl.cellNav.makeRowCol(newRowCol);
14937 uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown, originEvt);
14938 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, newRowCol, modifierDown, originEvt);
14941 uiGridCtrl.cellNav.clearFocus = grid.cellNav.clearFocus = function () {
14942 grid.cellNav.focusedCells = [];
14943 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT);
14946 uiGridCtrl.cellNav.broadcastFocus = function (rowCol, modifierDown, originEvt) {
14947 modifierDown = !(modifierDown === undefined || !modifierDown);
14949 rowCol = uiGridCtrl.cellNav.makeRowCol(rowCol);
14951 var row = rowCol.row,
14954 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14956 if (grid.cellNav.lastRowCol === null || rowColSelectIndex === -1) {
14957 var newRowCol = new GridRowColumn(row, col);
14959 if (grid.cellNav.lastRowCol === null || grid.cellNav.lastRowCol.row !== newRowCol.row || grid.cellNav.lastRowCol.col !== newRowCol.col){
14960 grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol);
14961 grid.cellNav.lastRowCol = newRowCol;
14963 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) {
14964 grid.cellNav.focusedCells.push(rowCol);
14966 grid.cellNav.focusedCells = [rowCol];
14968 } else if (grid.options.modifierKeysToMultiSelectCells && modifierDown &&
14969 rowColSelectIndex >= 0) {
14971 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14975 uiGridCtrl.cellNav.handleKeyDown = function (evt) {
14976 var direction = uiGridCellNavService.getDirection(evt);
14977 if (direction === null) {
14981 var containerId = 'body';
14982 if (evt.uiGridTargetRenderContainerId) {
14983 containerId = evt.uiGridTargetRenderContainerId;
14986 // Get the last-focused row+col combo
14987 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14989 // Figure out which new row+combo we're navigating to
14990 var rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(direction, lastRowCol.row, lastRowCol.col);
14991 var focusableCols = uiGridCtrl.grid.renderContainers[containerId].cellNav.getFocusableCols();
14992 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14993 // Shift+tab on top-left cell should exit cellnav on render container
14996 direction === uiGridCellNavConstants.direction.LEFT &&
14997 // New col is last col (i.e. wrap around)
14998 rowCol.col === focusableCols[focusableCols.length - 1] &&
14999 // Staying on same row, which means we're at first row
15000 rowCol.row === lastRowCol.row &&
15001 evt.keyCode === uiGridConstants.keymap.TAB &&
15004 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
15005 uiGridCtrl.cellNav.clearFocus();
15008 // Tab on bottom-right cell should exit cellnav on render container
15010 direction === uiGridCellNavConstants.direction.RIGHT &&
15011 // New col is first col (i.e. wrap around)
15012 rowCol.col === focusableCols[0] &&
15013 // Staying on same row, which means we're at first row
15014 rowCol.row === lastRowCol.row &&
15015 evt.keyCode === uiGridConstants.keymap.TAB &&
15018 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
15019 uiGridCtrl.cellNav.clearFocus();
15023 // Scroll to the new cell, if it's not completely visible within the render container's viewport
15024 grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () {
15025 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
15029 evt.stopPropagation();
15030 evt.preventDefault();
15036 post: function ($scope, $elm, $attrs, uiGridCtrl) {
15037 var _scope = $scope;
15038 var grid = uiGridCtrl.grid;
15040 function addAriaLiveRegion(){
15041 // Thanks to google docs for the inspiration behind how to do this
15042 // XXX: Why is this entire mess nessasary?
15043 // Because browsers take a lot of coercing to get them to read out live regions
15044 //http://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
15045 var ariaNotifierDomElt = '<div ' +
15046 'id="' + grid.id +'-aria-speakable" ' +
15047 'class="ui-grid-a11y-ariascreenreader-speakable ui-grid-offscreen" ' +
15048 'aria-live="assertive" ' +
15050 'aria-atomic="true" ' +
15051 'aria-hidden="false" ' +
15052 'aria-relevant="additions" ' +
15057 var ariaNotifier = $compile(ariaNotifierDomElt)($scope);
15058 $elm.prepend(ariaNotifier);
15059 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown, originEvt) {
15061 * If the cell nav event was because of a focus event then we don't want to
15062 * change the notifier text.
15063 * Reasoning: Voice Over fires a focus events when moving arround the grid.
15064 * If the screen reader is handing the grid nav properly then we don't need to
15065 * use the alert to notify the user of the movement.
15066 * In all other cases we do want a notification event.
15068 if (originEvt && originEvt.type === 'focus'){return;}
15070 function setNotifyText(text){
15071 if (text === ariaNotifier.text()){return;}
15072 ariaNotifier[0].style.clip = 'rect(0px,0px,0px,0px)';
15074 * This is how google docs handles clearing the div. Seems to work better than setting the text of the div to ''
15076 ariaNotifier[0].innerHTML = "";
15077 ariaNotifier[0].style.visibility = 'hidden';
15078 ariaNotifier[0].style.visibility = 'visible';
15080 ariaNotifier[0].style.clip = 'auto';
15082 * The space after the text is something that google docs does.
15084 ariaNotifier[0].appendChild(document.createTextNode(text + " "));
15085 ariaNotifier[0].style.visibility = 'hidden';
15086 ariaNotifier[0].style.visibility = 'visible';
15091 var currentSelection = grid.api.cellNav.getCurrentSelection();
15092 for (var i = 0; i < currentSelection.length; i++) {
15093 values.push(currentSelection[i].getIntersectionValueFiltered());
15095 var cellText = values.toString();
15096 setNotifyText(cellText);
15100 addAriaLiveRegion();
15107 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', '$compile','uiGridCellNavConstants',
15108 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, $compile, uiGridCellNavConstants) {
15111 priority: -99999, //this needs to run very last
15112 require: ['^uiGrid', 'uiGridRenderContainer', '?^uiGridCellnav'],
15114 compile: function () {
15116 post: function ($scope, $elm, $attrs, controllers) {
15117 var uiGridCtrl = controllers[0],
15118 renderContainerCtrl = controllers[1],
15119 uiGridCellnavCtrl = controllers[2];
15121 // Skip attaching cell-nav specific logic if the directive is not attached above us
15122 if (!uiGridCtrl.grid.api.cellNav) { return; }
15124 var containerId = renderContainerCtrl.containerId;
15126 var grid = uiGridCtrl.grid;
15128 //run each time a render container is created
15129 uiGridCellNavService.decorateRenderContainers(grid);
15131 // focusser only created for body
15132 if (containerId !== 'body') {
15138 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells){
15139 $elm.attr('aria-multiselectable', true);
15141 $elm.attr('aria-multiselectable', false);
15144 //add an element with no dimensions that can be used to set focus and capture keystrokes
15145 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);
15146 $elm.append(focuser);
15148 focuser.on('focus', function (evt) {
15149 evt.uiGridTargetRenderContainerId = containerId;
15150 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15151 if (rowCol === null) {
15152 rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(uiGridCellNavConstants.direction.DOWN, null, null);
15153 if (rowCol.row && rowCol.col) {
15154 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
15159 uiGridCellnavCtrl.setAriaActivedescendant = function(id){
15160 $elm.attr('aria-activedescendant', id);
15163 uiGridCellnavCtrl.removeAriaActivedescendant = function(id){
15164 if ($elm.attr('aria-activedescendant') === id){
15165 $elm.attr('aria-activedescendant', '');
15170 uiGridCtrl.focus = function () {
15171 gridUtil.focus.byElement(focuser[0]);
15172 //allow for first time grid focus
15175 var viewPortKeyDownWasRaisedForRowCol = null;
15176 // Bind to keydown events in the render container
15177 focuser.on('keydown', function (evt) {
15178 evt.uiGridTargetRenderContainerId = containerId;
15179 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15180 var result = uiGridCtrl.cellNav.handleKeyDown(evt);
15181 if (result === null) {
15182 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyDown(evt, rowCol);
15183 viewPortKeyDownWasRaisedForRowCol = rowCol;
15186 //Bind to keypress events in the render container
15187 //keypress events are needed by edit function so the key press
15188 //that initiated an edit is not lost
15189 //must fire the event in a timeout so the editor can
15190 //initialize and subscribe to the event on another event loop
15191 focuser.on('keypress', function (evt) {
15192 if (viewPortKeyDownWasRaisedForRowCol) {
15193 $timeout(function () {
15194 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyPress(evt, viewPortKeyDownWasRaisedForRowCol);
15197 viewPortKeyDownWasRaisedForRowCol = null;
15201 $scope.$on('$destroy', function(){
15202 //Remove all event handlers associated with this focuser.
15212 module.directive('uiGridViewport', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log','$compile',
15213 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log, $compile) {
15216 priority: -99999, //this needs to run very last
15217 require: ['^uiGrid', '^uiGridRenderContainer', '?^uiGridCellnav'],
15219 compile: function () {
15221 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
15223 post: function ($scope, $elm, $attrs, controllers) {
15224 var uiGridCtrl = controllers[0],
15225 renderContainerCtrl = controllers[1];
15227 // Skip attaching cell-nav specific logic if the directive is not attached above us
15228 if (!uiGridCtrl.grid.api.cellNav) { return; }
15230 var containerId = renderContainerCtrl.containerId;
15231 //no need to process for other containers
15232 if (containerId !== 'body') {
15236 var grid = uiGridCtrl.grid;
15238 grid.api.core.on.scrollBegin($scope, function (args) {
15240 // Skip if there's no currently-focused cell
15241 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15242 if (lastRowCol === null) {
15246 //if not in my container, move on
15247 //todo: worry about horiz scroll
15248 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
15252 uiGridCtrl.cellNav.clearFocus();
15256 grid.api.core.on.scrollEnd($scope, function (args) {
15257 // Skip if there's no currently-focused cell
15258 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
15259 if (lastRowCol === null) {
15263 //if not in my container, move on
15264 //todo: worry about horiz scroll
15265 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
15269 uiGridCtrl.cellNav.broadcastCellNav(lastRowCol);
15273 grid.api.cellNav.on.navigate($scope, function () {
15274 //focus again because it can be lost
15275 uiGridCtrl.focus();
15286 * @name ui.grid.cellNav.directive:uiGridCell
15289 * @description Stacks on top of ui.grid.uiGridCell to provide cell navigation
15291 module.directive('uiGridCell', ['$timeout', '$document', 'uiGridCellNavService', 'gridUtil', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn',
15292 function ($timeout, $document, uiGridCellNavService, gridUtil, uiGridCellNavConstants, uiGridConstants, GridRowColumn) {
15294 priority: -150, // run after default uiGridCell directive and ui.grid.edit uiGridCell
15296 require: ['^uiGrid', '?^uiGridCellnav'],
15298 link: function ($scope, $elm, $attrs, controllers) {
15299 var uiGridCtrl = controllers[0],
15300 uiGridCellnavCtrl = controllers[1];
15301 // Skip attaching cell-nav specific logic if the directive is not attached above us
15302 if (!uiGridCtrl.grid.api.cellNav) { return; }
15304 if (!$scope.col.colDef.allowCellFocus) {
15308 //Convinience local variables
15309 var grid = uiGridCtrl.grid;
15310 $scope.focused = false;
15312 // Make this cell focusable but only with javascript/a mouse click
15313 $elm.attr('tabindex', -1);
15315 // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused
15316 $elm.find('div').on('click', function (evt) {
15317 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), evt.ctrlKey || evt.metaKey, evt);
15319 evt.stopPropagation();
15325 * XXX Hack for screen readers.
15326 * This allows the grid to focus using only the screen reader cursor.
15327 * Since the focus event doesn't include key press information we can't use it
15328 * as our primary source of the event.
15330 $elm.on('mousedown', preventMouseDown);
15332 //turn on and off for edit events
15333 if (uiGridCtrl.grid.api.edit) {
15334 uiGridCtrl.grid.api.edit.on.beginCellEdit($scope, function () {
15335 $elm.off('mousedown', preventMouseDown);
15338 uiGridCtrl.grid.api.edit.on.afterCellEdit($scope, function () {
15339 $elm.on('mousedown', preventMouseDown);
15342 uiGridCtrl.grid.api.edit.on.cancelCellEdit($scope, function () {
15343 $elm.on('mousedown', preventMouseDown);
15347 function preventMouseDown(evt) {
15348 //Prevents the foucus event from firing if the click event is already going to fire.
15349 //If both events fire it will cause bouncing behavior.
15350 evt.preventDefault();
15353 //You can only focus on elements with a tabindex value
15354 $elm.on('focus', function (evt) {
15355 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), false, evt);
15356 evt.stopPropagation();
15360 // This event is fired for all cells. If the cell matches, then focus is set
15361 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown) {
15362 var isFocused = grid.cellNav.focusedCells.some(function(focusedRowCol, index){
15363 return (focusedRowCol.row === $scope.row && focusedRowCol.col === $scope.col);
15372 function setFocused() {
15373 if (!$scope.focused){
15374 var div = $elm.find('div');
15375 div.addClass('ui-grid-cell-focus');
15376 $elm.attr('aria-selected', true);
15377 uiGridCellnavCtrl.setAriaActivedescendant($elm.attr('id'));
15378 $scope.focused = true;
15382 function clearFocus() {
15383 if ($scope.focused){
15384 var div = $elm.find('div');
15385 div.removeClass('ui-grid-cell-focus');
15386 $elm.attr('aria-selected', false);
15387 uiGridCellnavCtrl.removeAriaActivedescendant($elm.attr('id'));
15388 $scope.focused = false;
15392 $scope.$on('$destroy', function () {
15393 //.off withouth paramaters removes all handlers
15394 $elm.find('div').off();
15408 * @name ui.grid.edit
15413 * <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>
15415 * This module provides cell editing capability to ui.grid. The goal was to emulate keying data in a spreadsheet via
15419 * To really get the full spreadsheet-like data entry, the ui.grid.cellNav module should be used. This will allow the
15420 * user to key data and then tab, arrow, or enter to the cells beside or below.
15422 * <div doc-module-components="ui.grid.edit"></div>
15425 var module = angular.module('ui.grid.edit', ['ui.grid']);
15429 * @name ui.grid.edit.constant:uiGridEditConstants
15431 * @description constants available in edit module
15433 module.constant('uiGridEditConstants', {
15434 EDITABLE_CELL_TEMPLATE: /EDITABLE_CELL_TEMPLATE/g,
15435 //must be lowercase because template bulder converts to lower
15436 EDITABLE_CELL_DIRECTIVE: /editable_cell_directive/g,
15438 BEGIN_CELL_EDIT: 'uiGridEventBeginCellEdit',
15439 END_CELL_EDIT: 'uiGridEventEndCellEdit',
15440 CANCEL_CELL_EDIT: 'uiGridEventCancelCellEdit'
15446 * @name ui.grid.edit.service:uiGridEditService
15448 * @description Services for editing features
15450 module.service('uiGridEditService', ['$q', 'uiGridConstants', 'gridUtil',
15451 function ($q, uiGridConstants, gridUtil) {
15455 initializeGrid: function (grid) {
15457 service.defaultGridOptions(grid.options);
15459 grid.registerColumnBuilder(service.editColumnBuilder);
15464 * @name ui.grid.edit.api:PublicApi
15466 * @description Public Api for edit feature
15473 * @name afterCellEdit
15474 * @eventOf ui.grid.edit.api:PublicApi
15475 * @description raised when cell editing is complete
15477 * gridApi.edit.on.afterCellEdit(scope,function(rowEntity, colDef){})
15479 * @param {object} rowEntity the options.data element that was edited
15480 * @param {object} colDef the column that was edited
15481 * @param {object} newValue new value
15482 * @param {object} oldValue old value
15484 afterCellEdit: function (rowEntity, colDef, newValue, oldValue) {
15488 * @name beginCellEdit
15489 * @eventOf ui.grid.edit.api:PublicApi
15490 * @description raised when cell editing starts on a cell
15492 * gridApi.edit.on.beginCellEdit(scope,function(rowEntity, colDef){})
15494 * @param {object} rowEntity the options.data element that was edited
15495 * @param {object} colDef the column that was edited
15496 * @param {object} triggerEvent the event that triggered the edit. Useful to prevent losing keystrokes on some
15499 beginCellEdit: function (rowEntity, colDef, triggerEvent) {
15503 * @name cancelCellEdit
15504 * @eventOf ui.grid.edit.api:PublicApi
15505 * @description raised when cell editing is cancelled on a cell
15507 * gridApi.edit.on.cancelCellEdit(scope,function(rowEntity, colDef){})
15509 * @param {object} rowEntity the options.data element that was edited
15510 * @param {object} colDef the column that was edited
15512 cancelCellEdit: function (rowEntity, colDef) {
15521 grid.api.registerEventsFromObject(publicApi.events);
15522 //grid.api.registerMethodsFromObject(publicApi.methods);
15526 defaultGridOptions: function (gridOptions) {
15530 * @name ui.grid.edit.api:GridOptions
15532 * @description Options for configuring the edit feature, these are available to be
15533 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
15538 * @name enableCellEdit
15539 * @propertyOf ui.grid.edit.api:GridOptions
15540 * @description If defined, sets the default value for the editable flag on each individual colDefs
15541 * if their individual enableCellEdit configuration is not defined. Defaults to undefined.
15546 * @name cellEditableCondition
15547 * @propertyOf ui.grid.edit.api:GridOptions
15548 * @description If specified, either a value or function to be used by all columns before editing.
15549 * If falsy, then editing of cell is not allowed.
15552 * function($scope){
15553 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15558 gridOptions.cellEditableCondition = gridOptions.cellEditableCondition === undefined ? true : gridOptions.cellEditableCondition;
15562 * @name editableCellTemplate
15563 * @propertyOf ui.grid.edit.api:GridOptions
15564 * @description If specified, cellTemplate to use as the editor for all columns.
15565 * <br/> defaults to 'ui-grid/cellTextEditor'
15570 * @name enableCellEditOnFocus
15571 * @propertyOf ui.grid.edit.api:GridOptions
15572 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15573 * <br/>_requires cellNav feature and the edit feature to be enabled_
15575 //enableCellEditOnFocus can only be used if cellnav module is used
15576 gridOptions.enableCellEditOnFocus = gridOptions.enableCellEditOnFocus === undefined ? false : gridOptions.enableCellEditOnFocus;
15581 * @name editColumnBuilder
15582 * @methodOf ui.grid.edit.service:uiGridEditService
15583 * @description columnBuilder function that adds edit properties to grid column
15584 * @returns {promise} promise that will load any needed templates when resolved
15586 editColumnBuilder: function (colDef, col, gridOptions) {
15592 * @name ui.grid.edit.api:ColumnDef
15594 * @description Column Definition for edit feature, these are available to be
15595 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
15600 * @name enableCellEdit
15601 * @propertyOf ui.grid.edit.api:ColumnDef
15602 * @description enable editing on column
15604 colDef.enableCellEdit = colDef.enableCellEdit === undefined ? (gridOptions.enableCellEdit === undefined ?
15605 (colDef.type !== 'object') : gridOptions.enableCellEdit) : colDef.enableCellEdit;
15609 * @name cellEditableCondition
15610 * @propertyOf ui.grid.edit.api:ColumnDef
15611 * @description If specified, either a value or function evaluated before editing cell. If falsy, then editing of cell is not allowed.
15614 * function($scope){
15615 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15620 colDef.cellEditableCondition = colDef.cellEditableCondition === undefined ? gridOptions.cellEditableCondition : colDef.cellEditableCondition;
15624 * @name editableCellTemplate
15625 * @propertyOf ui.grid.edit.api:ColumnDef
15626 * @description cell template to be used when editing this column. Can be Url or text template
15627 * <br/>Defaults to gridOptions.editableCellTemplate
15629 if (colDef.enableCellEdit) {
15630 colDef.editableCellTemplate = colDef.editableCellTemplate || gridOptions.editableCellTemplate || 'ui-grid/cellEditor';
15632 promises.push(gridUtil.getTemplate(colDef.editableCellTemplate)
15634 function (template) {
15635 col.editableCellTemplate = template;
15638 // Todo handle response error here?
15639 throw new Error("Couldn't fetch/use colDef.editableCellTemplate '" + colDef.editableCellTemplate + "'");
15645 * @name enableCellEditOnFocus
15646 * @propertyOf ui.grid.edit.api:ColumnDef
15647 * @requires ui.grid.cellNav
15648 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15649 * <br>_requires both the cellNav feature and the edit feature to be enabled_
15651 //enableCellEditOnFocus can only be used if cellnav module is used
15652 colDef.enableCellEditOnFocus = colDef.enableCellEditOnFocus === undefined ? gridOptions.enableCellEditOnFocus : colDef.enableCellEditOnFocus;
15657 * @name editModelField
15658 * @propertyOf ui.grid.edit.api:ColumnDef
15659 * @description a bindable string value that is used when binding to edit controls instead of colDef.field
15660 * <br/> example: You have a complex property on and object like state:{abbrev:'MS',name:'Mississippi'}. The
15661 * grid should display state.name in the cell and sort/filter based on the state.name property but the editor
15662 * requires the full state object.
15663 * <br/>colDef.field = 'state.name'
15664 * <br/>colDef.editModelField = 'state'
15666 //colDef.editModelField
15668 return $q.all(promises);
15673 * @name isStartEditKey
15674 * @methodOf ui.grid.edit.service:uiGridEditService
15675 * @description Determines if a keypress should start editing. Decorate this service to override with your
15676 * own key events. See service decorator in angular docs.
15677 * @param {Event} evt keydown event
15678 * @returns {boolean} true if an edit should start
15680 isStartEditKey: function (evt) {
15682 evt.keyCode === uiGridConstants.keymap.ESC ||
15683 evt.keyCode === uiGridConstants.keymap.SHIFT ||
15684 evt.keyCode === uiGridConstants.keymap.CTRL ||
15685 evt.keyCode === uiGridConstants.keymap.ALT ||
15686 evt.keyCode === uiGridConstants.keymap.WIN ||
15687 evt.keyCode === uiGridConstants.keymap.CAPSLOCK ||
15689 evt.keyCode === uiGridConstants.keymap.LEFT ||
15690 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey) ||
15692 evt.keyCode === uiGridConstants.keymap.RIGHT ||
15693 evt.keyCode === uiGridConstants.keymap.TAB ||
15695 evt.keyCode === uiGridConstants.keymap.UP ||
15696 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ||
15698 evt.keyCode === uiGridConstants.keymap.DOWN ||
15699 evt.keyCode === uiGridConstants.keymap.ENTER) {
15715 * @name ui.grid.edit.directive:uiGridEdit
15719 * @description Adds editing features to the ui-grid directive.
15722 <example module="app">
15723 <file name="app.js">
15724 var app = angular.module('app', ['ui.grid', 'ui.grid.edit']);
15726 app.controller('MainCtrl', ['$scope', function ($scope) {
15728 { name: 'Bob', title: 'CEO' },
15729 { name: 'Frank', title: 'Lowly Developer' }
15732 $scope.columnDefs = [
15733 {name: 'name', enableCellEdit: true},
15734 {name: 'title', enableCellEdit: true}
15738 <file name="index.html">
15739 <div ng-controller="MainCtrl">
15740 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit></div>
15745 module.directive('uiGridEdit', ['gridUtil', 'uiGridEditService', function (gridUtil, uiGridEditService) {
15749 require: '^uiGrid',
15751 compile: function () {
15753 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
15754 uiGridEditService.initializeGrid(uiGridCtrl.grid);
15756 post: function ($scope, $elm, $attrs, uiGridCtrl) {
15765 * @name ui.grid.edit.directive:uiGridRenderContainer
15769 * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits
15772 module.directive('uiGridViewport', [ 'uiGridEditConstants',
15773 function ( uiGridEditConstants) {
15776 priority: -99998, //run before cellNav
15777 require: ['^uiGrid', '^uiGridRenderContainer'],
15779 compile: function () {
15781 post: function ($scope, $elm, $attrs, controllers) {
15782 var uiGridCtrl = controllers[0];
15784 // Skip attaching if edit and cellNav is not enabled
15785 if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; }
15787 var containerId = controllers[1].containerId;
15788 //no need to process for other containers
15789 if (containerId !== 'body') {
15793 //refocus on the grid
15794 $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
15795 uiGridCtrl.focus();
15797 $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
15798 uiGridCtrl.focus();
15809 * @name ui.grid.edit.directive:uiGridCell
15813 * @description Stacks on top of ui.grid.uiGridCell to provide in-line editing capabilities to the cell
15816 * Binds edit start events to the uiGridCell element. When the events fire, the gridCell element is appended
15817 * with the columnDef.editableCellTemplate element ('cellEditor.html' by default).
15819 * The editableCellTemplate should respond to uiGridEditConstants.events.BEGIN\_CELL\_EDIT angular event
15820 * and do the initial steps needed to edit the cell (setfocus on input element, etc).
15822 * When the editableCellTemplate recognizes that the editing is ended (blur event, Enter key, etc.)
15823 * it should emit the uiGridEditConstants.events.END\_CELL\_EDIT event.
15825 * If editableCellTemplate recognizes that the editing has been cancelled (esc key)
15826 * it should emit the uiGridEditConstants.events.CANCEL\_CELL\_EDIT event. The original value
15827 * will be set back on the model by the uiGridCell directive.
15829 * Events that invoke editing:
15831 * - F2 keydown (when using cell selection)
15833 * Events that end editing:
15834 * - Dependent on the specific editableCellTemplate
15835 * - Standards should be blur and enter keydown
15837 * Events that cancel editing:
15838 * - Dependent on the specific editableCellTemplate
15839 * - Standards should be Esc keydown
15841 * Grid Events that end editing:
15842 * - uiGridConstants.events.GRID_SCROLL
15848 * @name ui.grid.edit.api:GridRow
15850 * @description GridRow options for edit feature, these are available to be
15851 * set internally only, by other features
15856 * @name enableCellEdit
15857 * @propertyOf ui.grid.edit.api:GridRow
15858 * @description enable editing on row, grouping for example might disable editing on group header rows
15861 module.directive('uiGridCell',
15862 ['$compile', '$injector', '$timeout', 'uiGridConstants', 'uiGridEditConstants', 'gridUtil', '$parse', 'uiGridEditService', '$rootScope', '$q',
15863 function ($compile, $injector, $timeout, uiGridConstants, uiGridEditConstants, gridUtil, $parse, uiGridEditService, $rootScope, $q) {
15864 var touchstartTimeout = 500;
15865 if ($injector.has('uiGridCellNavService')) {
15866 var uiGridCellNavService = $injector.get('uiGridCellNavService');
15870 priority: -100, // run after default uiGridCell directive
15873 require: '?^uiGrid',
15874 link: function ($scope, $elm, $attrs, uiGridCtrl) {
15877 var inEdit = false;
15879 var cancelTouchstartTimeout;
15883 if (!$scope.col.colDef.enableCellEdit) {
15887 var cellNavNavigateDereg = function() {};
15888 var viewPortKeyDownDereg = function() {};
15891 var setEditable = function() {
15892 if ($scope.col.colDef.enableCellEdit && $scope.row.enableCellEdit !== false) {
15893 if (!$scope.beginEditEventsWired) { //prevent multiple attachments
15894 registerBeginEditEvents();
15897 if ($scope.beginEditEventsWired) {
15898 cancelBeginEditEvents();
15905 var rowWatchDereg = $scope.$watch('row', function (n, o) {
15912 $scope.$on( '$destroy', rowWatchDereg );
15914 function registerBeginEditEvents() {
15915 $elm.on('dblclick', beginEdit);
15917 // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit
15918 $elm.on('touchstart', touchStart);
15920 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15922 viewPortKeyDownDereg = uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
15923 if (rowCol === null) {
15927 if (rowCol.row === $scope.row && rowCol.col === $scope.col && !$scope.col.colDef.enableCellEditOnFocus) {
15928 //important to do this before scrollToIfNecessary
15929 beginEditKeyDown(evt);
15933 cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) {
15934 if ($scope.col.colDef.enableCellEditOnFocus) {
15935 // Don't begin edit if the cell hasn't changed
15936 if ((!oldRowCol || newRowCol.row !== oldRowCol.row || newRowCol.col !== oldRowCol.col) &&
15937 newRowCol.row === $scope.row && newRowCol.col === $scope.col) {
15938 $timeout(function () {
15946 $scope.beginEditEventsWired = true;
15950 function touchStart(event) {
15951 // jQuery masks events
15952 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
15953 event = event.originalEvent;
15956 // Bind touchend handler
15957 $elm.on('touchend', touchEnd);
15960 cancelTouchstartTimeout = $timeout(function() { }, touchstartTimeout);
15962 // Timeout's done! Start the edit
15963 cancelTouchstartTimeout.then(function () {
15964 // Use setTimeout to start the edit because beginEdit expects to be outside of $digest
15965 setTimeout(beginEdit, 0);
15967 // Undbind the touchend handler, we don't need it anymore
15968 $elm.off('touchend', touchEnd);
15972 // Cancel any touchstart timeout
15973 function touchEnd(event) {
15974 $timeout.cancel(cancelTouchstartTimeout);
15975 $elm.off('touchend', touchEnd);
15978 function cancelBeginEditEvents() {
15979 $elm.off('dblclick', beginEdit);
15980 $elm.off('keydown', beginEditKeyDown);
15981 $elm.off('touchstart', touchStart);
15982 cellNavNavigateDereg();
15983 viewPortKeyDownDereg();
15984 $scope.beginEditEventsWired = false;
15987 function beginEditKeyDown(evt) {
15988 if (uiGridEditService.isStartEditKey(evt)) {
15993 function shouldEdit(col, row) {
15994 return !row.isSaving &&
15995 ( angular.isFunction(col.colDef.cellEditableCondition) ?
15996 col.colDef.cellEditableCondition($scope) :
15997 col.colDef.cellEditableCondition );
16001 function beginEdit(triggerEvent) {
16002 //we need to scroll the cell into focus before invoking the editor
16003 $scope.grid.api.core.scrollToIfNecessary($scope.row, $scope.col)
16004 .then(function () {
16005 beginEditAfterScroll(triggerEvent);
16011 * @name editDropdownOptionsArray
16012 * @propertyOf ui.grid.edit.api:ColumnDef
16013 * @description an array of values in the format
16014 * [ {id: xxx, value: xxx} ], which is populated
16015 * into the edit dropdown
16020 * @name editDropdownIdLabel
16021 * @propertyOf ui.grid.edit.api:ColumnDef
16022 * @description the label for the "id" field
16023 * in the editDropdownOptionsArray. Defaults
16027 * $scope.gridOptions = {
16029 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16030 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
16031 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16038 * @name editDropdownRowEntityOptionsArrayPath
16039 * @propertyOf ui.grid.edit.api:ColumnDef
16040 * @description a path to a property on row.entity containing an
16041 * array of values in the format
16042 * [ {id: xxx, value: xxx} ], which will be used to populate
16043 * the edit dropdown. This can be used when the dropdown values are dependent on
16044 * the backing row entity.
16045 * If this property is set then editDropdownOptionsArray will be ignored.
16048 * $scope.gridOptions = {
16050 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16051 * editDropdownRowEntityOptionsArrayPath: 'foo.bars[0].baz',
16052 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16059 * @name editDropdownOptionsFunction
16060 * @methodOf ui.grid.edit.api:ColumnDef
16061 * @description a function returning an array of values in the format
16062 * [ {id: xxx, value: xxx} ], which will be used to populate
16063 * the edit dropdown. This can be used when the dropdown values are dependent on
16064 * the backing row entity with some kind of algorithm.
16065 * If this property is set then both editDropdownOptionsArray and
16066 * editDropdownRowEntityOptionsArrayPath will be ignored.
16067 * @param {object} rowEntity the options.data element that the returned array refers to
16068 * @param {object} colDef the column that implements this dropdown
16069 * @returns {object} an array of values in the format
16070 * [ {id: xxx, value: xxx} ] used to populate the edit dropdown
16073 * $scope.gridOptions = {
16075 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16076 * editDropdownOptionsFunction: function(rowEntity, colDef) {
16077 * if (rowEntity.foo === 'bar') {
16078 * return [{id: 'bar1', value: 'BAR 1'},
16079 * {id: 'bar2', value: 'BAR 2'},
16080 * {id: 'bar3', value: 'BAR 3'}];
16082 * return [{id: 'foo1', value: 'FOO 1'},
16083 * {id: 'foo2', value: 'FOO 2'}];
16086 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16093 * @name editDropdownValueLabel
16094 * @propertyOf ui.grid.edit.api:ColumnDef
16095 * @description the label for the "value" field
16096 * in the editDropdownOptionsArray. Defaults
16100 * $scope.gridOptions = {
16102 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16103 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
16104 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
16111 * @name editDropdownFilter
16112 * @propertyOf ui.grid.edit.api:ColumnDef
16113 * @description A filter that you would like to apply to the values in the options list
16114 * of the dropdown. For example if you were using angular-translate you might set this
16118 * $scope.gridOptions = {
16120 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
16121 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
16122 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status', editDropdownFilter: 'translate' }
16127 function beginEditAfterScroll(triggerEvent) {
16128 // If we are already editing, then just skip this so we don't try editing twice...
16133 if (!shouldEdit($scope.col, $scope.row)) {
16138 cellModel = $parse($scope.row.getQualifiedColField($scope.col));
16139 //get original value from the cell
16140 origCellValue = cellModel($scope);
16142 html = $scope.col.editableCellTemplate;
16144 if ($scope.col.colDef.editModelField) {
16145 html = html.replace(uiGridConstants.MODEL_COL_FIELD, gridUtil.preEval('row.entity.' + $scope.col.colDef.editModelField));
16148 html = html.replace(uiGridConstants.MODEL_COL_FIELD, $scope.row.getQualifiedColField($scope.col));
16151 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
16153 var optionFilter = $scope.col.colDef.editDropdownFilter ? '|' + $scope.col.colDef.editDropdownFilter : '';
16154 html = html.replace(uiGridConstants.CUSTOM_FILTERS, optionFilter);
16156 var inputType = 'text';
16157 switch ($scope.col.colDef.type){
16159 inputType = 'checkbox';
16162 inputType = 'number';
16165 inputType = 'date';
16168 html = html.replace('INPUT_TYPE', inputType);
16170 // In order to fill dropdown options we use:
16171 // - A function/promise or
16172 // - An array inside of row entity if no function exists or
16173 // - A single array for the whole column if none of the previous exists.
16174 var editDropdownOptionsFunction = $scope.col.colDef.editDropdownOptionsFunction;
16175 if (editDropdownOptionsFunction) {
16176 $q.when(editDropdownOptionsFunction($scope.row.entity, $scope.col.colDef))
16177 .then(function(result) {
16178 $scope.editDropdownOptionsArray = result;
16181 var editDropdownRowEntityOptionsArrayPath = $scope.col.colDef.editDropdownRowEntityOptionsArrayPath;
16182 if (editDropdownRowEntityOptionsArrayPath) {
16183 $scope.editDropdownOptionsArray = resolveObjectFromPath($scope.row.entity, editDropdownRowEntityOptionsArrayPath);
16186 $scope.editDropdownOptionsArray = $scope.col.colDef.editDropdownOptionsArray;
16189 $scope.editDropdownIdLabel = $scope.col.colDef.editDropdownIdLabel ? $scope.col.colDef.editDropdownIdLabel : 'id';
16190 $scope.editDropdownValueLabel = $scope.col.colDef.editDropdownValueLabel ? $scope.col.colDef.editDropdownValueLabel : 'value';
16193 var createEditor = function(){
16195 cancelBeginEditEvents();
16196 var cellElement = angular.element(html);
16197 $elm.append(cellElement);
16198 editCellScope = $scope.$new();
16199 $compile(cellElement)(editCellScope);
16200 var gridCellContentsEl = angular.element($elm.children()[0]);
16201 gridCellContentsEl.addClass('ui-grid-cell-contents-hidden');
16203 if (!$rootScope.$$phase) {
16204 $scope.$apply(createEditor);
16209 //stop editing when grid is scrolled
16210 var deregOnGridScroll = $scope.col.grid.api.core.on.scrollBegin($scope, function () {
16211 if ($scope.grid.disableScrolling) {
16215 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
16216 deregOnGridScroll();
16217 deregOnEndCellEdit();
16218 deregOnCancelCellEdit();
16222 var deregOnEndCellEdit = $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
16224 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
16225 deregOnEndCellEdit();
16226 deregOnGridScroll();
16227 deregOnCancelCellEdit();
16231 var deregOnCancelCellEdit = $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
16233 deregOnCancelCellEdit();
16234 deregOnGridScroll();
16235 deregOnEndCellEdit();
16238 $scope.$broadcast(uiGridEditConstants.events.BEGIN_CELL_EDIT, triggerEvent);
16239 $timeout(function () {
16240 //execute in a timeout to give any complex editor templates a cycle to completely render
16241 $scope.grid.api.edit.raise.beginCellEdit($scope.row.entity, $scope.col.colDef, triggerEvent);
16245 function endEdit() {
16246 $scope.grid.disableScrolling = false;
16251 //sometimes the events can't keep up with the keyboard and grid focus is lost, so always focus
16252 //back to grid here. The focus call needs to be before the $destroy and removal of the control,
16253 //otherwise ng-model-options of UpdateOn: 'blur' will not work.
16254 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16255 uiGridCtrl.focus();
16258 var gridCellContentsEl = angular.element($elm.children()[0]);
16259 //remove edit element
16260 editCellScope.$destroy();
16261 angular.element($elm.children()[1]).remove();
16262 gridCellContentsEl.removeClass('ui-grid-cell-contents-hidden');
16264 registerBeginEditEvents();
16265 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.EDIT );
16268 function cancelEdit() {
16269 $scope.grid.disableScrolling = false;
16273 cellModel.assign($scope, origCellValue);
16276 $scope.grid.api.edit.raise.cancelCellEdit($scope.row.entity, $scope.col.colDef);
16280 // resolves a string path against the given object
16281 // shamelessly borrowed from
16282 // http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
16283 function resolveObjectFromPath(object, path) {
16284 path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
16285 path = path.replace(/^\./, ''); // strip a leading dot
16286 var a = path.split('.');
16290 object = object[n];
16304 * @name ui.grid.edit.directive:uiGridEditor
16308 * @description input editor directive for editable fields.
16309 * Provides EndEdit and CancelEdit events
16311 * Events that end editing:
16312 * blur and enter keydown
16314 * Events that cancel editing:
16318 module.directive('uiGridEditor',
16319 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', 'uiGridEditService',
16320 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout, uiGridEditService) {
16323 require: ['?^uiGrid', '?^uiGridRenderContainer', 'ngModel'],
16324 compile: function () {
16326 pre: function ($scope, $elm, $attrs) {
16329 post: function ($scope, $elm, $attrs, controllers) {
16330 var uiGridCtrl, renderContainerCtrl, ngModel;
16331 if (controllers[0]) { uiGridCtrl = controllers[0]; }
16332 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
16333 if (controllers[2]) { ngModel = controllers[2]; }
16335 //set focus at start of edit
16336 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function (evt,triggerEvent) {
16337 $timeout(function () {
16339 //only select text if it is not being replaced below in the cellNav viewPortKeyPress
16340 if ($scope.col.colDef.enableCellEditOnFocus || !(uiGridCtrl && uiGridCtrl.grid.api.cellNav)) {
16344 //some browsers (Chrome) stupidly, imo, support the w3 standard that number, email, ...
16345 //fields should not allow setSelectionRange. We ignore the error for those browsers
16346 //https://www.w3.org/Bugs/Public/show_bug.cgi?id=24796
16348 $elm[0].setSelectionRange($elm[0].value.length, $elm[0].value.length);
16356 //set the keystroke that started the edit event
16357 //we must do this because the BeginEdit is done in a different event loop than the intitial
16359 //fire this event for the keypress that is received
16360 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16361 var viewPortKeyDownUnregister = uiGridCtrl.grid.api.cellNav.on.viewPortKeyPress($scope, function (evt, rowCol) {
16362 if (uiGridEditService.isStartEditKey(evt)) {
16363 ngModel.$setViewValue(String.fromCharCode( typeof evt.which === 'number' ? evt.which : evt.keyCode), evt);
16366 viewPortKeyDownUnregister();
16370 $elm.on('blur', function (evt) {
16371 $scope.stopEdit(evt);
16376 $scope.deepEdit = false;
16378 $scope.stopEdit = function (evt) {
16379 if ($scope.inputForm && !$scope.inputForm.$valid) {
16380 evt.stopPropagation();
16381 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16384 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16386 $scope.deepEdit = false;
16390 $elm.on('click', function (evt) {
16391 if ($elm[0].type !== 'checkbox') {
16392 $scope.deepEdit = true;
16393 $timeout(function () {
16394 $scope.grid.disableScrolling = true;
16399 $elm.on('keydown', function (evt) {
16400 switch (evt.keyCode) {
16401 case uiGridConstants.keymap.ESC:
16402 evt.stopPropagation();
16403 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16407 if ($scope.deepEdit &&
16408 (evt.keyCode === uiGridConstants.keymap.LEFT ||
16409 evt.keyCode === uiGridConstants.keymap.RIGHT ||
16410 evt.keyCode === uiGridConstants.keymap.UP ||
16411 evt.keyCode === uiGridConstants.keymap.DOWN)) {
16412 evt.stopPropagation();
16414 // Pass the keydown event off to the cellNav service, if it exists
16415 else if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16416 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
16417 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
16418 $scope.stopEdit(evt);
16422 //handle enter and tab for editing not using cellNav
16423 switch (evt.keyCode) {
16424 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
16425 case uiGridConstants.keymap.TAB:
16426 evt.stopPropagation();
16427 evt.preventDefault();
16428 $scope.stopEdit(evt);
16443 * @name ui.grid.edit.directive:input
16447 * @description directive to provide binding between input[date] value and ng-model for angular 1.2
16448 * It is similar to input[date] directive of angular 1.3
16450 * Supported date format for input is 'yyyy-MM-dd'
16451 * The directive will set the $valid property of input element and the enclosing form to false if
16452 * model is invalid date or value of input is entered wrong.
16455 module.directive('uiGridEditor', ['$filter', function ($filter) {
16456 function parseDateString(dateString) {
16457 if (typeof(dateString) === 'undefined' || dateString === '') {
16460 var parts = dateString.split('-');
16461 if (parts.length !== 3) {
16464 var year = parseInt(parts[0], 10);
16465 var month = parseInt(parts[1], 10);
16466 var day = parseInt(parts[2], 10);
16468 if (month < 1 || year < 1 || day < 1) {
16471 return new Date(year, (month - 1), day);
16474 priority: -100, // run after default uiGridEditor directive
16475 require: '?ngModel',
16476 link: function (scope, element, attrs, ngModel) {
16478 if (angular.version.minor === 2 && attrs.type && attrs.type === 'date' && ngModel) {
16480 ngModel.$formatters.push(function (modelValue) {
16481 ngModel.$setValidity(null,(!modelValue || !isNaN(modelValue.getTime())));
16482 return $filter('date')(modelValue, 'yyyy-MM-dd');
16485 ngModel.$parsers.push(function (viewValue) {
16486 if (viewValue && viewValue.length > 0) {
16487 var dateValue = parseDateString(viewValue);
16488 ngModel.$setValidity(null, (dateValue && !isNaN(dateValue.getTime())));
16492 ngModel.$setValidity(null, true);
16504 * @name ui.grid.edit.directive:uiGridEditDropdown
16508 * @description dropdown editor for editable fields.
16509 * Provides EndEdit and CancelEdit events
16511 * Events that end editing:
16512 * blur and enter keydown, and any left/right nav
16514 * Events that cancel editing:
16518 module.directive('uiGridEditDropdown',
16519 ['uiGridConstants', 'uiGridEditConstants',
16520 function (uiGridConstants, uiGridEditConstants) {
16522 require: ['?^uiGrid', '?^uiGridRenderContainer'],
16524 compile: function () {
16526 pre: function ($scope, $elm, $attrs) {
16529 post: function ($scope, $elm, $attrs, controllers) {
16530 var uiGridCtrl = controllers[0];
16531 var renderContainerCtrl = controllers[1];
16533 //set focus at start of edit
16534 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16536 $elm[0].style.width = ($elm[0].parentElement.offsetWidth - 1) + 'px';
16537 $elm.on('blur', function (evt) {
16538 $scope.stopEdit(evt);
16543 $scope.stopEdit = function (evt) {
16544 // no need to validate a dropdown - invalid values shouldn't be
16545 // available in the list
16546 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16549 $elm.on('keydown', function (evt) {
16550 switch (evt.keyCode) {
16551 case uiGridConstants.keymap.ESC:
16552 evt.stopPropagation();
16553 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16556 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16557 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
16558 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
16559 $scope.stopEdit(evt);
16563 //handle enter and tab for editing not using cellNav
16564 switch (evt.keyCode) {
16565 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
16566 case uiGridConstants.keymap.TAB:
16567 evt.stopPropagation();
16568 evt.preventDefault();
16569 $scope.stopEdit(evt);
16583 * @name ui.grid.edit.directive:uiGridEditFileChooser
16587 * @description input editor directive for editable fields.
16588 * Provides EndEdit and CancelEdit events
16590 * Events that end editing:
16591 * blur and enter keydown
16593 * Events that cancel editing:
16597 module.directive('uiGridEditFileChooser',
16598 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout',
16599 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) {
16602 require: ['?^uiGrid', '?^uiGridRenderContainer'],
16603 compile: function () {
16605 pre: function ($scope, $elm, $attrs) {
16608 post: function ($scope, $elm, $attrs, controllers) {
16609 var uiGridCtrl, renderContainerCtrl;
16610 if (controllers[0]) { uiGridCtrl = controllers[0]; }
16611 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
16612 var grid = uiGridCtrl.grid;
16614 var handleFileSelect = function( event ){
16615 var target = event.srcElement || event.target;
16617 if (target && target.files && target.files.length > 0) {
16620 * @name editFileChooserCallback
16621 * @propertyOf ui.grid.edit.api:ColumnDef
16622 * @description A function that should be called when any files have been chosen
16623 * by the user. You should use this to process the files appropriately for your
16626 * It passes the gridCol, the gridRow (from which you can get gridRow.entity),
16627 * and the files. The files are in the format as returned from the file chooser,
16628 * an array of files, with each having useful information such as:
16629 * - `files[0].lastModifiedDate`
16630 * - `files[0].name`
16631 * - `files[0].size` (appears to be in bytes)
16632 * - `files[0].type` (MIME type by the looks)
16634 * Typically you would do something with these files - most commonly you would
16635 * use the filename or read the file itself in. The example function does both.
16639 * editFileChooserCallBack: function(gridRow, gridCol, files ){
16640 * // ignore all but the first file, it can only choose one anyway
16641 * // set the filename into this column
16642 * gridRow.entity.filename = file[0].name;
16644 * // read the file and set it into a hidden column, which we may do stuff with later
16645 * var setFile = function(fileContent){
16646 * gridRow.entity.file = fileContent.currentTarget.result;
16648 * var reader = new FileReader();
16649 * reader.onload = setFile;
16650 * reader.readAsText( files[0] );
16654 if ( typeof($scope.col.colDef.editFileChooserCallback) === 'function' ) {
16655 $scope.col.colDef.editFileChooserCallback($scope.row, $scope.col, target.files);
16657 gridUtil.logError('You need to set colDef.editFileChooserCallback to use the file chooser');
16660 target.form.reset();
16661 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16663 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16667 $elm[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
16669 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16673 $elm.on('blur', function (evt) {
16674 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16691 * @name ui.grid.expandable
16694 * # ui.grid.expandable
16696 * <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>
16698 * This module provides the ability to create subgrids with the ability to expand a row
16699 * to show the subgrid.
16701 * <div doc-module-components="ui.grid.expandable"></div>
16703 var module = angular.module('ui.grid.expandable', ['ui.grid']);
16707 * @name ui.grid.expandable.service:uiGridExpandableService
16709 * @description Services for the expandable grid
16711 module.service('uiGridExpandableService', ['gridUtil', '$compile', function (gridUtil, $compile) {
16713 initializeGrid: function (grid) {
16715 grid.expandable = {};
16716 grid.expandable.expandedAll = false;
16720 * @name enableExpandable
16721 * @propertyOf ui.grid.expandable.api:GridOptions
16722 * @description Whether or not to use expandable feature, allows you to turn off expandable on specific grids
16723 * within your application, or in specific modes on _this_ grid. Defaults to true.
16726 * $scope.gridOptions = {
16727 * enableExpandable: false
16731 grid.options.enableExpandable = grid.options.enableExpandable !== false;
16735 * @name expandableRowHeight
16736 * @propertyOf ui.grid.expandable.api:GridOptions
16737 * @description Height in pixels of the expanded subgrid. Defaults to
16741 * $scope.gridOptions = {
16742 * expandableRowHeight: 150
16746 grid.options.expandableRowHeight = grid.options.expandableRowHeight || 150;
16751 * @propertyOf ui.grid.expandable.api:GridOptions
16752 * @description Width in pixels of the expandable column. Defaults to 40
16755 * $scope.gridOptions = {
16756 * expandableRowHeaderWidth: 40
16760 grid.options.expandableRowHeaderWidth = grid.options.expandableRowHeaderWidth || 40;
16764 * @name expandableRowTemplate
16765 * @propertyOf ui.grid.expandable.api:GridOptions
16766 * @description Mandatory. The template for your expanded row
16769 * $scope.gridOptions = {
16770 * expandableRowTemplate: 'expandableRowTemplate.html'
16774 if ( grid.options.enableExpandable && !grid.options.expandableRowTemplate ){
16775 gridUtil.logError( 'You have not set the expandableRowTemplate, disabling expandable module' );
16776 grid.options.enableExpandable = false;
16781 * @name ui.grid.expandable.api:PublicApi
16783 * @description Public Api for expandable feature
16787 * @name ui.grid.expandable.api:GridRow
16789 * @description Additional properties added to GridRow when using the expandable module
16793 * @name ui.grid.expandable.api:GridOptions
16795 * @description Options for configuring the expandable feature, these are available to be
16796 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
16803 * @name rowExpandedStateChanged
16804 * @eventOf ui.grid.expandable.api:PublicApi
16805 * @description raised when cell editing is complete
16807 * gridApi.expandable.on.rowExpandedStateChanged(scope,function(row){})
16809 * @param {GridRow} row the row that was expanded
16811 rowExpandedBeforeStateChanged: function(scope,row){
16813 rowExpandedStateChanged: function (scope, row) {
16822 * @name toggleRowExpansion
16823 * @methodOf ui.grid.expandable.api:PublicApi
16824 * @description Toggle a specific row
16826 * gridApi.expandable.toggleRowExpansion(rowEntity);
16828 * @param {object} rowEntity the data entity for the row you want to expand
16830 toggleRowExpansion: function (rowEntity) {
16831 var row = grid.getRow(rowEntity);
16832 if (row !== null) {
16833 service.toggleRowExpansion(grid, row);
16839 * @name expandAllRows
16840 * @methodOf ui.grid.expandable.api:PublicApi
16841 * @description Expand all subgrids.
16843 * gridApi.expandable.expandAllRows();
16846 expandAllRows: function() {
16847 service.expandAllRows(grid);
16852 * @name collapseAllRows
16853 * @methodOf ui.grid.expandable.api:PublicApi
16854 * @description Collapse all subgrids.
16856 * gridApi.expandable.collapseAllRows();
16859 collapseAllRows: function() {
16860 service.collapseAllRows(grid);
16865 * @name toggleAllRows
16866 * @methodOf ui.grid.expandable.api:PublicApi
16867 * @description Toggle all subgrids.
16869 * gridApi.expandable.toggleAllRows();
16872 toggleAllRows: function() {
16873 service.toggleAllRows(grid);
16878 grid.api.registerEventsFromObject(publicApi.events);
16879 grid.api.registerMethodsFromObject(publicApi.methods);
16882 toggleRowExpansion: function (grid, row) {
16883 // trigger the "before change" event. Can change row height dynamically this way.
16884 grid.api.expandable.raise.rowExpandedBeforeStateChanged(row);
16888 * @propertyOf ui.grid.expandable.api:GridRow
16889 * @description Whether or not the row is currently expanded.
16892 * $scope.api.expandable.on.rowExpandedStateChanged($scope, function (row) {
16893 * if (row.isExpanded) {
16899 row.isExpanded = !row.isExpanded;
16900 if (angular.isUndefined(row.expandedRowHeight)){
16901 row.expandedRowHeight = grid.options.expandableRowHeight;
16904 if (row.isExpanded) {
16905 row.height = row.grid.options.rowHeight + row.expandedRowHeight;
16908 row.height = row.grid.options.rowHeight;
16909 grid.expandable.expandedAll = false;
16911 grid.api.expandable.raise.rowExpandedStateChanged(row);
16914 expandAllRows: function(grid, $scope) {
16915 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16916 if (!row.isExpanded) {
16917 service.toggleRowExpansion(grid, row);
16920 grid.expandable.expandedAll = true;
16921 grid.queueGridRefresh();
16924 collapseAllRows: function(grid) {
16925 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16926 if (row.isExpanded) {
16927 service.toggleRowExpansion(grid, row);
16930 grid.expandable.expandedAll = false;
16931 grid.queueGridRefresh();
16934 toggleAllRows: function(grid) {
16935 if (grid.expandable.expandedAll) {
16936 service.collapseAllRows(grid);
16939 service.expandAllRows(grid);
16948 * @name enableExpandableRowHeader
16949 * @propertyOf ui.grid.expandable.api:GridOptions
16950 * @description Show a rowHeader to provide the expandable buttons. If set to false then implies
16951 * you're going to use a custom method for expanding and collapsing the subgrids. Defaults to true.
16954 * $scope.gridOptions = {
16955 * enableExpandableRowHeader: false
16959 module.directive('uiGridExpandable', ['uiGridExpandableService', '$templateCache',
16960 function (uiGridExpandableService, $templateCache) {
16964 require: '^uiGrid',
16966 compile: function () {
16968 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16969 if ( uiGridCtrl.grid.options.enableExpandableRowHeader !== false ) {
16970 var expandableRowHeaderColDef = {
16971 name: 'expandableButtons',
16973 exporterSuppressExport: true,
16974 enableColumnResizing: false,
16975 enableColumnMenu: false,
16976 width: uiGridCtrl.grid.options.expandableRowHeaderWidth || 40
16978 expandableRowHeaderColDef.cellTemplate = $templateCache.get('ui-grid/expandableRowHeader');
16979 expandableRowHeaderColDef.headerCellTemplate = $templateCache.get('ui-grid/expandableTopRowHeader');
16980 uiGridCtrl.grid.addRowHeaderColumn(expandableRowHeaderColDef);
16982 uiGridExpandableService.initializeGrid(uiGridCtrl.grid);
16984 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16993 * @name ui.grid.expandable.directive:uiGrid
16994 * @description stacks on the uiGrid directive to register child grid with parent row when child is created
16996 module.directive('uiGrid', ['uiGridExpandableService', '$templateCache',
16997 function (uiGridExpandableService, $templateCache) {
17001 require: '^uiGrid',
17003 compile: function () {
17005 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
17007 uiGridCtrl.grid.api.core.on.renderingComplete($scope, function() {
17008 //if a parent grid row is on the scope, then add the parentRow property to this childGrid
17009 if ($scope.row && $scope.row.grid && $scope.row.grid.options && $scope.row.grid.options.enableExpandable) {
17013 * @name ui.grid.expandable.class:Grid
17014 * @description Additional Grid properties added by expandable module
17020 * @propertyOf ui.grid.expandable.class:Grid
17021 * @description reference to the expanded parent row that owns this grid
17023 uiGridCtrl.grid.parentRow = $scope.row;
17025 //todo: adjust height on parent row when child grid height changes. we need some sort of gridHeightChanged event
17026 // uiGridCtrl.grid.core.on.canvasHeightChanged($scope, function(oldHeight, newHeight) {
17027 // uiGridCtrl.grid.parentRow = newHeight;
17033 post: function ($scope, $elm, $attrs, uiGridCtrl) {
17043 * @name ui.grid.expandable.directive:uiGridExpandableRow
17044 * @description directive to render the expandable row template
17046 module.directive('uiGridExpandableRow',
17047 ['uiGridExpandableService', '$timeout', '$compile', 'uiGridConstants','gridUtil','$interval', '$log',
17048 function (uiGridExpandableService, $timeout, $compile, uiGridConstants, gridUtil, $interval, $log) {
17055 compile: function () {
17057 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
17058 gridUtil.getTemplate($scope.grid.options.expandableRowTemplate).then(
17059 function (template) {
17060 if ($scope.grid.options.expandableRowScope) {
17061 var expandableRowScope = $scope.grid.options.expandableRowScope;
17062 for (var property in expandableRowScope) {
17063 if (expandableRowScope.hasOwnProperty(property)) {
17064 $scope[property] = expandableRowScope[property];
17068 var expandedRowElement = $compile(template)($scope);
17069 $elm.append(expandedRowElement);
17070 $scope.row.expandedRendered = true;
17074 post: function ($scope, $elm, $attrs, uiGridCtrl) {
17075 $scope.$on('$destroy', function() {
17076 $scope.row.expandedRendered = false;
17086 * @name ui.grid.expandable.directive:uiGridRow
17087 * @description stacks on the uiGridRow directive to add support for expandable rows
17089 module.directive('uiGridRow',
17090 ['$compile', 'gridUtil', '$templateCache',
17091 function ($compile, gridUtil, $templateCache) {
17095 compile: function ($elm, $attrs) {
17097 pre: function ($scope, $elm, $attrs, controllers) {
17099 $scope.expandableRow = {};
17101 $scope.expandableRow.shouldRenderExpand = function () {
17102 var ret = $scope.colContainer.name === 'body' && $scope.grid.options.enableExpandable !== false && $scope.row.isExpanded && (!$scope.grid.isScrollingVertically || $scope.row.expandedRendered);
17106 $scope.expandableRow.shouldRenderFiller = function () {
17107 var ret = $scope.row.isExpanded && ( $scope.colContainer.name !== 'body' || ($scope.grid.isScrollingVertically && !$scope.row.expandedRendered));
17112 * Commented out @PaulL1. This has no purpose that I can see, and causes #2964. If this code needs to be reinstated for some
17113 * reason it needs to use drawnWidth, not width, and needs to check column visibility. It should really use render container
17114 * visible column cache also instead of checking column.renderContainer.
17115 function updateRowContainerWidth() {
17116 var grid = $scope.grid;
17118 grid.columns.forEach( function (column) {
17119 if (column.renderContainer === 'left') {
17120 colWidth += column.width;
17123 colWidth = Math.floor(colWidth);
17124 return '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.colContainer.name + ', .grid' + grid.id +
17125 ' .ui-grid-pinned-container-' + $scope.colContainer.name + ' .ui-grid-render-container-' + $scope.colContainer.name +
17126 ' .ui-grid-viewport .ui-grid-canvas .ui-grid-row { width: ' + colWidth + 'px; }';
17129 if ($scope.colContainer.name === 'left') {
17130 $scope.grid.registerStyleComputation({
17132 func: updateRowContainerWidth
17137 post: function ($scope, $elm, $attrs, controllers) {
17146 * @name ui.grid.expandable.directive:uiGridViewport
17147 * @description stacks on the uiGridViewport directive to append the expandable row html elements to the
17148 * default gridRow template
17150 module.directive('uiGridViewport',
17151 ['$compile', 'gridUtil', '$templateCache',
17152 function ($compile, gridUtil, $templateCache) {
17156 compile: function ($elm, $attrs) {
17157 var rowRepeatDiv = angular.element($elm.children().children()[0]);
17158 var expandedRowFillerElement = $templateCache.get('ui-grid/expandableScrollFiller');
17159 var expandedRowElement = $templateCache.get('ui-grid/expandableRow');
17160 rowRepeatDiv.append(expandedRowElement);
17161 rowRepeatDiv.append(expandedRowFillerElement);
17163 pre: function ($scope, $elm, $attrs, controllers) {
17165 post: function ($scope, $elm, $attrs, controllers) {
17174 /* global console */
17181 * @name ui.grid.exporter
17184 * # ui.grid.exporter
17186 * <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>
17188 * This module provides the ability to export data from the grid.
17190 * Data can be exported in a range of formats, and all data, visible
17191 * data, or selected rows can be exported, with all columns or visible
17194 * No UI is provided, the caller should provide their own UI/buttons
17195 * as appropriate, or enable the gridMenu
17200 * <div doc-module-components="ui.grid.exporter"></div>
17203 var module = angular.module('ui.grid.exporter', ['ui.grid']);
17207 * @name ui.grid.exporter.constant:uiGridExporterConstants
17209 * @description constants available in exporter module
17213 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
17215 * @description export all data, including data not visible. Can
17216 * be set for either rowTypes or colTypes
17220 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
17222 * @description export only visible data, including data not visible. Can
17223 * be set for either rowTypes or colTypes
17227 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
17229 * @description export all data, including data not visible. Can
17230 * be set only for rowTypes, selection of only some columns is
17233 module.constant('uiGridExporterConstants', {
17234 featureName: 'exporter',
17236 VISIBLE: 'visible',
17237 SELECTED: 'selected',
17238 CSV_CONTENT: 'CSV_CONTENT',
17239 BUTTON_LABEL: 'BUTTON_LABEL',
17240 FILE_NAME: 'FILE_NAME'
17245 * @name ui.grid.exporter.service:uiGridExporterService
17247 * @description Services for exporter feature
17249 module.service('uiGridExporterService', ['$q', 'uiGridExporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService',
17250 function ($q, uiGridExporterConstants, gridUtil, $compile, $interval, i18nService) {
17256 initializeGrid: function (grid) {
17258 //add feature namespace and any properties to grid for needed state
17259 grid.exporter = {};
17260 this.defaultGridOptions(grid.options);
17264 * @name ui.grid.exporter.api:PublicApi
17266 * @description Public Api for exporter feature
17278 * @methodOf ui.grid.exporter.api:PublicApi
17279 * @description Exports rows from the grid in csv format,
17280 * the data exported is selected based on the provided options
17281 * @param {string} rowTypes which rows to export, valid values are
17282 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17283 * uiGridExporterConstants.SELECTED
17284 * @param {string} colTypes which columns to export, valid values are
17285 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
17287 csvExport: function (rowTypes, colTypes) {
17288 service.csvExport(grid, rowTypes, colTypes);
17293 * @methodOf ui.grid.exporter.api:PublicApi
17294 * @description Exports rows from the grid in pdf format,
17295 * the data exported is selected based on the provided options
17296 * Note that this function has a dependency on pdfMake, all
17297 * going well this has been installed for you.
17298 * The resulting pdf opens in a new browser window.
17299 * @param {string} rowTypes which rows to export, valid values are
17300 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17301 * uiGridExporterConstants.SELECTED
17302 * @param {string} colTypes which columns to export, valid values are
17303 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
17305 pdfExport: function (rowTypes, colTypes) {
17306 service.pdfExport(grid, rowTypes, colTypes);
17312 grid.api.registerEventsFromObject(publicApi.events);
17314 grid.api.registerMethodsFromObject(publicApi.methods);
17316 if (grid.api.core.addToGridMenu){
17317 service.addToMenu( grid );
17319 // order of registration is not guaranteed, register in a little while
17320 $interval( function() {
17321 if (grid.api.core.addToGridMenu){
17322 service.addToMenu( grid );
17329 defaultGridOptions: function (gridOptions) {
17330 //default option to true unless it was explicitly set to false
17333 * @name ui.grid.exporter.api:GridOptions
17335 * @description GridOptions for exporter feature, these are available to be
17336 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
17340 * @name ui.grid.exporter.api:ColumnDef
17341 * @description ColumnDef settings for exporter
17345 * @name exporterSuppressMenu
17346 * @propertyOf ui.grid.exporter.api:GridOptions
17347 * @description Don't show the export menu button, implying the user
17348 * will roll their own UI for calling the exporter
17349 * <br/>Defaults to false
17351 gridOptions.exporterSuppressMenu = gridOptions.exporterSuppressMenu === true;
17354 * @name exporterMenuLabel
17355 * @propertyOf ui.grid.exporter.api:GridOptions
17356 * @description The text to show on the exporter menu button
17358 * <br/>Defaults to 'Export'
17360 gridOptions.exporterMenuLabel = gridOptions.exporterMenuLabel ? gridOptions.exporterMenuLabel : 'Export';
17363 * @name exporterSuppressColumns
17364 * @propertyOf ui.grid.exporter.api:GridOptions
17365 * @description Columns that should not be exported. The selectionRowHeader is already automatically
17366 * suppressed, but if you had a button column or some other "system" column that shouldn't be shown in the
17367 * output then add it in this list. You should provide an array of column names.
17368 * <br/>Defaults to: []
17370 * gridOptions.exporterSuppressColumns = [ 'buttons' ];
17373 gridOptions.exporterSuppressColumns = gridOptions.exporterSuppressColumns ? gridOptions.exporterSuppressColumns : [];
17376 * @name exporterCsvColumnSeparator
17377 * @propertyOf ui.grid.exporter.api:GridOptions
17378 * @description The character to use as column separator
17380 * <br/>Defaults to ','
17382 gridOptions.exporterCsvColumnSeparator = gridOptions.exporterCsvColumnSeparator ? gridOptions.exporterCsvColumnSeparator : ',';
17385 * @name exporterCsvFilename
17386 * @propertyOf ui.grid.exporter.api:GridOptions
17387 * @description The default filename to use when saving the downloaded csv.
17388 * This will only work in some browsers.
17389 * <br/>Defaults to 'download.csv'
17391 gridOptions.exporterCsvFilename = gridOptions.exporterCsvFilename ? gridOptions.exporterCsvFilename : 'download.csv';
17394 * @name exporterPdfFilename
17395 * @propertyOf ui.grid.exporter.api:GridOptions
17396 * @description The default filename to use when saving the downloaded pdf, only used in IE (other browsers open pdfs in a new window)
17397 * <br/>Defaults to 'download.pdf'
17399 gridOptions.exporterPdfFilename = gridOptions.exporterPdfFilename ? gridOptions.exporterPdfFilename : 'download.pdf';
17402 * @name exporterOlderExcelCompatibility
17403 * @propertyOf ui.grid.exporter.api:GridOptions
17404 * @description Some versions of excel don't like the utf-16 BOM on the front, and it comes
17405 * through as  in the first column header. Setting this option to false will suppress this, at the
17406 * expense of proper utf-16 handling in applications that do recognise the BOM
17407 * <br/>Defaults to false
17409 gridOptions.exporterOlderExcelCompatibility = gridOptions.exporterOlderExcelCompatibility === true;
17412 * @name exporterPdfDefaultStyle
17413 * @propertyOf ui.grid.exporter.api:GridOptions
17414 * @description The default style in pdfMake format
17415 * <br/>Defaults to:
17422 gridOptions.exporterPdfDefaultStyle = gridOptions.exporterPdfDefaultStyle ? gridOptions.exporterPdfDefaultStyle : { fontSize: 11 };
17425 * @name exporterPdfTableStyle
17426 * @propertyOf ui.grid.exporter.api:GridOptions
17427 * @description The table style in pdfMake format
17428 * <br/>Defaults to:
17431 * margin: [0, 5, 0, 15]
17435 gridOptions.exporterPdfTableStyle = gridOptions.exporterPdfTableStyle ? gridOptions.exporterPdfTableStyle : { margin: [0, 5, 0, 15] };
17438 * @name exporterPdfTableHeaderStyle
17439 * @propertyOf ui.grid.exporter.api:GridOptions
17440 * @description The tableHeader style in pdfMake format
17441 * <br/>Defaults to:
17450 gridOptions.exporterPdfTableHeaderStyle = gridOptions.exporterPdfTableHeaderStyle ? gridOptions.exporterPdfTableHeaderStyle : { bold: true, fontSize: 12, color: 'black' };
17453 * @name exporterPdfHeader
17454 * @propertyOf ui.grid.exporter.api:GridOptions
17455 * @description The header section for pdf exports. Can be
17458 * gridOptions.exporterPdfHeader = 'My Header';
17460 * Can be a more complex object in pdfMake format:
17462 * gridOptions.exporterPdfHeader = {
17465 * { text: 'Right part', alignment: 'right' }
17469 * Or can be a function, allowing page numbers and the like
17471 * gridOptions.exporterPdfHeader: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
17474 gridOptions.exporterPdfHeader = gridOptions.exporterPdfHeader ? gridOptions.exporterPdfHeader : null;
17477 * @name exporterPdfFooter
17478 * @propertyOf ui.grid.exporter.api:GridOptions
17479 * @description The header section for pdf exports. Can be
17482 * gridOptions.exporterPdfFooter = 'My Footer';
17484 * Can be a more complex object in pdfMake format:
17486 * gridOptions.exporterPdfFooter = {
17489 * { text: 'Right part', alignment: 'right' }
17493 * Or can be a function, allowing page numbers and the like
17495 * gridOptions.exporterPdfFooter: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
17498 gridOptions.exporterPdfFooter = gridOptions.exporterPdfFooter ? gridOptions.exporterPdfFooter : null;
17501 * @name exporterPdfOrientation
17502 * @propertyOf ui.grid.exporter.api:GridOptions
17503 * @description The orientation, should be a valid pdfMake value,
17504 * 'landscape' or 'portrait'
17505 * <br/>Defaults to landscape
17507 gridOptions.exporterPdfOrientation = gridOptions.exporterPdfOrientation ? gridOptions.exporterPdfOrientation : 'landscape';
17510 * @name exporterPdfPageSize
17511 * @propertyOf ui.grid.exporter.api:GridOptions
17512 * @description The orientation, should be a valid pdfMake
17513 * paper size, usually 'A4' or 'LETTER'
17514 * {@link https://github.com/bpampuch/pdfmake/blob/master/src/standardPageSizes.js pdfMake page sizes}
17515 * <br/>Defaults to A4
17517 gridOptions.exporterPdfPageSize = gridOptions.exporterPdfPageSize ? gridOptions.exporterPdfPageSize : 'A4';
17520 * @name exporterPdfMaxGridWidth
17521 * @propertyOf ui.grid.exporter.api:GridOptions
17522 * @description The maxium grid width - the current grid width
17523 * will be scaled to match this, with any fixed width columns
17524 * being adjusted accordingly.
17525 * <br/>Defaults to 720 (for A4 landscape), use 670 for LETTER
17527 gridOptions.exporterPdfMaxGridWidth = gridOptions.exporterPdfMaxGridWidth ? gridOptions.exporterPdfMaxGridWidth : 720;
17530 * @name exporterPdfTableLayout
17531 * @propertyOf ui.grid.exporter.api:GridOptions
17532 * @description A tableLayout in pdfMake format,
17533 * controls gridlines and the like. We use the default
17535 * <br/>Defaults to null, which means no layout
17540 * @name exporterMenuAllData
17541 * @porpertyOf ui.grid.exporter.api:GridOptions
17542 * @description Add export all data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
17544 gridOptions.exporterMenuAllData = gridOptions.exporterMenuAllData !== undefined ? gridOptions.exporterMenuAllData : true;
17548 * @name exporterMenuVisibleData
17549 * @porpertyOf ui.grid.exporter.api:GridOptions
17550 * @description Add export visible data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
17552 gridOptions.exporterMenuVisibleData = gridOptions.exporterMenuVisibleData !== undefined ? gridOptions.exporterMenuVisibleData : true;
17556 * @name exporterMenuSelectedData
17557 * @porpertyOf ui.grid.exporter.api:GridOptions
17558 * @description Add export selected data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
17560 gridOptions.exporterMenuSelectedData = gridOptions.exporterMenuSelectedData !== undefined ? gridOptions.exporterMenuSelectedData : true;
17564 * @name exporterMenuCsv
17565 * @propertyOf ui.grid.exporter.api:GridOptions
17566 * @description Add csv export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17568 gridOptions.exporterMenuCsv = gridOptions.exporterMenuCsv !== undefined ? gridOptions.exporterMenuCsv : true;
17572 * @name exporterMenuPdf
17573 * @propertyOf ui.grid.exporter.api:GridOptions
17574 * @description Add pdf export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17576 gridOptions.exporterMenuPdf = gridOptions.exporterMenuPdf !== undefined ? gridOptions.exporterMenuPdf : true;
17580 * @name exporterPdfCustomFormatter
17581 * @propertyOf ui.grid.exporter.api:GridOptions
17582 * @description A custom callback routine that changes the pdf document, adding any
17583 * custom styling or content that is supported by pdfMake. Takes in the complete docDefinition, and
17584 * must return an updated docDefinition ready for pdfMake.
17586 * In this example we add a style to the style array, so that we can use it in our
17587 * footer definition.
17589 * gridOptions.exporterPdfCustomFormatter = function ( docDefinition ) {
17590 * docDefinition.styles.footerStyle = { bold: true, fontSize: 10 };
17591 * return docDefinition;
17594 * gridOptions.exporterPdfFooter = { text: 'My footer', style: 'footerStyle' }
17597 gridOptions.exporterPdfCustomFormatter = ( gridOptions.exporterPdfCustomFormatter && typeof( gridOptions.exporterPdfCustomFormatter ) === 'function' ) ? gridOptions.exporterPdfCustomFormatter : function ( docDef ) { return docDef; };
17601 * @name exporterHeaderFilterUseName
17602 * @propertyOf ui.grid.exporter.api:GridOptions
17603 * @description Defaults to false, which leads to `displayName` being passed into the headerFilter.
17604 * If set to true, then will pass `name` instead.
17609 * gridOptions.exporterHeaderFilterUseName = true;
17612 gridOptions.exporterHeaderFilterUseName = gridOptions.exporterHeaderFilterUseName === true;
17616 * @name exporterHeaderFilter
17617 * @propertyOf ui.grid.exporter.api:GridOptions
17618 * @description A function to apply to the header displayNames before exporting. Useful for internationalisation,
17619 * for example if you were using angular-translate you'd set this to `$translate.instant`. Note that this
17620 * call must be synchronous, it cannot be a call that returns a promise.
17622 * Behaviour can be changed to pass in `name` instead of `displayName` through use of `exporterHeaderFilterUseName: true`.
17626 * gridOptions.exporterHeaderFilter = function( displayName ){ return 'col: ' + name; };
17630 * gridOptions.exporterHeaderFilter = $translate.instant;
17636 * @name exporterFieldCallback
17637 * @propertyOf ui.grid.exporter.api:GridOptions
17638 * @description A function to call for each field before exporting it. Allows
17639 * massaging of raw data into a display format, for example if you have applied
17640 * filters to convert codes into decodes, or you require
17641 * a specific date format in the exported content.
17643 * The method is called once for each field exported, and provides the grid, the
17644 * gridCol and the GridRow for you to use as context in massaging the data.
17646 * @param {Grid} grid provides the grid in case you have need of it
17647 * @param {GridRow} row the row from which the data comes
17648 * @param {GridCol} col the column from which the data comes
17649 * @param {object} value the value for your massaging
17650 * @returns {object} you must return the massaged value ready for exporting
17654 * gridOptions.exporterFieldCallback = function ( grid, row, col, value ){
17655 * if ( col.name === 'status' ){
17656 * value = decodeStatus( value );
17662 gridOptions.exporterFieldCallback = gridOptions.exporterFieldCallback ? gridOptions.exporterFieldCallback : function( grid, row, col, value ) { return value; };
17666 * @name exporterAllDataFn
17667 * @propertyOf ui.grid.exporter.api:GridOptions
17668 * @description This promise is needed when exporting all rows,
17669 * and the data need to be provided by server side. Default is null.
17670 * @returns {Promise} a promise to load all data from server
17674 * gridOptions.exporterAllDataFn = function () {
17675 * return $http.get('/data/100.json')
17679 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataFn ? gridOptions.exporterAllDataFn : null;
17683 * @name exporterAllDataPromise
17684 * @propertyOf ui.grid.exporter.api:GridOptions
17685 * @description DEPRECATED - exporterAllDataFn used to be
17686 * called this, but it wasn't a promise, it was a function that returned
17687 * a promise. Deprecated, but supported for backward compatibility, use
17688 * exporterAllDataFn instead.
17689 * @returns {Promise} a promise to load all data from server
17693 * gridOptions.exporterAllDataFn = function () {
17694 * return $http.get('/data/100.json')
17698 if ( gridOptions.exporterAllDataFn == null && gridOptions.exporterAllDataPromise ) {
17699 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataPromise;
17707 * @methodOf ui.grid.exporter.service:uiGridExporterService
17708 * @description Adds export items to the grid menu,
17709 * allowing the user to select export options
17710 * @param {Grid} grid the grid from which data should be exported
17712 addToMenu: function ( grid ) {
17713 grid.api.core.addToGridMenu( grid, [
17715 title: i18nService.getSafeText('gridMenu.exporterAllAsCsv'),
17716 action: function ($event) {
17717 this.grid.api.exporter.csvExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17719 shown: function() {
17720 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuAllData;
17725 title: i18nService.getSafeText('gridMenu.exporterVisibleAsCsv'),
17726 action: function ($event) {
17727 this.grid.api.exporter.csvExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17729 shown: function() {
17730 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuVisibleData;
17735 title: i18nService.getSafeText('gridMenu.exporterSelectedAsCsv'),
17736 action: function ($event) {
17737 this.grid.api.exporter.csvExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17739 shown: function() {
17740 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuSelectedData &&
17741 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17746 title: i18nService.getSafeText('gridMenu.exporterAllAsPdf'),
17747 action: function ($event) {
17748 this.grid.api.exporter.pdfExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17750 shown: function() {
17751 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuAllData;
17756 title: i18nService.getSafeText('gridMenu.exporterVisibleAsPdf'),
17757 action: function ($event) {
17758 this.grid.api.exporter.pdfExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17760 shown: function() {
17761 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuVisibleData;
17766 title: i18nService.getSafeText('gridMenu.exporterSelectedAsPdf'),
17767 action: function ($event) {
17768 this.grid.api.exporter.pdfExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17770 shown: function() {
17771 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuSelectedData &&
17772 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17783 * @methodOf ui.grid.exporter.service:uiGridExporterService
17784 * @description Exports rows from the grid in csv format,
17785 * the data exported is selected based on the provided options
17786 * @param {Grid} grid the grid from which data should be exported
17787 * @param {string} rowTypes which rows to export, valid values are
17788 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17789 * uiGridExporterConstants.SELECTED
17790 * @param {string} colTypes which columns to export, valid values are
17791 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17792 * uiGridExporterConstants.SELECTED
17794 csvExport: function (grid, rowTypes, colTypes) {
17796 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function() {
17797 var exportColumnHeaders = grid.options.showHeader ? self.getColumnHeaders(grid, colTypes) : [];
17798 var exportData = self.getData(grid, rowTypes, colTypes);
17799 var csvContent = self.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator);
17801 self.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility);
17807 * @name loadAllDataIfNeeded
17808 * @methodOf ui.grid.exporter.service:uiGridExporterService
17809 * @description When using server side pagination, use exporterAllDataFn to
17810 * load all data before continuing processing.
17811 * When using client side pagination, return a resolved promise so processing
17812 * continues immediately
17813 * @param {Grid} grid the grid from which data should be exported
17814 * @param {string} rowTypes which rows to export, valid values are
17815 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17816 * uiGridExporterConstants.SELECTED
17817 * @param {string} colTypes which columns to export, valid values are
17818 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17819 * uiGridExporterConstants.SELECTED
17821 loadAllDataIfNeeded: function (grid, rowTypes, colTypes) {
17822 if ( rowTypes === uiGridExporterConstants.ALL && grid.rows.length !== grid.options.totalItems && grid.options.exporterAllDataFn) {
17823 return grid.options.exporterAllDataFn()
17825 grid.modifyRows(grid.options.data);
17828 var deferred = $q.defer();
17829 deferred.resolve();
17830 return deferred.promise;
17836 * @propertyOf ui.grid.exporter.api:ColumnDef
17837 * @name exporterSuppressExport
17838 * @description Suppresses export for this column. Used by selection and expandable.
17843 * @name getColumnHeaders
17844 * @methodOf ui.grid.exporter.service:uiGridExporterService
17845 * @description Gets the column headers from the grid to use
17846 * as a title row for the exported file, all headers have
17847 * headerCellFilters applied as appropriate.
17849 * Column headers are an array of objects, each object has
17850 * name, displayName, width and align attributes. Only name is
17851 * used for csv, all attributes are used for pdf.
17853 * @param {Grid} grid the grid from which data should be exported
17854 * @param {string} colTypes which columns to export, valid values are
17855 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17856 * uiGridExporterConstants.SELECTED
17858 getColumnHeaders: function (grid, colTypes) {
17862 if ( colTypes === uiGridExporterConstants.ALL ){
17863 columns = grid.columns;
17865 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17866 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17867 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17869 columns = leftColumns.concat(bodyColumns,rightColumns);
17872 columns.forEach( function( gridCol, index ) {
17873 if ( gridCol.colDef.exporterSuppressExport !== true &&
17874 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17876 name: gridCol.field,
17877 displayName: grid.options.exporterHeaderFilter ? ( grid.options.exporterHeaderFilterUseName ? grid.options.exporterHeaderFilter(gridCol.name) : grid.options.exporterHeaderFilter(gridCol.displayName) ) : gridCol.displayName,
17878 width: gridCol.drawnWidth ? gridCol.drawnWidth : gridCol.width,
17879 align: gridCol.colDef.type === 'number' ? 'right' : 'left'
17890 * @propertyOf ui.grid.exporter.api:ColumnDef
17891 * @name exporterPdfAlign
17892 * @description the alignment you'd like for this specific column when
17893 * exported into a pdf. Can be 'left', 'right', 'center' or any other
17894 * valid pdfMake alignment option.
17900 * @name ui.grid.exporter.api:GridRow
17901 * @description GridRow settings for exporter
17905 * @name exporterEnableExporting
17906 * @propertyOf ui.grid.exporter.api:GridRow
17907 * @description If set to false, then don't export this row, notwithstanding visible or
17909 * <br/>Defaults to true
17915 * @methodOf ui.grid.exporter.service:uiGridExporterService
17916 * @description Gets data from the grid based on the provided options,
17917 * all cells have cellFilters applied as appropriate. Any rows marked
17918 * `exporterEnableExporting: false` will not be exported
17919 * @param {Grid} grid the grid from which data should be exported
17920 * @param {string} rowTypes which rows to export, valid values are
17921 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17922 * uiGridExporterConstants.SELECTED
17923 * @param {string} colTypes which columns to export, valid values are
17924 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17925 * uiGridExporterConstants.SELECTED
17926 * @param {boolean} applyCellFilters whether or not to get the display value or the raw value of the data
17928 getData: function (grid, rowTypes, colTypes, applyCellFilters) {
17933 switch ( rowTypes ) {
17934 case uiGridExporterConstants.ALL:
17937 case uiGridExporterConstants.VISIBLE:
17938 rows = grid.getVisibleRows();
17940 case uiGridExporterConstants.SELECTED:
17941 if ( grid.api.selection ){
17942 rows = grid.api.selection.getSelectedGridRows();
17944 gridUtil.logError('selection feature must be enabled to allow selected rows to be exported');
17949 if ( colTypes === uiGridExporterConstants.ALL ){
17950 columns = grid.columns;
17952 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17953 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17954 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17956 columns = leftColumns.concat(bodyColumns,rightColumns);
17959 rows.forEach( function( row, index ) {
17961 if (row.exporterEnableExporting !== false) {
17962 var extractedRow = [];
17965 columns.forEach( function( gridCol, index ) {
17966 if ( (gridCol.visible || colTypes === uiGridExporterConstants.ALL ) &&
17967 gridCol.colDef.exporterSuppressExport !== true &&
17968 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17969 var cellValue = applyCellFilters ? grid.getCellDisplayValue( row, gridCol ) : grid.getCellValue( row, gridCol );
17970 var extractedField = { value: grid.options.exporterFieldCallback( grid, row, gridCol, cellValue ) };
17971 if ( gridCol.colDef.exporterPdfAlign ) {
17972 extractedField.alignment = gridCol.colDef.exporterPdfAlign;
17974 extractedRow.push(extractedField);
17978 data.push(extractedRow);
17988 * @name formatAsCSV
17989 * @methodOf ui.grid.exporter.service:uiGridExporterService
17990 * @description Formats the column headers and data as a CSV,
17991 * and sends that data to the user
17992 * @param {array} exportColumnHeaders an array of column headers,
17993 * where each header is an object with name, width and maybe alignment
17994 * @param {array} exportData an array of rows, where each row is
17995 * an array of column data
17996 * @returns {string} csv the formatted csv as a string
17998 formatAsCsv: function (exportColumnHeaders, exportData, separator) {
18001 var bareHeaders = exportColumnHeaders.map(function(header){return { value: header.displayName };});
18003 var csv = bareHeaders.length > 0 ? (self.formatRowAsCsv(this, separator)(bareHeaders) + '\n') : '';
18005 csv += exportData.map(this.formatRowAsCsv(this, separator)).join('\n');
18012 * @name formatRowAsCsv
18013 * @methodOf ui.grid.exporter.service:uiGridExporterService
18014 * @description Renders a single field as a csv field, including
18015 * quotes around the value
18016 * @param {exporterService} exporter pass in exporter
18017 * @param {array} row the row to be turned into a csv string
18018 * @returns {string} a csv-ified version of the row
18020 formatRowAsCsv: function (exporter, separator) {
18021 return function (row) {
18022 return row.map(exporter.formatFieldAsCsv).join(separator);
18028 * @name formatFieldAsCsv
18029 * @methodOf ui.grid.exporter.service:uiGridExporterService
18030 * @description Renders a single field as a csv field, including
18031 * quotes around the value
18032 * @param {field} field the field to be turned into a csv string,
18033 * may be of any type
18034 * @returns {string} a csv-ified version of the field
18036 formatFieldAsCsv: function (field) {
18037 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
18040 if (typeof(field.value) === 'number') {
18041 return field.value;
18043 if (typeof(field.value) === 'boolean') {
18044 return (field.value ? 'TRUE' : 'FALSE') ;
18046 if (typeof(field.value) === 'string') {
18047 return '"' + field.value.replace(/"/g,'""') + '"';
18050 return JSON.stringify(field.value);
18057 * @methodOf ui.grid.exporter.service:uiGridExporterService
18058 * @description Checks whether current browser is IE and returns it's version if it is
18060 isIE: function () {
18061 var match = navigator.userAgent.search(/(?:Edge|MSIE|Trident\/.*; rv:)/);
18064 if (match !== -1) {
18074 * @name downloadFile
18075 * @methodOf ui.grid.exporter.service:uiGridExporterService
18076 * @description Triggers download of a csv file. Logic provided
18077 * by @cssensei (from his colleagues at https://github.com/ifeelgoods) in issue #2391
18078 * @param {string} fileName the filename we'd like our file to be
18080 * @param {string} csvContent the csv content that we'd like to
18081 * download as a file
18082 * @param {boolean} exporterOlderExcelCompatibility whether or not we put a utf-16 BOM on the from (\uFEFF)
18084 downloadFile: function (fileName, csvContent, exporterOlderExcelCompatibility) {
18086 var a = D.createElement('a');
18087 var strMimeType = 'application/octet-stream;charset=utf-8';
18091 ieVersion = this.isIE();
18092 if (ieVersion && ieVersion < 10) {
18093 var frame = D.createElement('iframe');
18094 document.body.appendChild(frame);
18096 frame.contentWindow.document.open("text/html", "replace");
18097 frame.contentWindow.document.write('sep=,\r\n' + csvContent);
18098 frame.contentWindow.document.close();
18099 frame.contentWindow.focus();
18100 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
18102 document.body.removeChild(frame);
18107 if (navigator.msSaveBlob) {
18108 return navigator.msSaveOrOpenBlob(
18110 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
18111 { type: strMimeType } ),
18116 //html5 A[download]
18117 if ('download' in a) {
18118 var blob = new Blob(
18119 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
18120 { type: strMimeType }
18122 rawFile = URL.createObjectURL(blob);
18123 a.setAttribute('download', fileName);
18125 rawFile = 'data:' + strMimeType + ',' + encodeURIComponent(csvContent);
18126 a.setAttribute('target', '_blank');
18130 a.setAttribute('style', 'display:none;');
18131 D.body.appendChild(a);
18132 setTimeout(function() {
18135 // Workaround for Safari 5
18136 } else if (document.createEvent) {
18137 var eventObj = document.createEvent('MouseEvents');
18138 eventObj.initEvent('click', true, true);
18139 a.dispatchEvent(eventObj);
18141 D.body.removeChild(a);
18149 * @methodOf ui.grid.exporter.service:uiGridExporterService
18150 * @description Exports rows from the grid in pdf format,
18151 * the data exported is selected based on the provided options.
18152 * Note that this function has a dependency on pdfMake, which must
18153 * be installed. The resulting pdf opens in a new
18155 * @param {Grid} grid the grid from which data should be exported
18156 * @param {string} rowTypes which rows to export, valid values are
18157 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
18158 * uiGridExporterConstants.SELECTED
18159 * @param {string} colTypes which columns to export, valid values are
18160 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
18161 * uiGridExporterConstants.SELECTED
18163 pdfExport: function (grid, rowTypes, colTypes) {
18165 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function () {
18166 var exportColumnHeaders = self.getColumnHeaders(grid, colTypes);
18167 var exportData = self.getData(grid, rowTypes, colTypes);
18168 var docDefinition = self.prepareAsPdf(grid, exportColumnHeaders, exportData);
18170 if (self.isIE() || navigator.appVersion.indexOf("Edge") !== -1) {
18171 self.downloadPDF(grid.options.exporterPdfFilename, docDefinition);
18173 pdfMake.createPdf(docDefinition).open();
18181 * @name downloadPdf
18182 * @methodOf ui.grid.exporter.service:uiGridExporterService
18183 * @description Generates and retrieves the pdf as a blob, then downloads
18184 * it as a file. Only used in IE, in all other browsers we use the native
18185 * pdfMake.open function to just open the PDF
18186 * @param {string} fileName the filename to give to the pdf, can be set
18187 * through exporterPdfFilename
18188 * @param {object} docDefinition a pdf docDefinition that we can generate
18189 * and get a blob from
18191 downloadPDF: function (fileName, docDefinition) {
18193 var a = D.createElement('a');
18194 var strMimeType = 'application/octet-stream;charset=utf-8';
18198 ieVersion = this.isIE(); // This is now a boolean value
18199 var doc = pdfMake.createPdf(docDefinition);
18202 doc.getBuffer( function (buffer) {
18203 blob = new Blob([buffer]);
18206 if (navigator.msSaveBlob) {
18207 return navigator.msSaveBlob(
18212 // Previously: && ieVersion < 10
18213 // ieVersion now returns a boolean for the
18214 // sake of sanity. We just check `msSaveBlob` first.
18216 var frame = D.createElement('iframe');
18217 document.body.appendChild(frame);
18219 frame.contentWindow.document.open("text/html", "replace");
18220 frame.contentWindow.document.write(blob);
18221 frame.contentWindow.document.close();
18222 frame.contentWindow.focus();
18223 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
18225 document.body.removeChild(frame);
18234 * @name renderAsPdf
18235 * @methodOf ui.grid.exporter.service:uiGridExporterService
18236 * @description Renders the data into a pdf, and opens that pdf.
18238 * @param {Grid} grid the grid from which data should be exported
18239 * @param {array} exportColumnHeaders an array of column headers,
18240 * where each header is an object with name, width and maybe alignment
18241 * @param {array} exportData an array of rows, where each row is
18242 * an array of column data
18243 * @returns {object} a pdfMake format document definition, ready
18246 prepareAsPdf: function(grid, exportColumnHeaders, exportData) {
18247 var headerWidths = this.calculatePdfHeaderWidths( grid, exportColumnHeaders );
18249 var headerColumns = exportColumnHeaders.map( function( header ) {
18250 return { text: header.displayName, style: 'tableHeader' };
18253 var stringData = exportData.map(this.formatRowAsPdf(this));
18255 var allData = [headerColumns].concat(stringData);
18257 var docDefinition = {
18258 pageOrientation: grid.options.exporterPdfOrientation,
18259 pageSize: grid.options.exporterPdfPageSize,
18261 style: 'tableStyle',
18264 widths: headerWidths,
18269 tableStyle: grid.options.exporterPdfTableStyle,
18270 tableHeader: grid.options.exporterPdfTableHeaderStyle
18272 defaultStyle: grid.options.exporterPdfDefaultStyle
18275 if ( grid.options.exporterPdfLayout ){
18276 docDefinition.layout = grid.options.exporterPdfLayout;
18279 if ( grid.options.exporterPdfHeader ){
18280 docDefinition.header = grid.options.exporterPdfHeader;
18283 if ( grid.options.exporterPdfFooter ){
18284 docDefinition.footer = grid.options.exporterPdfFooter;
18287 if ( grid.options.exporterPdfCustomFormatter ){
18288 docDefinition = grid.options.exporterPdfCustomFormatter( docDefinition );
18290 return docDefinition;
18297 * @name calculatePdfHeaderWidths
18298 * @methodOf ui.grid.exporter.service:uiGridExporterService
18299 * @description Determines the column widths base on the
18300 * widths we got from the grid. If the column is drawn
18301 * then we have a drawnWidth. If the column is not visible
18302 * then we have '*', 'x%' or a width. When columns are
18303 * not visible they don't contribute to the overall gridWidth,
18304 * so we need to adjust to allow for extra columns
18306 * Our basic heuristic is to take the current gridWidth, plus
18307 * numeric columns and call this the base gridwidth.
18309 * To that we add 100 for any '*' column, and x% of the base gridWidth
18310 * for any column that is a %
18312 * @param {Grid} grid the grid from which data should be exported
18313 * @param {array} exportHeaders array of header information
18314 * @returns {object} an array of header widths
18316 calculatePdfHeaderWidths: function ( grid, exportHeaders ) {
18317 var baseGridWidth = 0;
18318 exportHeaders.forEach( function(value){
18319 if (typeof(value.width) === 'number'){
18320 baseGridWidth += value.width;
18324 var extraColumns = 0;
18325 exportHeaders.forEach( function(value){
18326 if (value.width === '*'){
18327 extraColumns += 100;
18329 if (typeof(value.width) === 'string' && value.width.match(/(\d)*%/)) {
18330 var percent = parseInt(value.width.match(/(\d)*%/)[0]);
18332 value.width = baseGridWidth * percent / 100;
18333 extraColumns += value.width;
18337 var gridWidth = baseGridWidth + extraColumns;
18339 return exportHeaders.map(function( header ) {
18340 return header.width === '*' ? header.width : header.width * grid.options.exporterPdfMaxGridWidth / gridWidth;
18347 * @name formatRowAsPdf
18348 * @methodOf ui.grid.exporter.service:uiGridExporterService
18349 * @description Renders a row in a format consumable by PDF,
18350 * mainly meaning casting everything to a string
18351 * @param {exporterService} exporter pass in exporter
18352 * @param {array} row the row to be turned into a csv string
18353 * @returns {string} a csv-ified version of the row
18355 formatRowAsPdf: function ( exporter ) {
18356 return function( row ) {
18357 return row.map(exporter.formatFieldAsPdfString);
18364 * @name formatFieldAsCsv
18365 * @methodOf ui.grid.exporter.service:uiGridExporterService
18366 * @description Renders a single field as a pdf-able field, which
18367 * is different from a csv field only in that strings don't have quotes
18369 * @param {field} field the field to be turned into a pdf string,
18370 * may be of any type
18371 * @returns {string} a string-ified version of the field
18373 formatFieldAsPdfString: function (field) {
18375 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
18377 } else if (typeof(field.value) === 'number') {
18378 returnVal = field.value.toString();
18379 } else if (typeof(field.value) === 'boolean') {
18380 returnVal = (field.value ? 'TRUE' : 'FALSE') ;
18381 } else if (typeof(field.value) === 'string') {
18382 returnVal = field.value.replace(/"/g,'""');
18384 returnVal = JSON.stringify(field.value).replace(/^"/,'').replace(/"$/,'');
18387 if (field.alignment && typeof(field.alignment) === 'string' ){
18388 returnVal = { text: returnVal, alignment: field.alignment };
18402 * @name ui.grid.exporter.directive:uiGridExporter
18406 * @description Adds exporter features to grid
18409 <example module="app">
18410 <file name="app.js">
18411 var app = angular.module('app', ['ui.grid', 'ui.grid.exporter']);
18413 app.controller('MainCtrl', ['$scope', function ($scope) {
18415 { name: 'Bob', title: 'CEO' },
18416 { name: 'Frank', title: 'Lowly Developer' }
18419 $scope.gridOptions = {
18420 enableGridMenu: true,
18421 exporterMenuCsv: false,
18423 {name: 'name', enableCellEdit: true},
18424 {name: 'title', enableCellEdit: true}
18430 <file name="index.html">
18431 <div ng-controller="MainCtrl">
18432 <div ui-grid="gridOptions" ui-grid-exporter></div>
18437 module.directive('uiGridExporter', ['uiGridExporterConstants', 'uiGridExporterService', 'gridUtil', '$compile',
18438 function (uiGridExporterConstants, uiGridExporterService, gridUtil, $compile) {
18442 require: '^uiGrid',
18444 link: function ($scope, $elm, $attrs, uiGridCtrl) {
18445 uiGridExporterService.initializeGrid(uiGridCtrl.grid);
18446 uiGridCtrl.grid.exporter.$scope = $scope;
18458 * @name ui.grid.grouping
18461 * # ui.grid.grouping
18463 * <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>
18465 * This module provides grouping of rows based on the data in them, similar
18466 * in concept to excel grouping. You can group multiple columns, resulting in
18469 * In concept this feature is similar to sorting + grid footer/aggregation, it
18470 * sorts the data based on the grouped columns, then creates group rows that
18471 * reflect a break in the data. Each of those group rows can have aggregations for
18472 * the data within that group.
18474 * This feature leverages treeBase to provide the tree functionality itself,
18475 * the key thing this feature does therefore is to set treeLevels on the rows
18476 * and insert the group headers.
18478 * Design information:
18479 * -------------------
18481 * Each column will get new menu items - group by, and aggregate by. Group by
18482 * will cause this column to be sorted (if not already), and will move this column
18483 * to the front of the sorted columns (i.e. grouped columns take precedence over
18484 * sorted columns). It will respect the sort order already set if there is one,
18485 * and it will allow the sorting logic to change that sort order, it just forces
18486 * the column to the front of the sorting. You can group by multiple columns, the
18487 * logic will add this column to the sorting after any already grouped columns.
18489 * Once a grouping is defined, grouping logic is added to the rowsProcessors. This
18490 * will process the rows, identifying a break in the data value, and inserting a grouping row.
18491 * Grouping rows have specific attributes on them:
18493 * - internalRow = true: tells us that this isn't a real row, so we can ignore it
18494 * from any processing that it looking at core data rows. This is used by the core
18495 * logic (or will be one day), as it's not grouping specific
18496 * - groupHeader = true: tells us this is a groupHeader. This is used by the grouping logic
18497 * to know if this is a groupHeader row or not
18499 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
18500 * row order or filtering or anything like that is changed. In order to avoid the row instantiation
18501 * time, and to preserve state across invocations, we hold a cache of the rows that we created
18502 * last time, and we use them again this time if we can.
18504 * By default rows are collapsed, which means all data rows have their visible property
18505 * set to false, and only level 0 group rows are set to visible.
18510 * <div doc-module-components="ui.grid.grouping"></div>
18513 var module = angular.module('ui.grid.grouping', ['ui.grid', 'ui.grid.treeBase']);
18517 * @name ui.grid.grouping.constant:uiGridGroupingConstants
18519 * @description constants available in grouping module, this includes
18520 * all the constants declared in the treeBase module (these are manually copied
18521 * as there isn't an easy way to include constants in another constants file, and
18522 * we don't want to make users include treeBase)
18525 module.constant('uiGridGroupingConstants', {
18526 featureName: "grouping",
18527 rowHeaderColName: 'treeBaseRowHeaderCol',
18528 EXPANDED: 'expanded',
18529 COLLAPSED: 'collapsed',
18541 * @name ui.grid.grouping.service:uiGridGroupingService
18543 * @description Services for grouping features
18545 module.service('uiGridGroupingService', ['$q', 'uiGridGroupingConstants', 'gridUtil', 'rowSorter', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'uiGridTreeBaseService',
18546 function ($q, uiGridGroupingConstants, gridUtil, rowSorter, GridRow, gridClassFactory, i18nService, uiGridConstants, uiGridTreeBaseService) {
18550 initializeGrid: function (grid, $scope) {
18551 uiGridTreeBaseService.initializeGrid( grid, $scope );
18553 //add feature namespace and any properties to grid for needed
18556 * @name ui.grid.grouping.grid:grouping
18558 * @description Grid properties and functions added for grouping
18560 grid.grouping = {};
18564 * @propertyOf ui.grid.grouping.grid:grouping
18565 * @name groupHeaderCache
18567 * @description Cache that holds the group header rows we created last time, we'll
18568 * reuse these next time, not least because they hold our expanded states.
18570 * We need to take care with these that they don't become a memory leak, we
18571 * create a new cache each time using the values from the old cache. This works
18572 * so long as we're creating group rows for invisible rows as well.
18574 * The cache is a nested hash, indexed on the value we grouped by. So if we
18575 * grouped by gender then age, we'd maybe have something like:
18579 * row: <pointer to the old row>,
18581 * 22: { row: <pointer to the old row> },
18582 * 31: { row: <pointer to the old row> }
18585 * row: <pointer to the old row>,
18587 * 28: { row: <pointer to the old row> },
18588 * 55: { row: <pointer to the old row> }
18593 * We create new rows for any missing rows, this means that they come in as collapsed.
18596 grid.grouping.groupHeaderCache = {};
18598 service.defaultGridOptions(grid.options);
18600 grid.registerRowsProcessor(service.groupRows, 400);
18602 grid.registerColumnBuilder( service.groupingColumnBuilder);
18604 grid.registerColumnsProcessor(service.groupingColumnProcessor, 400);
18608 * @name ui.grid.grouping.api:PublicApi
18610 * @description Public Api for grouping feature
18617 * @eventOf ui.grid.grouping.api:PublicApi
18618 * @name aggregationChanged
18619 * @description raised whenever aggregation is changed, added or removed from a column
18622 * gridApi.grouping.on.aggregationChanged(scope,function(col){})
18624 * @param {gridCol} col the column which on which aggregation changed. The aggregation
18625 * type is available as `col.treeAggregation.type`
18627 aggregationChanged: {},
18630 * @eventOf ui.grid.grouping.api:PublicApi
18631 * @name groupingChanged
18632 * @description raised whenever the grouped columns changes
18635 * gridApi.grouping.on.groupingChanged(scope,function(col){})
18637 * @param {gridCol} col the column which on which grouping changed. The new grouping is
18638 * available as `col.grouping`
18640 groupingChanged: {}
18647 * @name getGrouping
18648 * @methodOf ui.grid.grouping.api:PublicApi
18649 * @description Get the grouping configuration for this grid,
18650 * used by the saveState feature. Adds expandedState to the information
18651 * provided by the internal getGrouping, and removes any aggregations that have a source
18652 * of grouping (i.e. will be automatically reapplied when we regroup the column)
18653 * Returned grouping is an object
18654 * `{ grouping: groupArray, treeAggregations: aggregateArray, expandedState: hash }`
18655 * where grouping contains an array of objects:
18656 * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }`
18657 * and aggregations contains an array of objects:
18658 * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }`
18659 * and expandedState is a hash of the currently expanded nodes
18661 * The groupArray will be sorted by groupPriority.
18663 * @param {boolean} getExpanded whether or not to return the expanded state
18664 * @returns {object} grouping configuration
18666 getGrouping: function ( getExpanded ) {
18667 var grouping = service.getGrouping(grid);
18669 grouping.grouping.forEach( function( group ) {
18670 group.colName = group.col.name;
18674 grouping.aggregations.forEach( function( aggregation ) {
18675 aggregation.colName = aggregation.col.name;
18676 delete aggregation.col;
18679 grouping.aggregations = grouping.aggregations.filter( function( aggregation ){
18680 return !aggregation.aggregation.source || aggregation.aggregation.source !== 'grouping';
18683 if ( getExpanded ){
18684 grouping.rowExpandedStates = service.getRowExpandedStates( grid.grouping.groupingHeaderCache );
18692 * @name setGrouping
18693 * @methodOf ui.grid.grouping.api:PublicApi
18694 * @description Set the grouping configuration for this grid,
18695 * used by the saveState feature, but can also be used by any
18696 * user to specify a combined grouping and aggregation configuration
18697 * @param {object} config the config you want to apply, in the format
18698 * provided out by getGrouping
18700 setGrouping: function ( config ) {
18701 service.setGrouping(grid, config);
18706 * @name groupColumn
18707 * @methodOf ui.grid.grouping.api:PublicApi
18708 * @description Adds this column to the existing grouping, at the end of the priority order.
18709 * If the column doesn't have a sort, adds one, by default ASC
18711 * This column will move to the left of any non-group columns, the
18712 * move is handled in a columnProcessor, so gets called as part of refresh
18714 * @param {string} columnName the name of the column we want to group
18716 groupColumn: function( columnName ) {
18717 var column = grid.getColumn(columnName);
18718 service.groupColumn(grid, column);
18723 * @name ungroupColumn
18724 * @methodOf ui.grid.grouping.api:PublicApi
18725 * @description Removes the groupPriority from this column. If the
18726 * column was previously aggregated the aggregation will come back.
18727 * The sort will remain.
18729 * This column will move to the right of any other group columns, the
18730 * move is handled in a columnProcessor, so gets called as part of refresh
18732 * @param {string} columnName the name of the column we want to ungroup
18734 ungroupColumn: function( columnName ) {
18735 var column = grid.getColumn(columnName);
18736 service.ungroupColumn(grid, column);
18741 * @name clearGrouping
18742 * @methodOf ui.grid.grouping.api:PublicApi
18743 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
18744 * as we don't know whether that sorting was added by grouping or was there beforehand
18747 clearGrouping: function() {
18748 service.clearGrouping(grid);
18753 * @name aggregateColumn
18754 * @methodOf ui.grid.grouping.api:PublicApi
18755 * @description Sets the aggregation type on a column, if the
18756 * column is currently grouped then it removes the grouping first.
18757 * If the aggregationDef is null then will result in the aggregation
18760 * @param {string} columnName the column we want to aggregate
18761 * @param {string} or {function} aggregationDef one of the recognised types
18762 * from uiGridGroupingConstants or a custom aggregation function.
18763 * @param {string} aggregationLabel (optional) The label to use for this aggregation.
18765 aggregateColumn: function( columnName, aggregationDef, aggregationLabel){
18766 var column = grid.getColumn(columnName);
18767 service.aggregateColumn( grid, column, aggregationDef, aggregationLabel);
18774 grid.api.registerEventsFromObject(publicApi.events);
18776 grid.api.registerMethodsFromObject(publicApi.methods);
18778 grid.api.core.on.sortChanged( $scope, service.tidyPriorities);
18782 defaultGridOptions: function (gridOptions) {
18783 //default option to true unless it was explicitly set to false
18786 * @name ui.grid.grouping.api:GridOptions
18788 * @description GridOptions for grouping feature, these are available to be
18789 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
18794 * @name enableGrouping
18795 * @propertyOf ui.grid.grouping.api:GridOptions
18796 * @description Enable row grouping for entire grid.
18797 * <br/>Defaults to true
18799 gridOptions.enableGrouping = gridOptions.enableGrouping !== false;
18803 * @name groupingShowCounts
18804 * @propertyOf ui.grid.grouping.api:GridOptions
18805 * @description shows counts on the groupHeader rows. Not that if you are using a cellFilter or a
18806 * sortingAlgorithm which relies on a specific format or data type, showing counts may cause that
18807 * to break, since the group header rows will always be a string with groupingShowCounts enabled.
18808 * <br/>Defaults to true except on columns of type 'date'
18810 gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false;
18814 * @name groupingNullLabel
18815 * @propertyOf ui.grid.grouping.api:GridOptions
18816 * @description The string to use for the grouping header row label on rows which contain a null or undefined value in the grouped column.
18817 * <br/>Defaults to "Null"
18819 gridOptions.groupingNullLabel = typeof(gridOptions.groupingNullLabel) === 'undefined' ? 'Null' : gridOptions.groupingNullLabel;
18823 * @name enableGroupHeaderSelection
18824 * @propertyOf ui.grid.grouping.api:GridOptions
18825 * @description Allows group header rows to be selected.
18826 * <br/>Defaults to false
18828 gridOptions.enableGroupHeaderSelection = gridOptions.enableGroupHeaderSelection === true;
18834 * @name groupingColumnBuilder
18835 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18836 * @description Sets the grouping defaults based on the columnDefs
18838 * @param {object} colDef columnDef we're basing on
18839 * @param {GridCol} col the column we're to update
18840 * @param {object} gridOptions the options we should use
18841 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
18843 groupingColumnBuilder: function (colDef, col, gridOptions) {
18846 * @name ui.grid.grouping.api:ColumnDef
18848 * @description ColumnDef for grouping feature, these are available to be
18849 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
18854 * @name enableGrouping
18855 * @propertyOf ui.grid.grouping.api:ColumnDef
18856 * @description Enable grouping on this column
18857 * <br/>Defaults to true.
18859 if (colDef.enableGrouping === false){
18866 * @propertyOf ui.grid.grouping.api:ColumnDef
18867 * @description Set the grouping for a column. Format is:
18870 * groupPriority: <number, starts at 0, if less than 0 or undefined then we're aggregating in this column>
18874 * **Note that aggregation used to be included in grouping, but is now separately set on the column via treeAggregation
18875 * setting in treeBase**
18877 * We group in the priority order given, this will also put these columns to the high order of the sort irrespective
18878 * of the sort priority given them. If there is no sort defined then we sort ascending, if there is a sort defined then
18879 * we use that sort.
18881 * If the groupPriority is undefined or less than 0, then we expect to be aggregating, and we look at the
18882 * aggregation types to determine what sort of aggregation we can do. Values are in the constants file, but
18883 * include SUM, COUNT, MAX, MIN
18885 * groupPriorities should generally be sequential, if they're not then the next time getGrouping is called
18886 * we'll renumber them to be sequential.
18887 * <br/>Defaults to undefined.
18890 if ( typeof(col.grouping) === 'undefined' && typeof(colDef.grouping) !== 'undefined') {
18891 col.grouping = angular.copy(colDef.grouping);
18892 if ( typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority > -1 ){
18893 col.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
18894 col.treeAggregationFinalizerFn = service.groupedFinalizerFn;
18896 } else if (typeof(col.grouping) === 'undefined'){
18900 if (typeof(col.grouping) !== 'undefined' && typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority >= 0){
18901 col.suppressRemoveSort = true;
18904 var groupColumn = {
18905 name: 'ui.grid.grouping.group',
18906 title: i18nService.get().grouping.group,
18907 icon: 'ui-grid-icon-indent-right',
18908 shown: function () {
18909 return typeof(this.context.col.grouping) === 'undefined' ||
18910 typeof(this.context.col.grouping.groupPriority) === 'undefined' ||
18911 this.context.col.grouping.groupPriority < 0;
18913 action: function () {
18914 service.groupColumn( this.context.col.grid, this.context.col );
18918 var ungroupColumn = {
18919 name: 'ui.grid.grouping.ungroup',
18920 title: i18nService.get().grouping.ungroup,
18921 icon: 'ui-grid-icon-indent-left',
18922 shown: function () {
18923 return typeof(this.context.col.grouping) !== 'undefined' &&
18924 typeof(this.context.col.grouping.groupPriority) !== 'undefined' &&
18925 this.context.col.grouping.groupPriority >= 0;
18927 action: function () {
18928 service.ungroupColumn( this.context.col.grid, this.context.col );
18932 var aggregateRemove = {
18933 name: 'ui.grid.grouping.aggregateRemove',
18934 title: i18nService.get().grouping.aggregate_remove,
18935 shown: function () {
18936 return typeof(this.context.col.treeAggregationFn) !== 'undefined';
18938 action: function () {
18939 service.aggregateColumn( this.context.col.grid, this.context.col, null);
18943 // generic adder for the aggregation menus, which follow a pattern
18944 var addAggregationMenu = function(type, title){
18945 title = title || i18nService.get().grouping['aggregate_' + type] || type;
18947 name: 'ui.grid.grouping.aggregate' + type,
18949 shown: function () {
18950 return typeof(this.context.col.treeAggregation) === 'undefined' ||
18951 typeof(this.context.col.treeAggregation.type) === 'undefined' ||
18952 this.context.col.treeAggregation.type !== type;
18954 action: function () {
18955 service.aggregateColumn( this.context.col.grid, this.context.col, type);
18959 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregate' + type)) {
18960 col.menuItems.push(menuItem);
18966 * @name groupingShowGroupingMenu
18967 * @propertyOf ui.grid.grouping.api:ColumnDef
18968 * @description Show the grouping (group and ungroup items) menu on this column
18969 * <br/>Defaults to true.
18971 if ( col.colDef.groupingShowGroupingMenu !== false ){
18972 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.group')) {
18973 col.menuItems.push(groupColumn);
18976 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.ungroup')) {
18977 col.menuItems.push(ungroupColumn);
18984 * @name groupingShowAggregationMenu
18985 * @propertyOf ui.grid.grouping.api:ColumnDef
18986 * @description Show the aggregation menu on this column
18987 * <br/>Defaults to true.
18989 if ( col.colDef.groupingShowAggregationMenu !== false ){
18990 angular.forEach(uiGridTreeBaseService.nativeAggregations(), function(aggregationDef, name){
18991 addAggregationMenu(name);
18993 angular.forEach(gridOptions.treeCustomAggregations, function(aggregationDef, name){
18994 addAggregationMenu(name, aggregationDef.menuTitle);
18997 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregateRemove')) {
18998 col.menuItems.push(aggregateRemove);
19008 * @name groupingColumnProcessor
19009 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19010 * @description Moves the columns around based on which are grouped
19012 * @param {array} columns the columns to consider rendering
19013 * @param {array} rows the grid rows, which we don't use but are passed to us
19014 * @returns {array} updated columns array
19016 groupingColumnProcessor: function( columns, rows ) {
19019 columns = service.moveGroupColumns(this, columns, rows);
19025 * @name groupedFinalizerFn
19026 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19027 * @description Used on group columns to display the rendered value and optionally
19028 * display the count of rows.
19030 * @param {aggregation} the aggregation entity for a grouped column
19032 groupedFinalizerFn: function( aggregation ){
19035 if ( typeof(aggregation.groupVal) !== 'undefined') {
19036 aggregation.rendered = aggregation.groupVal;
19037 if ( col.grid.options.groupingShowCounts && col.colDef.type !== 'date' ){
19038 aggregation.rendered += (' (' + aggregation.value + ')');
19041 aggregation.rendered = null;
19047 * @name moveGroupColumns
19048 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19049 * @description Moves the column order so that the grouped columns are lined up
19050 * to the left (well, unless you're RTL, then it's the right). By doing this in
19051 * the columnsProcessor, we make it transient - when the column is ungrouped it'll
19052 * go back to where it was.
19054 * Does nothing if the option `moveGroupColumns` is set to false.
19056 * @param {Grid} grid grid object
19057 * @param {array} columns the columns that we should process/move
19058 * @param {array} rows the grid rows
19059 * @returns {array} updated columns
19061 moveGroupColumns: function( grid, columns, rows ){
19062 if ( grid.options.moveGroupColumns === false){
19066 columns.forEach( function(column, index){
19067 // position used to make stable sort in moveGroupColumns
19068 column.groupingPosition = index;
19071 columns.sort(function(a, b){
19072 var a_group, b_group;
19073 if (a.isRowHeader){
19076 else if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){
19079 a_group = a.grouping.groupPriority;
19082 if (b.isRowHeader){
19085 else if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){
19088 b_group = b.grouping.groupPriority;
19091 // groups get sorted to the top
19092 if ( a_group !== null && b_group === null) { return -1; }
19093 if ( b_group !== null && a_group === null) { return 1; }
19094 if ( a_group !== null && b_group !== null) {return a_group - b_group; }
19096 return a.groupingPosition - b.groupingPosition;
19099 columns.forEach( function(column, index) {
19100 delete column.groupingPosition;
19109 * @name groupColumn
19110 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19111 * @description Adds this column to the existing grouping, at the end of the priority order.
19112 * If the column doesn't have a sort, adds one, by default ASC
19114 * This column will move to the left of any non-group columns, the
19115 * move is handled in a columnProcessor, so gets called as part of refresh
19117 * @param {Grid} grid grid object
19118 * @param {GridCol} column the column we want to group
19120 groupColumn: function( grid, column){
19121 if ( typeof(column.grouping) === 'undefined' ){
19122 column.grouping = {};
19125 // set the group priority to the next number in the hierarchy
19126 var existingGrouping = service.getGrouping( grid );
19127 column.grouping.groupPriority = existingGrouping.grouping.length;
19129 // add sort if not present
19130 if ( !column.sort ){
19131 column.sort = { direction: uiGridConstants.ASC };
19132 } else if ( typeof(column.sort.direction) === 'undefined' || column.sort.direction === null ){
19133 column.sort.direction = uiGridConstants.ASC;
19136 column.treeAggregation = { type: uiGridGroupingConstants.aggregation.COUNT, source: 'grouping' };
19137 column.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
19138 column.treeAggregationFinalizerFn = service.groupedFinalizerFn;
19140 grid.api.grouping.raise.groupingChanged(column);
19141 // This indirectly calls service.tidyPriorities( grid );
19142 grid.api.core.raise.sortChanged(grid, grid.getColumnSorting());
19144 grid.queueGridRefresh();
19150 * @name ungroupColumn
19151 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19152 * @description Removes the groupPriority from this column. If the
19153 * column was previously aggregated the aggregation will come back.
19154 * The sort will remain.
19156 * This column will move to the right of any other group columns, the
19157 * move is handled in a columnProcessor, so gets called as part of refresh
19159 * @param {Grid} grid grid object
19160 * @param {GridCol} column the column we want to ungroup
19162 ungroupColumn: function( grid, column){
19163 if ( typeof(column.grouping) === 'undefined' ){
19167 delete column.grouping.groupPriority;
19168 delete column.treeAggregation;
19169 delete column.customTreeAggregationFinalizer;
19171 service.tidyPriorities( grid );
19173 grid.api.grouping.raise.groupingChanged(column);
19175 grid.queueGridRefresh();
19180 * @name aggregateColumn
19181 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19182 * @description Sets the aggregation type on a column, if the
19183 * column is currently grouped then it removes the grouping first.
19185 * @param {Grid} grid grid object
19186 * @param {GridCol} column the column we want to aggregate
19187 * @param {string} one of the recognised types from uiGridGroupingConstants or one of the custom aggregations from gridOptions
19189 aggregateColumn: function( grid, column, aggregationType){
19191 if (typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
19192 service.ungroupColumn( grid, column );
19195 var aggregationDef = {};
19196 if ( typeof(grid.options.treeCustomAggregations[aggregationType]) !== 'undefined' ){
19197 aggregationDef = grid.options.treeCustomAggregations[aggregationType];
19198 } else if ( typeof(uiGridTreeBaseService.nativeAggregations()[aggregationType]) !== 'undefined' ){
19199 aggregationDef = uiGridTreeBaseService.nativeAggregations()[aggregationType];
19202 column.treeAggregation = { type: aggregationType, label: i18nService.get().aggregation[aggregationDef.label] || aggregationDef.label };
19203 column.treeAggregationFn = aggregationDef.aggregationFn;
19204 column.treeAggregationFinalizerFn = aggregationDef.finalizerFn;
19206 grid.api.grouping.raise.aggregationChanged(column);
19208 grid.queueGridRefresh();
19214 * @name setGrouping
19215 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19216 * @description Set the grouping based on a config object, used by the save state feature
19217 * (more specifically, by the restore function in that feature )
19219 * @param {Grid} grid grid object
19220 * @param {object} config the config we want to set, same format as that returned by getGrouping
19222 setGrouping: function ( grid, config ){
19223 if ( typeof(config) === 'undefined' ){
19227 // first remove any existing grouping
19228 service.clearGrouping(grid);
19230 if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){
19231 config.grouping.forEach( function( group ) {
19232 var col = grid.getColumn(group.colName);
19235 service.groupColumn( grid, col );
19240 if ( config.aggregations && config.aggregations.length ){
19241 config.aggregations.forEach( function( aggregation ) {
19242 var col = grid.getColumn(aggregation.colName);
19245 service.aggregateColumn( grid, col, aggregation.aggregation.type );
19250 if ( config.rowExpandedStates ){
19251 service.applyRowExpandedStates( grid.grouping.groupingHeaderCache, config.rowExpandedStates );
19258 * @name clearGrouping
19259 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19260 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
19261 * as we don't know whether that sorting was added by grouping or was there beforehand
19263 * @param {Grid} grid grid object
19265 clearGrouping: function( grid ) {
19266 var currentGrouping = service.getGrouping(grid);
19268 if ( currentGrouping.grouping.length > 0 ){
19269 currentGrouping.grouping.forEach( function( group ) {
19271 // should have a group.colName if there's no col
19272 group.col = grid.getColumn(group.colName);
19274 service.ungroupColumn(grid, group.col);
19278 if ( currentGrouping.aggregations.length > 0 ){
19279 currentGrouping.aggregations.forEach( function( aggregation ){
19280 if (!aggregation.col){
19281 // should have a group.colName if there's no col
19282 aggregation.col = grid.getColumn(aggregation.colName);
19284 service.aggregateColumn(grid, aggregation.col, null);
19292 * @name tidyPriorities
19293 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19294 * @description Renumbers groupPriority and sortPriority such that
19295 * groupPriority is contiguous, and sortPriority either matches
19296 * groupPriority (for group columns), and otherwise is contiguous and
19297 * higher than groupPriority.
19299 * @param {Grid} grid grid object
19301 tidyPriorities: function( grid ){
19302 // if we're called from sortChanged, grid is in this, not passed as param, the param can be a column or undefined
19303 if ( ( typeof(grid) === 'undefined' || typeof(grid.grid) !== 'undefined' ) && typeof(this.grid) !== 'undefined' ) {
19307 var groupArray = [];
19308 var sortArray = [];
19310 grid.columns.forEach( function(column, index){
19311 if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
19312 groupArray.push(column);
19313 } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0){
19314 sortArray.push(column);
19318 groupArray.sort(function(a, b){ return a.grouping.groupPriority - b.grouping.groupPriority; });
19319 groupArray.forEach( function(column, index){
19320 column.grouping.groupPriority = index;
19321 column.suppressRemoveSort = true;
19322 if ( typeof(column.sort) === 'undefined'){
19325 column.sort.priority = index;
19328 var i = groupArray.length;
19329 sortArray.sort(function(a, b){ return a.sort.priority - b.sort.priority; });
19330 sortArray.forEach( function(column, index){
19331 column.sort.priority = i;
19332 column.suppressRemoveSort = column.colDef.suppressRemoveSort;
19341 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19342 * @description The rowProcessor that creates the groupHeaders (i.e. does
19343 * the actual grouping).
19345 * Assumes it is always called after the sorting processor, guaranteed by the priority setting
19347 * Processes all the rows in order, inserting a groupHeader row whenever there is a change
19348 * in value of a grouped row, based on the sortAlgorithm used for the column. The group header row
19349 * is looked up in the groupHeaderCache, and used from there if there is one. The entity is reset
19350 * to {} if one is found.
19352 * As it processes it maintains a `processingState` array. This records, for each level of grouping we're
19353 * working with, the following information:
19358 * initialised: boolean,
19359 * currentValue: value,
19360 * currentRow: gridRow,
19363 * We look for changes in the currentValue at any of the levels. Where we find a change we:
19365 * - create a new groupHeader row in the array
19367 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
19368 * @returns {array} the updated rows, including our new group rows
19370 groupRows: function( renderableRows ) {
19371 if (renderableRows.length === 0){
19372 return renderableRows;
19376 grid.grouping.oldGroupingHeaderCache = grid.grouping.groupingHeaderCache || {};
19377 grid.grouping.groupingHeaderCache = {};
19379 var processingState = service.initialiseProcessingState( grid );
19381 // processes each of the fields we are grouping by, checks if the value has changed and inserts a groupHeader
19382 // Broken out as shouldn't create functions in a loop.
19383 var updateProcessingState = function( groupFieldState, stateIndex ) {
19384 var fieldValue = grid.getCellValue(row, groupFieldState.col);
19386 // look for change of value - and insert a header
19387 if ( !groupFieldState.initialised || rowSorter.getSortFn(grid, groupFieldState.col, renderableRows)(fieldValue, groupFieldState.currentValue) !== 0 ){
19388 service.insertGroupHeader( grid, renderableRows, i, processingState, stateIndex );
19393 // use a for loop because it's tolerant of the array length changing whilst we go - we can
19394 // manipulate the iterator when we insert groupHeader rows
19395 for (var i = 0; i < renderableRows.length; i++ ){
19396 var row = renderableRows[i];
19398 if ( row.visible ){
19399 processingState.forEach( updateProcessingState );
19403 delete grid.grouping.oldGroupingHeaderCache;
19404 return renderableRows;
19410 * @name initialiseProcessingState
19411 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19412 * @description Creates the processing state array that is used
19415 * @param {Grid} grid grid object
19416 * @returns {array} an array in the format described in the groupRows method,
19417 * initialised with blank values
19419 initialiseProcessingState: function( grid ){
19420 var processingState = [];
19421 var columnSettings = service.getGrouping( grid );
19423 columnSettings.grouping.forEach( function( groupItem, index){
19424 processingState.push({
19425 fieldName: groupItem.field,
19426 col: groupItem.col,
19427 initialised: false,
19428 currentValue: null,
19433 return processingState;
19439 * @name getGrouping
19440 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19441 * @description Get the grouping settings from the columns. As a side effect
19442 * this always renumbers the grouping starting at 0
19443 * @param {Grid} grid grid object
19444 * @returns {array} an array of the group fields, in order of priority
19446 getGrouping: function( grid ){
19447 var groupArray = [];
19448 var aggregateArray = [];
19450 // get all the grouping
19451 grid.columns.forEach( function(column, columnIndex){
19452 if ( column.grouping ){
19453 if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
19454 groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping });
19457 if ( column.treeAggregation && column.treeAggregation.type ){
19458 aggregateArray.push({ field: column.field, col: column, aggregation: column.treeAggregation });
19462 // sort grouping into priority order
19463 groupArray.sort( function(a, b){
19464 return a.groupPriority - b.groupPriority;
19467 // renumber the priority in case it was somewhat messed up, then remove the grouping reference
19468 groupArray.forEach( function( group, index) {
19469 group.grouping.groupPriority = index;
19470 group.groupPriority = index;
19471 delete group.grouping;
19474 return { grouping: groupArray, aggregations: aggregateArray };
19480 * @name insertGroupHeader
19481 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19482 * @description Create a group header row, and link it to the various configuration
19483 * items that we use.
19485 * Look for the row in the oldGroupingHeaderCache, write the row into the new groupingHeaderCache.
19487 * @param {Grid} grid grid object
19488 * @param {array} renderableRows the rows that we are processing
19489 * @param {number} rowIndex the row we were up to processing
19490 * @param {array} processingState the current processing state
19491 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header -
19492 * i.e. the column that we want to create a header for
19494 insertGroupHeader: function( grid, renderableRows, rowIndex, processingState, stateIndex ) {
19495 // set the value that caused the end of a group into the header row and the processing state
19496 var fieldName = processingState[stateIndex].fieldName;
19497 var col = processingState[stateIndex].col;
19499 var newValue = grid.getCellValue(renderableRows[rowIndex], col);
19500 var newDisplayValue = newValue;
19501 if ( typeof(newValue) === 'undefined' || newValue === null ) {
19502 newDisplayValue = grid.options.groupingNullLabel;
19505 var getKeyAsValueForCacheMap = function(key) {
19506 if (angular.isObject(key)) {
19507 return JSON.stringify(key);
19513 var cacheItem = grid.grouping.oldGroupingHeaderCache;
19514 for ( var i = 0; i < stateIndex; i++ ){
19515 if ( cacheItem && cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)] ){
19516 cacheItem = cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)].children;
19521 if ( cacheItem && cacheItem[getKeyAsValueForCacheMap(newValue)]){
19522 headerRow = cacheItem[getKeyAsValueForCacheMap(newValue)].row;
19523 headerRow.entity = {};
19525 headerRow = new GridRow( {}, null, grid );
19526 gridClassFactory.rowTemplateAssigner.call(grid, headerRow);
19529 headerRow.entity['$$' + processingState[stateIndex].col.uid] = { groupVal: newDisplayValue };
19530 headerRow.treeLevel = stateIndex;
19531 headerRow.groupHeader = true;
19532 headerRow.internalRow = true;
19533 headerRow.enableCellEdit = false;
19534 headerRow.enableSelection = grid.options.enableGroupHeaderSelection;
19535 processingState[stateIndex].initialised = true;
19536 processingState[stateIndex].currentValue = newValue;
19537 processingState[stateIndex].currentRow = headerRow;
19539 // set all processing states below this one to not be initialised - change of this state
19540 // means all those need to start again
19541 service.finaliseProcessingState( processingState, stateIndex + 1);
19543 // insert our new header row
19544 renderableRows.splice(rowIndex, 0, headerRow);
19546 // add our new header row to the cache
19547 cacheItem = grid.grouping.groupingHeaderCache;
19548 for ( i = 0; i < stateIndex; i++ ){
19549 cacheItem = cacheItem[getKeyAsValueForCacheMap(processingState[i].currentValue)].children;
19551 cacheItem[getKeyAsValueForCacheMap(newValue)] = { row: headerRow, children: {} };
19557 * @name finaliseProcessingState
19558 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19559 * @description Set all processing states lower than the one that had a break in value to
19560 * no longer be initialised. Render the counts into the entity ready for display.
19562 * @param {Grid} grid grid object
19563 * @param {array} processingState the current processing state
19564 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header, all
19565 * processing states after this need to be finalised
19567 finaliseProcessingState: function( processingState, stateIndex ){
19568 for ( var i = stateIndex; i < processingState.length; i++){
19569 processingState[i].initialised = false;
19570 processingState[i].currentRow = null;
19571 processingState[i].currentValue = null;
19578 * @name getRowExpandedStates
19579 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19580 * @description Extract the groupHeaderCache hash, pulling out only the states.
19582 * The example below shows a grid that is grouped by gender then age
19587 * state: 'expanded',
19589 * 22: { state: 'expanded' },
19590 * 30: { state: 'collapsed' }
19594 * state: 'expanded',
19596 * 28: { state: 'expanded' },
19597 * 55: { state: 'collapsed' }
19603 * @param {Grid} grid grid object
19604 * @returns {hash} the expanded states as a hash
19606 getRowExpandedStates: function(treeChildren){
19607 if ( typeof(treeChildren) === 'undefined' ){
19611 var newChildren = {};
19613 angular.forEach( treeChildren, function( value, key ){
19614 newChildren[key] = { state: value.row.treeNode.state };
19615 if ( value.children ){
19616 newChildren[key].children = service.getRowExpandedStates( value.children );
19618 newChildren[key].children = {};
19622 return newChildren;
19628 * @name applyRowExpandedStates
19629 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19630 * @description Take a hash in the format as created by getRowExpandedStates,
19631 * and apply it to the grid.grouping.groupHeaderCache.
19633 * Takes a treeSubset, and applies to a treeSubset - so can be called
19636 * @param {object} currentNode can be grid.grouping.groupHeaderCache, or any of
19637 * the children of that hash
19638 * @returns {hash} expandedStates can be the full expanded states, or children
19639 * of that expanded states (which hopefully matches the subset of the groupHeaderCache)
19641 applyRowExpandedStates: function( currentNode, expandedStates ){
19642 if ( typeof(expandedStates) === 'undefined' ){
19646 angular.forEach(expandedStates, function( value, key ) {
19647 if ( currentNode[key] ){
19648 currentNode[key].row.treeNode.state = value.state;
19650 if (value.children && currentNode[key].children){
19651 service.applyRowExpandedStates( currentNode[key].children, value.children );
19667 * @name ui.grid.grouping.directive:uiGridGrouping
19671 * @description Adds grouping features to grid
19674 <example module="app">
19675 <file name="app.js">
19676 var app = angular.module('app', ['ui.grid', 'ui.grid.grouping']);
19678 app.controller('MainCtrl', ['$scope', function ($scope) {
19680 { name: 'Bob', title: 'CEO' },
19681 { name: 'Frank', title: 'Lowly Developer' }
19684 $scope.columnDefs = [
19685 {name: 'name', enableCellEdit: true},
19686 {name: 'title', enableCellEdit: true}
19689 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
19692 <file name="index.html">
19693 <div ng-controller="MainCtrl">
19694 <div ui-grid="gridOptions" ui-grid-grouping></div>
19699 module.directive('uiGridGrouping', ['uiGridGroupingConstants', 'uiGridGroupingService', '$templateCache',
19700 function (uiGridGroupingConstants, uiGridGroupingService, $templateCache) {
19704 require: '^uiGrid',
19706 compile: function () {
19708 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
19709 if (uiGridCtrl.grid.options.enableGrouping !== false){
19710 uiGridGroupingService.initializeGrid(uiGridCtrl.grid, $scope);
19713 post: function ($scope, $elm, $attrs, uiGridCtrl) {
19727 * @name ui.grid.importer
19730 * # ui.grid.importer
19732 * <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>
19734 * This module provides the ability to import data into the grid. It
19735 * uses the column defs to work out which data belongs in which column,
19736 * and creates entities from a configured class (typically a $resource).
19738 * If the rowEdit feature is enabled, it also calls save on those newly
19739 * created objects, and then displays any errors in the imported data.
19741 * Currently the importer imports only CSV and json files, although provision has been
19742 * made to process other file formats, and these can be added over time.
19744 * For json files, the properties within each object in the json must match the column names
19745 * (to put it another way, the importer doesn't process the json, it just copies the objects
19746 * within the json into a new instance of the specified object type)
19748 * For CSV import, the default column identification relies on each column in the
19749 * header row matching a column.name or column.displayName. Optionally, a column identification
19750 * callback can be used. This allows matching using other attributes, which is particularly
19751 * useful if your application has internationalised column headings (i.e. the headings that
19752 * the user sees don't match the column names).
19754 * The importer makes use of the grid menu as the UI for requesting an
19757 * <div ui-grid-importer></div>
19760 var module = angular.module('ui.grid.importer', ['ui.grid']);
19764 * @name ui.grid.importer.constant:uiGridImporterConstants
19766 * @description constants available in importer module
19769 module.constant('uiGridImporterConstants', {
19770 featureName: 'importer'
19775 * @name ui.grid.importer.service:uiGridImporterService
19777 * @description Services for importer feature
19779 module.service('uiGridImporterService', ['$q', 'uiGridConstants', 'uiGridImporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService', '$window',
19780 function ($q, uiGridConstants, uiGridImporterConstants, gridUtil, $compile, $interval, i18nService, $window) {
19784 initializeGrid: function ($scope, grid) {
19786 //add feature namespace and any properties to grid for needed state
19791 this.defaultGridOptions(grid.options);
19795 * @name ui.grid.importer.api:PublicApi
19797 * @description Public Api for importer feature
19809 * @methodOf ui.grid.importer.api:PublicApi
19810 * @description Imports a file into the grid using the file object
19811 * provided. Bypasses the grid menu
19812 * @param {File} fileObject the file we want to import, as a javascript
19815 importFile: function ( fileObject ) {
19816 service.importThisFile( grid, fileObject );
19822 grid.api.registerEventsFromObject(publicApi.events);
19824 grid.api.registerMethodsFromObject(publicApi.methods);
19826 if ( grid.options.enableImporter && grid.options.importerShowMenu ){
19827 if ( grid.api.core.addToGridMenu ){
19828 service.addToMenu( grid );
19830 // order of registration is not guaranteed, register in a little while
19831 $interval( function() {
19832 if (grid.api.core.addToGridMenu){
19833 service.addToMenu( grid );
19841 defaultGridOptions: function (gridOptions) {
19842 //default option to true unless it was explicitly set to false
19845 * @name ui.grid.importer.api:GridOptions
19847 * @description GridOptions for importer feature, these are available to be
19848 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
19853 * @propertyOf ui.grid.importer.api:GridOptions
19854 * @name enableImporter
19855 * @description Whether or not importer is enabled. Automatically set
19856 * to false if the user's browser does not support the required fileApi.
19857 * Otherwise defaults to true.
19860 if (gridOptions.enableImporter || gridOptions.enableImporter === undefined) {
19861 if ( !($window.hasOwnProperty('File') && $window.hasOwnProperty('FileReader') && $window.hasOwnProperty('FileList') && $window.hasOwnProperty('Blob')) ) {
19862 gridUtil.logError('The File APIs are not fully supported in this browser, grid importer cannot be used.');
19863 gridOptions.enableImporter = false;
19865 gridOptions.enableImporter = true;
19868 gridOptions.enableImporter = false;
19873 * @name importerProcessHeaders
19874 * @methodOf ui.grid.importer.api:GridOptions
19875 * @description A callback function that will process headers using custom
19876 * logic. Set this callback function if the headers that your user will provide in their
19877 * import file don't necessarily match the grid header or field names. This might commonly
19878 * occur where your application is internationalised, and therefore the field names
19879 * that the user recognises are in a different language than the field names that
19880 * ui-grid knows about.
19882 * Defaults to the internal `processHeaders` method, which seeks to match using both
19883 * displayName and column.name. Any non-matching columns are discarded.
19885 * Your callback routine should respond by processing the header array, and returning an array
19886 * of matching column names. A null value in any given position means "don't import this column"
19889 * gridOptions.importerProcessHeaders: function( headerArray ) {
19890 * var myHeaderColumns = [];
19892 * headerArray.forEach( function( value, index ) {
19893 * thisCol = mySpecialLookupFunction( value );
19894 * myHeaderColumns.push( thisCol.name );
19897 * return myHeaderCols;
19900 * @param {Grid} grid the grid we're importing into
19901 * @param {array} headerArray an array of the text from the first row of the csv file,
19902 * which you need to match to column.names
19903 * @returns {array} array of matching column names, in the same order as the headerArray
19906 gridOptions.importerProcessHeaders = gridOptions.importerProcessHeaders || service.processHeaders;
19910 * @name importerHeaderFilter
19911 * @methodOf ui.grid.importer.api:GridOptions
19912 * @description A callback function that will filter (usually translate) a single
19913 * header. Used when you want to match the passed in column names to the column
19914 * displayName after the header filter.
19916 * Your callback routine needs to return the filtered header value.
19918 * gridOptions.importerHeaderFilter: function( displayName ) {
19919 * return $translate.instant( displayName );
19925 * gridOptions.importerHeaderFilter: $translate.instant
19927 * @param {string} displayName the displayName that we'd like to translate
19928 * @returns {string} the translated name
19931 gridOptions.importerHeaderFilter = gridOptions.importerHeaderFilter || function( displayName ) { return displayName; };
19935 * @name importerErrorCallback
19936 * @methodOf ui.grid.importer.api:GridOptions
19937 * @description A callback function that provides custom error handling, rather
19938 * than the standard grid behaviour of an alert box and a console message. You
19939 * might use this to internationalise the console log messages, or to write to a
19940 * custom logging routine that returned errors to the server.
19943 * gridOptions.importerErrorCallback: function( grid, errorKey, consoleMessage, context ) {
19944 * myUserDisplayRoutine( errorKey );
19945 * myLoggingRoutine( consoleMessage, context );
19948 * @param {Grid} grid the grid we're importing into, may be useful if you're positioning messages
19950 * @param {string} errorKey one of the i18n keys the importer can return - importer.noHeaders,
19951 * importer.noObjects, importer.invalidCsv, importer.invalidJson, importer.jsonNotArray
19952 * @param {string} consoleMessage the English console message that importer would have written
19953 * @param {object} context the context data that importer would have appended to that console message,
19954 * often the file content itself or the element that is in error
19957 if ( !gridOptions.importerErrorCallback || typeof(gridOptions.importerErrorCallback) !== 'function' ){
19958 delete gridOptions.importerErrorCallback;
19963 * @name importerDataAddCallback
19964 * @methodOf ui.grid.importer.api:GridOptions
19965 * @description A mandatory callback function that adds data to the source data array. The grid
19966 * generally doesn't add rows to the source data array, it is tidier to handle this through a user
19970 * gridOptions.importerDataAddCallback: function( grid, newObjects ) {
19971 * $scope.myData = $scope.myData.concat( newObjects );
19974 * @param {Grid} grid the grid we're importing into, may be useful in some way
19975 * @param {array} newObjects an array of new objects that you should add to your data
19978 if ( gridOptions.enableImporter === true && !gridOptions.importerDataAddCallback ) {
19979 gridUtil.logError("You have not set an importerDataAddCallback, importer is disabled");
19980 gridOptions.enableImporter = false;
19985 * @name importerNewObject
19986 * @propertyOf ui.grid.importer.api:GridOptions
19987 * @description An object on which we call `new` to create each new row before inserting it into
19988 * the data array. Typically this would be a $resource entity, which means that if you're using
19989 * the rowEdit feature, you can directly call save on this entity when the save event is triggered.
19991 * Defaults to a vanilla javascript object
19995 * gridOptions.importerNewObject = MyRes;
20002 * @propertyOf ui.grid.importer.api:GridOptions
20003 * @name importerShowMenu
20004 * @description Whether or not to show an item in the grid menu. Defaults to true.
20007 gridOptions.importerShowMenu = gridOptions.importerShowMenu !== false;
20011 * @methodOf ui.grid.importer.api:GridOptions
20012 * @name importerObjectCallback
20013 * @description A callback that massages the data for each object. For example,
20014 * you might have data stored as a code value, but display the decode. This callback
20015 * can be used to change the decoded value back into a code. Defaults to doing nothing.
20016 * @param {Grid} grid in case you need it
20017 * @param {object} newObject the new object as importer has created it, modify it
20018 * then return the modified version
20019 * @returns {object} the modified object
20022 * gridOptions.importerObjectCallback = function ( grid, newObject ) {
20023 * switch newObject.status {
20025 * newObject.status = 1;
20028 * newObject.status = 2;
20031 * return newObject;
20035 gridOptions.importerObjectCallback = gridOptions.importerObjectCallback || function( grid, newObject ) { return newObject; };
20042 * @methodOf ui.grid.importer.service:uiGridImporterService
20043 * @description Adds import menu item to the grid menu,
20044 * allowing the user to request import of a file
20045 * @param {Grid} grid the grid into which data should be imported
20047 addToMenu: function ( grid ) {
20048 grid.api.core.addToGridMenu( grid, [
20050 title: i18nService.getSafeText('gridMenu.importerTitle'),
20054 templateUrl: 'ui-grid/importerMenuItemContainer',
20055 action: function ($event) {
20056 this.grid.api.importer.importAFile( grid );
20066 * @name importThisFile
20067 * @methodOf ui.grid.importer.service:uiGridImporterService
20068 * @description Imports the provided file into the grid using the file object
20069 * provided. Bypasses the grid menu
20070 * @param {Grid} grid the grid we're importing into
20071 * @param {File} fileObject the file we want to import, as returned from the File
20072 * javascript object
20074 importThisFile: function ( grid, fileObject ) {
20076 gridUtil.logError( 'No file object provided to importThisFile, should be impossible, aborting');
20080 var reader = new FileReader();
20082 switch ( fileObject.type ){
20083 case 'application/json':
20084 reader.onload = service.importJsonClosure( grid );
20087 reader.onload = service.importCsvClosure( grid );
20091 reader.readAsText( fileObject );
20098 * @methodOf ui.grid.importer.service:uiGridImporterService
20099 * @description Creates a function that imports a json file into the grid.
20100 * The json data is imported into new objects of type `gridOptions.importerNewObject`,
20101 * and if the rowEdit feature is enabled the rows are marked as dirty
20102 * @param {Grid} grid the grid we want to import into
20103 * @param {FileObject} importFile the file that we want to import, as
20106 importJsonClosure: function( grid ) {
20107 return function( importFile ){
20108 var newObjects = [];
20111 var importArray = service.parseJson( grid, importFile );
20112 if (importArray === null){
20115 importArray.forEach( function( value, index ) {
20116 newObject = service.newObject( grid );
20117 angular.extend( newObject, value );
20118 newObject = grid.options.importerObjectCallback( grid, newObject );
20119 newObjects.push( newObject );
20122 service.addObjects( grid, newObjects );
20131 * @methodOf ui.grid.importer.service:uiGridImporterService
20132 * @description Parses a json file, returns the parsed data.
20133 * Displays an error if file doesn't parse
20134 * @param {Grid} grid the grid that we want to import into
20135 * @param {FileObject} importFile the file that we want to import, as
20137 * @returns {array} array of objects from the imported json
20139 parseJson: function( grid, importFile ){
20142 loadedObjects = JSON.parse( importFile.target.result );
20144 service.alertError( grid, 'importer.invalidJson', 'File could not be processed, is it valid json? Content was: ', importFile.target.result );
20148 if ( !Array.isArray( loadedObjects ) ){
20149 service.alertError( grid, 'importer.jsonNotarray', 'Import failed, file is not an array, file was: ', importFile.target.result );
20152 return loadedObjects;
20160 * @name importCsvClosure
20161 * @methodOf ui.grid.importer.service:uiGridImporterService
20162 * @description Creates a function that imports a csv file into the grid
20163 * (allowing it to be used in the reader.onload event)
20164 * @param {Grid} grid the grid that we want to import into
20165 * @param {FileObject} importFile the file that we want to import, as
20168 importCsvClosure: function( grid ) {
20169 return function( importFile ){
20170 var importArray = service.parseCsv( importFile );
20171 if ( !importArray || importArray.length < 1 ){
20172 service.alertError( grid, 'importer.invalidCsv', 'File could not be processed, is it valid csv? Content was: ', importFile.target.result );
20176 var newObjects = service.createCsvObjects( grid, importArray );
20177 if ( !newObjects || newObjects.length === 0 ){
20178 service.alertError( grid, 'importer.noObjects', 'Objects were not able to be derived, content was: ', importFile.target.result );
20182 service.addObjects( grid, newObjects );
20190 * @methodOf ui.grid.importer.service:uiGridImporterService
20191 * @description Parses a csv file into an array of arrays, with the first
20192 * array being the headers, and the remaining arrays being the data.
20193 * The logic for this comes from https://github.com/thetalecrafter/excel.js/blob/master/src/csv.js,
20194 * which is noted as being under the MIT license. The code is modified to pass the jscs yoda condition
20196 * @param {FileObject} importFile the file that we want to import, as a
20199 parseCsv: function( importFile ) {
20200 var csv = importFile.target.result;
20202 // use the CSV-JS library to parse
20203 return CSV.parse(csv);
20209 * @name createCsvObjects
20210 * @methodOf ui.grid.importer.service:uiGridImporterService
20211 * @description Converts an array of arrays (representing the csv file)
20212 * into a set of objects. Uses the provided `gridOptions.importerNewObject`
20213 * to create the objects, and maps the header row into the individual columns
20214 * using either `gridOptions.importerProcessHeaders`, or by using a native method
20215 * of matching to either the displayName, column name or column field of
20216 * the columns in the column defs. The resulting objects will have attributes
20217 * that are named based on the column.field or column.name, in that order.
20218 * @param {Grid} grid the grid that we want to import into
20219 * @param {Array} importArray the data that we want to import, as an array
20221 createCsvObjects: function( grid, importArray ){
20222 // pull off header row and turn into headers
20223 var headerMapping = grid.options.importerProcessHeaders( grid, importArray.shift() );
20224 if ( !headerMapping || headerMapping.length === 0 ){
20225 service.alertError( grid, 'importer.noHeaders', 'Column names could not be derived, content was: ', importArray );
20229 var newObjects = [];
20231 importArray.forEach( function( row, index ) {
20232 newObject = service.newObject( grid );
20233 if ( row !== null ){
20234 row.forEach( function( field, index ){
20235 if ( headerMapping[index] !== null ){
20236 newObject[ headerMapping[index] ] = field;
20240 newObject = grid.options.importerObjectCallback( grid, newObject );
20241 newObjects.push( newObject );
20250 * @name processHeaders
20251 * @methodOf ui.grid.importer.service:uiGridImporterService
20252 * @description Determines the columns that the header row from
20253 * a csv (or other) file represents.
20254 * @param {Grid} grid the grid we're importing into
20255 * @param {array} headerRow the header row that we wish to match against
20256 * the column definitions
20257 * @returns {array} an array of the attribute names that should be used
20258 * for that column, based on matching the headers or creating the headers
20261 processHeaders: function( grid, headerRow ) {
20263 if ( !grid.options.columnDefs || grid.options.columnDefs.length === 0 ){
20264 // we are going to create new columnDefs for all these columns, so just remove
20265 // spaces from the names to create fields
20266 headerRow.forEach( function( value, index ) {
20267 headers.push( value.replace( /[^0-9a-zA-Z\-_]/g, '_' ) );
20271 var lookupHash = service.flattenColumnDefs( grid, grid.options.columnDefs );
20272 headerRow.forEach( function( value, index ) {
20273 if ( lookupHash[value] ) {
20274 headers.push( lookupHash[value] );
20275 } else if ( lookupHash[ value.toLowerCase() ] ) {
20276 headers.push( lookupHash[ value.toLowerCase() ] );
20278 headers.push( null );
20287 * @name flattenColumnDefs
20288 * @methodOf ui.grid.importer.service:uiGridImporterService
20289 * @description Runs through the column defs and creates a hash of
20290 * the displayName, name and field, and of each of those values forced to lower case,
20291 * with each pointing to the field or name
20292 * (whichever is present). Used to lookup column headers and decide what
20293 * attribute name to give to the resulting field.
20294 * @param {Grid} grid the grid we're importing into
20295 * @param {array} columnDefs the columnDefs that we should flatten
20296 * @returns {hash} the flattened version of the column def information, allowing
20297 * us to look up a value by `flattenedHash[ headerValue ]`
20299 flattenColumnDefs: function( grid, columnDefs ){
20300 var flattenedHash = {};
20301 columnDefs.forEach( function( columnDef, index) {
20302 if ( columnDef.name ){
20303 flattenedHash[ columnDef.name ] = columnDef.field || columnDef.name;
20304 flattenedHash[ columnDef.name.toLowerCase() ] = columnDef.field || columnDef.name;
20307 if ( columnDef.field ){
20308 flattenedHash[ columnDef.field ] = columnDef.field || columnDef.name;
20309 flattenedHash[ columnDef.field.toLowerCase() ] = columnDef.field || columnDef.name;
20312 if ( columnDef.displayName ){
20313 flattenedHash[ columnDef.displayName ] = columnDef.field || columnDef.name;
20314 flattenedHash[ columnDef.displayName.toLowerCase() ] = columnDef.field || columnDef.name;
20317 if ( columnDef.displayName && grid.options.importerHeaderFilter ){
20318 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName) ] = columnDef.field || columnDef.name;
20319 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName).toLowerCase() ] = columnDef.field || columnDef.name;
20323 return flattenedHash;
20330 * @methodOf ui.grid.importer.service:uiGridImporterService
20331 * @description Inserts our new objects into the grid data, and
20332 * sets the rows to dirty if the rowEdit feature is being used
20334 * Does this by registering a watch on dataChanges, which essentially
20335 * is waiting on the result of the grid data watch, and downstream processing.
20337 * When the callback is called, it deregisters itself - we don't want to run
20338 * again next time data is added.
20340 * If we never get called, we deregister on destroy.
20342 * @param {Grid} grid the grid we're importing into
20343 * @param {array} newObjects the objects we want to insert into the grid data
20344 * @returns {object} the new object
20346 addObjects: function( grid, newObjects, $scope ){
20347 if ( grid.api.rowEdit ){
20348 var dataChangeDereg = grid.registerDataChangeCallback( function() {
20349 grid.api.rowEdit.setRowsDirty( newObjects );
20351 }, [uiGridConstants.dataChange.ROW] );
20353 grid.importer.$scope.$on( '$destroy', dataChangeDereg );
20356 grid.importer.$scope.$apply( grid.options.importerDataAddCallback( grid, newObjects ) );
20364 * @methodOf ui.grid.importer.service:uiGridImporterService
20365 * @description Makes a new object based on `gridOptions.importerNewObject`,
20366 * or based on an empty object if not present
20367 * @param {Grid} grid the grid we're importing into
20368 * @returns {object} the new object
20370 newObject: function( grid ){
20371 if ( typeof(grid.options) !== "undefined" && typeof(grid.options.importerNewObject) !== "undefined" ){
20372 return new grid.options.importerNewObject();
20382 * @methodOf ui.grid.importer.service:uiGridImporterService
20383 * @description Provides an internationalised user alert for the failure,
20384 * and logs a console message including diagnostic content.
20385 * Optionally, if the the `gridOptions.importerErrorCallback` routine
20386 * is defined, then calls that instead, allowing user specified error routines
20387 * @param {Grid} grid the grid we're importing into
20388 * @param {array} headerRow the header row that we wish to match against
20389 * the column definitions
20391 alertError: function( grid, alertI18nToken, consoleMessage, context ){
20392 if ( grid.options.importerErrorCallback ){
20393 grid.options.importerErrorCallback( grid, alertI18nToken, consoleMessage, context );
20395 $window.alert(i18nService.getSafeText( alertI18nToken ));
20396 gridUtil.logError(consoleMessage + context );
20408 * @name ui.grid.importer.directive:uiGridImporter
20412 * @description Adds importer features to grid
20415 module.directive('uiGridImporter', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
20416 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
20420 require: '^uiGrid',
20422 link: function ($scope, $elm, $attrs, uiGridCtrl) {
20423 uiGridImporterService.initializeGrid($scope, uiGridCtrl.grid);
20431 * @name ui.grid.importer.directive:uiGridImporterMenuItem
20435 * @description Handles the processing from the importer menu item - once a file is
20439 module.directive('uiGridImporterMenuItem', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
20440 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
20444 require: '^uiGrid',
20446 templateUrl: 'ui-grid/importerMenuItem',
20447 link: function ($scope, $elm, $attrs, uiGridCtrl) {
20448 var handleFileSelect = function( event ){
20449 var target = event.srcElement || event.target;
20451 if (target && target.files && target.files.length === 1) {
20452 var fileObject = target.files[0];
20453 uiGridImporterService.importThisFile( grid, fileObject );
20454 target.form.reset();
20458 var fileChooser = $elm[0].querySelectorAll('.ui-grid-importer-file-chooser');
20459 var grid = uiGridCtrl.grid;
20461 if ( fileChooser.length !== 1 ){
20462 gridUtil.logError('Found > 1 or < 1 file choosers within the menu item, error, cannot continue');
20464 fileChooser[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
20476 * @name ui.grid.infiniteScroll
20480 * #ui.grid.infiniteScroll
20482 * <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>
20484 * This module provides infinite scroll functionality to ui-grid
20487 var module = angular.module('ui.grid.infiniteScroll', ['ui.grid']);
20490 * @name ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20492 * @description Service for infinite scroll features
20494 module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', '$q', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent, $q) {
20500 * @name initializeGrid
20501 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20502 * @description This method register events and methods into grid public API
20505 initializeGrid: function(grid, $scope) {
20506 service.defaultGridOptions(grid.options);
20508 if (!grid.options.enableInfiniteScroll){
20512 grid.infiniteScroll = { dataLoading: false };
20513 service.setScrollDirections( grid, grid.options.infiniteScrollUp, grid.options.infiniteScrollDown );
20514 grid.api.core.on.scrollEnd($scope, service.handleScroll);
20518 * @name ui.grid.infiniteScroll.api:PublicAPI
20520 * @description Public API for infinite scroll feature
20528 * @name needLoadMoreData
20529 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
20530 * @description This event fires when scroll reaches bottom percentage of grid
20531 * and needs to load data
20534 needLoadMoreData: function ($scope, fn) {
20539 * @name needLoadMoreDataTop
20540 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
20541 * @description This event fires when scroll reaches top percentage of grid
20542 * and needs to load data
20545 needLoadMoreDataTop: function ($scope, fn) {
20555 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20556 * @description Call this function when you have loaded the additional data
20557 * requested. You should set scrollUp and scrollDown to indicate
20558 * whether there are still more pages in each direction.
20560 * If you call dataLoaded without first calling `saveScrollPercentage` then we will
20561 * scroll the user to the start of the newly loaded data, which usually gives a smooth scroll
20562 * experience, but can give a jumpy experience with large `infiniteScrollRowsFromEnd` values, and
20563 * on variable speed internet connections. Using `saveScrollPercentage` as demonstrated in the tutorial
20564 * should give a smoother scrolling experience for users.
20566 * See infinite_scroll tutorial for example of usage
20567 * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire
20568 * any more infinite scroll events upward
20569 * @param {boolean} scrollDown if set to false flags that there are no more pages downwards, so don't
20570 * fire any more infinite scroll events downward
20571 * @returns {promise} a promise that is resolved when the grid scrolling is fully adjusted. If you're
20572 * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning
20574 dataLoaded: function( scrollUp, scrollDown ) {
20575 service.setScrollDirections(grid, scrollUp, scrollDown);
20577 var promise = service.adjustScroll(grid).then(function() {
20578 grid.infiniteScroll.dataLoading = false;
20586 * @name resetScroll
20587 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20588 * @description Call this function when you have taken some action that makes the current
20589 * scroll position invalid. For example, if you're using external sorting and you've resorted
20590 * then you might reset the scroll, or if you've otherwise substantially changed the data, perhaps
20591 * you've reused an existing grid for a new data set
20593 * You must tell us whether there is data upwards or downwards after the reset
20595 * @param {boolean} scrollUp flag that there are pages upwards, fire
20596 * infinite scroll events upward
20597 * @param {boolean} scrollDown flag that there are pages downwards, so
20598 * fire infinite scroll events downward
20599 * @returns {promise} promise that is resolved when the scroll reset is complete
20601 resetScroll: function( scrollUp, scrollDown ) {
20602 service.setScrollDirections( grid, scrollUp, scrollDown);
20604 return service.adjustInfiniteScrollPosition(grid, 0);
20610 * @name saveScrollPercentage
20611 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20612 * @description Saves the scroll percentage and number of visible rows before you adjust the data,
20613 * used if you're subsequently going to call `dataRemovedTop` or `dataRemovedBottom`
20615 saveScrollPercentage: function() {
20616 grid.infiniteScroll.prevScrollTop = grid.renderContainers.body.prevScrollTop;
20617 grid.infiniteScroll.previousVisibleRows = grid.getVisibleRowCount();
20623 * @name dataRemovedTop
20624 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20625 * @description Adjusts the scroll position after you've removed data at the top
20626 * @param {boolean} scrollUp flag that there are pages upwards, fire
20627 * infinite scroll events upward
20628 * @param {boolean} scrollDown flag that there are pages downwards, so
20629 * fire infinite scroll events downward
20631 dataRemovedTop: function( scrollUp, scrollDown ) {
20632 service.dataRemovedTop( grid, scrollUp, scrollDown );
20637 * @name dataRemovedBottom
20638 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20639 * @description Adjusts the scroll position after you've removed data at the bottom
20640 * @param {boolean} scrollUp flag that there are pages upwards, fire
20641 * infinite scroll events upward
20642 * @param {boolean} scrollDown flag that there are pages downwards, so
20643 * fire infinite scroll events downward
20645 dataRemovedBottom: function( scrollUp, scrollDown ) {
20646 service.dataRemovedBottom( grid, scrollUp, scrollDown );
20651 * @name setScrollDirections
20652 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20653 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20654 * and also sets the grid.suppressParentScroll
20655 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20656 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20658 setScrollDirections: function ( scrollUp, scrollDown ) {
20659 service.setScrollDirections( grid, scrollUp, scrollDown );
20665 grid.api.registerEventsFromObject(publicApi.events);
20666 grid.api.registerMethodsFromObject(publicApi.methods);
20670 defaultGridOptions: function (gridOptions) {
20671 //default option to true unless it was explicitly set to false
20674 * @name ui.grid.infiniteScroll.api:GridOptions
20676 * @description GridOptions for infinite scroll feature, these are available to be
20677 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
20682 * @name enableInfiniteScroll
20683 * @propertyOf ui.grid.infiniteScroll.api:GridOptions
20684 * @description Enable infinite scrolling for this grid
20685 * <br/>Defaults to true
20687 gridOptions.enableInfiniteScroll = gridOptions.enableInfiniteScroll !== false;
20691 * @name infiniteScrollRowsFromEnd
20692 * @propertyOf ui.grid.class:GridOptions
20693 * @description This setting controls how close to the end of the dataset a user gets before
20694 * more data is requested by the infinite scroll, whether scrolling up or down. This allows you to
20695 * 'prefetch' rows before the user actually runs out of scrolling.
20697 * Note that if you set this value too high it may give jumpy scrolling behaviour, if you're getting
20698 * this behaviour you could use the `saveScrollPercentageMethod` right before loading your data, and we'll
20699 * preserve that scroll position
20701 * <br> Defaults to 20
20703 gridOptions.infiniteScrollRowsFromEnd = gridOptions.infiniteScrollRowsFromEnd || 20;
20707 * @name infiniteScrollUp
20708 * @propertyOf ui.grid.class:GridOptions
20709 * @description Whether you allow infinite scroll up, implying that the first page of data
20710 * you have displayed is in the middle of your data set. If set to true then we trigger the
20711 * needMoreDataTop event when the user hits the top of the scrollbar.
20712 * <br> Defaults to false
20714 gridOptions.infiniteScrollUp = gridOptions.infiniteScrollUp === true;
20718 * @name infiniteScrollDown
20719 * @propertyOf ui.grid.class:GridOptions
20720 * @description Whether you allow infinite scroll down, implying that the first page of data
20721 * you have displayed is in the middle of your data set. If set to true then we trigger the
20722 * needMoreData event when the user hits the bottom of the scrollbar.
20723 * <br> Defaults to true
20725 gridOptions.infiniteScrollDown = gridOptions.infiniteScrollDown !== false;
20731 * @name setScrollDirections
20732 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20733 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20734 * and also sets the grid.suppressParentScroll
20735 * @param {grid} grid the grid we're operating on
20736 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20737 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20739 setScrollDirections: function ( grid, scrollUp, scrollDown ) {
20740 grid.infiniteScroll.scrollUp = ( scrollUp === true );
20741 grid.suppressParentScrollUp = ( scrollUp === true );
20743 grid.infiniteScroll.scrollDown = ( scrollDown !== false);
20744 grid.suppressParentScrollDown = ( scrollDown !== false);
20750 * @name handleScroll
20751 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20752 * @description Called whenever the grid scrolls, determines whether the scroll should
20753 * trigger an infinite scroll request for more data
20754 * @param {object} args the args from the event
20756 handleScroll: function (args) {
20757 // don't request data if already waiting for data, or if source is coming from ui.grid.adjustInfiniteScrollPosition() function
20758 if ( args.grid.infiniteScroll && args.grid.infiniteScroll.dataLoading || args.source === 'ui.grid.adjustInfiniteScrollPosition' ){
20764 var targetPercentage = args.grid.options.infiniteScrollRowsFromEnd / args.grid.renderContainers.body.visibleRowCache.length;
20765 if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) {
20766 percentage = args.y.percentage;
20767 if (percentage <= targetPercentage){
20768 service.loadData(args.grid);
20770 } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) {
20771 percentage = 1 - args.y.percentage;
20772 if (percentage <= targetPercentage){
20773 service.loadData(args.grid);
20783 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20784 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20785 * and whether there are more pages upwards or downwards. It also stores the number of rows that we had previously,
20786 * and clears out any saved scroll position so that we know whether or not the user calls `saveScrollPercentage`
20787 * @param {Grid} grid the grid we're working on
20789 loadData: function (grid) {
20790 // save number of currently visible rows to calculate new scroll position later - we know that we want
20791 // to be at approximately the row we're currently at
20792 grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20793 grid.infiniteScroll.direction = grid.scrollDirection;
20794 delete grid.infiniteScroll.prevScrollTop;
20796 if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) {
20797 grid.infiniteScroll.dataLoading = true;
20798 grid.api.infiniteScroll.raise.needLoadMoreDataTop();
20799 } else if (grid.scrollDirection === uiGridConstants.scrollDirection.DOWN && grid.infiniteScroll.scrollDown ) {
20800 grid.infiniteScroll.dataLoading = true;
20801 grid.api.infiniteScroll.raise.needLoadMoreData();
20808 * @name adjustScroll
20809 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20810 * @description Once we are informed that data has been loaded, adjust the scroll position to account for that
20811 * addition and to make things look clean.
20813 * If we're scrolling up we scroll to the first row of the old data set -
20814 * so we're assuming that you would have gotten to the top of the grid (from the 20% need more data trigger) by
20815 * 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
20816 * assuming that you would have gotten to the bottom of the grid (from the 80% need more data trigger) by the time
20817 * the data comes back.
20819 * Neither of these are good assumptions, but making this a smoother experience really requires
20820 * 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
20821 * 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
20822 * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for
20823 * now, until someone wants to do better.
20824 * @param {Grid} grid the grid we're working on
20825 * @returns {promise} a promise that is resolved when scrolling has finished
20827 adjustScroll: function(grid){
20828 var promise = $q.defer();
20829 $timeout(function () {
20830 var newPercentage, viewportHeight, rowHeight, newVisibleRows, oldTop, newTop;
20832 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight;
20833 rowHeight = grid.options.rowHeight;
20835 if ( grid.infiniteScroll.direction === undefined ){
20836 // called from initialize, tweak our scroll up a little
20837 service.adjustInfiniteScrollPosition(grid, 0);
20840 newVisibleRows = grid.getVisibleRowCount();
20842 // in case not enough data is loaded to enable scroller - load more data
20843 var canvasHeight = rowHeight * newVisibleRows;
20844 if (grid.infiniteScroll.scrollDown && (viewportHeight > canvasHeight)) {
20845 grid.api.infiniteScroll.raise.needLoadMoreData();
20848 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){
20849 oldTop = grid.infiniteScroll.prevScrollTop || 0;
20850 newTop = oldTop + (newVisibleRows - grid.infiniteScroll.previousVisibleRows)*rowHeight;
20851 service.adjustInfiniteScrollPosition(grid, newTop);
20852 $timeout( function() {
20857 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){
20858 newTop = grid.infiniteScroll.prevScrollTop || (grid.infiniteScroll.previousVisibleRows*rowHeight - viewportHeight);
20859 service.adjustInfiniteScrollPosition(grid, newTop);
20860 $timeout( function() {
20866 return promise.promise;
20872 * @name adjustInfiniteScrollPosition
20873 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20874 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20875 * @param {Grid} grid the grid we're working on
20876 * @param {number} scrollTop the position through the grid that we want to scroll to
20877 * @returns {promise} a promise that is resolved when the scrolling finishes
20879 adjustInfiniteScrollPosition: function (grid, scrollTop) {
20880 var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'),
20881 visibleRows = grid.getVisibleRowCount(),
20882 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight,
20883 rowHeight = grid.options.rowHeight,
20884 scrollHeight = visibleRows*rowHeight-viewportHeight;
20886 //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
20887 if (scrollTop === 0 && grid.infiniteScroll.scrollUp) {
20888 // using pixels results in a relative scroll, hence we have to use percentage
20889 scrollEvent.y = {percentage: 1/scrollHeight};
20892 scrollEvent.y = {percentage: scrollTop/scrollHeight};
20894 grid.scrollContainers('', scrollEvent);
20900 * @name dataRemovedTop
20901 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20902 * @description Adjusts the scroll position after you've removed data at the top. You should
20903 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20904 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20905 * before you start removing data
20906 * @param {Grid} grid the grid we're working on
20907 * @param {boolean} scrollUp flag that there are pages upwards, fire
20908 * infinite scroll events upward
20909 * @param {boolean} scrollDown flag that there are pages downwards, so
20910 * fire infinite scroll events downward
20911 * @returns {promise} a promise that is resolved when the scrolling finishes
20913 dataRemovedTop: function( grid, scrollUp, scrollDown ) {
20914 var newVisibleRows, oldTop, newTop, rowHeight;
20915 service.setScrollDirections( grid, scrollUp, scrollDown );
20917 newVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20918 oldTop = grid.infiniteScroll.prevScrollTop;
20919 rowHeight = grid.options.rowHeight;
20921 // since we removed from the top, our new scroll row will be the old scroll row less the number
20923 newTop = oldTop - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows )*rowHeight;
20925 return service.adjustInfiniteScrollPosition( grid, newTop );
20930 * @name dataRemovedBottom
20931 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20932 * @description Adjusts the scroll position after you've removed data at the bottom. You should
20933 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20934 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20935 * before you start removing data
20936 * @param {Grid} grid the grid we're working on
20937 * @param {boolean} scrollUp flag that there are pages upwards, fire
20938 * infinite scroll events upward
20939 * @param {boolean} scrollDown flag that there are pages downwards, so
20940 * fire infinite scroll events downward
20942 dataRemovedBottom: function( grid, scrollUp, scrollDown ) {
20944 service.setScrollDirections( grid, scrollUp, scrollDown );
20946 newTop = grid.infiniteScroll.prevScrollTop;
20948 return service.adjustInfiniteScrollPosition( grid, newTop );
20955 * @name ui.grid.infiniteScroll.directive:uiGridInfiniteScroll
20959 * @description Adds infinite scroll features to grid
20962 <example module="app">
20963 <file name="app.js">
20964 var app = angular.module('app', ['ui.grid', 'ui.grid.infiniteScroll']);
20966 app.controller('MainCtrl', ['$scope', function ($scope) {
20968 { name: 'Alex', car: 'Toyota' },
20969 { name: 'Sam', car: 'Lexus' }
20972 $scope.columnDefs = [
20978 <file name="index.html">
20979 <div ng-controller="MainCtrl">
20980 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-infinite-scroll="20"></div>
20986 module.directive('uiGridInfiniteScroll', ['uiGridInfiniteScrollService',
20987 function (uiGridInfiniteScrollService) {
20991 require: '^uiGrid',
20992 compile: function($scope, $elm, $attr){
20994 pre: function($scope, $elm, $attr, uiGridCtrl) {
20995 uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid, $scope);
20997 post: function($scope, $elm, $attr) {
21011 * @name ui.grid.moveColumns
21014 * # ui.grid.moveColumns
21016 * <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>
21018 * This module provides column moving capability to ui.grid. It enables to change the position of columns.
21019 * <div doc-module-components="ui.grid.moveColumns"></div>
21021 var module = angular.module('ui.grid.moveColumns', ['ui.grid']);
21025 * @name ui.grid.moveColumns.service:uiGridMoveColumnService
21026 * @description Service for column moving feature.
21028 module.service('uiGridMoveColumnService', ['$q', '$timeout', '$log', 'ScrollEvent', 'uiGridConstants', 'gridUtil', function ($q, $timeout, $log, ScrollEvent, uiGridConstants, gridUtil) {
21031 initializeGrid: function (grid) {
21033 this.registerPublicApi(grid);
21034 this.defaultGridOptions(grid.options);
21035 grid.moveColumns = {orderCache: []}; // Used to cache the order before columns are rebuilt
21036 grid.registerColumnBuilder(self.movableColumnBuilder);
21037 grid.registerDataChangeCallback(self.verifyColumnOrder, [uiGridConstants.dataChange.COLUMN]);
21039 registerPublicApi: function (grid) {
21043 * @name ui.grid.moveColumns.api:PublicApi
21044 * @description Public Api for column moving feature.
21050 * @name columnPositionChanged
21051 * @eventOf ui.grid.moveColumns.api:PublicApi
21052 * @description raised when column is moved
21054 * gridApi.colMovable.on.columnPositionChanged(scope,function(colDef, originalPosition, newPosition){})
21056 * @param {object} colDef the column that was moved
21057 * @param {integer} originalPosition of the column
21058 * @param {integer} finalPosition of the column
21061 columnPositionChanged: function (colDef, originalPosition, newPosition) {
21069 * @methodOf ui.grid.moveColumns.api:PublicApi
21070 * @description Method can be used to change column position.
21072 * gridApi.colMovable.moveColumn(oldPosition, newPosition)
21074 * @param {integer} originalPosition of the column
21075 * @param {integer} finalPosition of the column
21078 moveColumn: function (originalPosition, finalPosition) {
21079 var columns = grid.columns;
21080 if (!angular.isNumber(originalPosition) || !angular.isNumber(finalPosition)) {
21081 gridUtil.logError('MoveColumn: Please provide valid values for originalPosition and finalPosition');
21084 var nonMovableColumns = 0;
21085 for (var i = 0; i < columns.length; i++) {
21086 if ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true) {
21087 nonMovableColumns++;
21090 if (originalPosition >= (columns.length - nonMovableColumns) || finalPosition >= (columns.length - nonMovableColumns)) {
21091 gridUtil.logError('MoveColumn: Invalid values for originalPosition, finalPosition');
21094 var findPositionForRenderIndex = function (index) {
21095 var position = index;
21096 for (var i = 0; i <= position; i++) {
21097 if (angular.isDefined(columns[i]) && ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true)) {
21103 self.redrawColumnAtPosition(grid, findPositionForRenderIndex(originalPosition), findPositionForRenderIndex(finalPosition));
21108 grid.api.registerEventsFromObject(publicApi.events);
21109 grid.api.registerMethodsFromObject(publicApi.methods);
21111 defaultGridOptions: function (gridOptions) {
21114 * @name ui.grid.moveColumns.api:GridOptions
21116 * @description Options for configuring the move column feature, these are available to be
21117 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21121 * @name enableColumnMoving
21122 * @propertyOf ui.grid.moveColumns.api:GridOptions
21123 * @description If defined, sets the default value for the colMovable flag on each individual colDefs
21124 * if their individual enableColumnMoving configuration is not defined. Defaults to true.
21126 gridOptions.enableColumnMoving = gridOptions.enableColumnMoving !== false;
21128 movableColumnBuilder: function (colDef, col, gridOptions) {
21132 * @name ui.grid.moveColumns.api:ColumnDef
21134 * @description Column Definition for move column feature, these are available to be
21135 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
21139 * @name enableColumnMoving
21140 * @propertyOf ui.grid.moveColumns.api:ColumnDef
21141 * @description Enable column moving for the column.
21143 colDef.enableColumnMoving = colDef.enableColumnMoving === undefined ? gridOptions.enableColumnMoving
21144 : colDef.enableColumnMoving;
21145 return $q.all(promises);
21149 * @name updateColumnCache
21150 * @methodOf ui.grid.moveColumns
21151 * @description Cache the current order of columns, so we can restore them after new columnDefs are defined
21153 updateColumnCache: function(grid){
21154 grid.moveColumns.orderCache = grid.getOnlyDataColumns();
21158 * @name verifyColumnOrder
21159 * @methodOf ui.grid.moveColumns
21160 * @description dataChangeCallback which uses the cached column order to restore the column order
21161 * when it is reset by altering the columnDefs array.
21163 verifyColumnOrder: function(grid){
21164 var headerRowOffset = grid.rowHeaderColumns.length;
21167 angular.forEach(grid.moveColumns.orderCache, function(cacheCol, cacheIndex){
21168 newIndex = grid.columns.indexOf(cacheCol);
21169 if ( newIndex !== -1 && newIndex - headerRowOffset !== cacheIndex ){
21170 var column = grid.columns.splice(newIndex, 1)[0];
21171 grid.columns.splice(cacheIndex + headerRowOffset, 0, column);
21175 redrawColumnAtPosition: function (grid, originalPosition, newPosition) {
21176 if (originalPosition === newPosition) {
21180 var columns = grid.columns;
21182 var originalColumn = columns[originalPosition];
21183 if (originalColumn.colDef.enableColumnMoving) {
21184 if (originalPosition > newPosition) {
21185 for (var i1 = originalPosition; i1 > newPosition; i1--) {
21186 columns[i1] = columns[i1 - 1];
21189 else if (newPosition > originalPosition) {
21190 for (var i2 = originalPosition; i2 < newPosition; i2++) {
21191 columns[i2] = columns[i2 + 1];
21194 columns[newPosition] = originalColumn;
21195 service.updateColumnCache(grid);
21196 grid.queueGridRefresh();
21197 $timeout(function () {
21198 grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
21199 grid.api.colMovable.raise.columnPositionChanged(originalColumn.colDef, originalPosition, newPosition);
21209 * @name ui.grid.moveColumns.directive:uiGridMoveColumns
21212 * @description Adds column moving features to the ui-grid directive.
21214 <example module="app">
21215 <file name="app.js">
21216 var app = angular.module('app', ['ui.grid', 'ui.grid.moveColumns']);
21217 app.controller('MainCtrl', ['$scope', function ($scope) {
21219 { name: 'Bob', title: 'CEO', age: 45 },
21220 { name: 'Frank', title: 'Lowly Developer', age: 25 },
21221 { name: 'Jenny', title: 'Highly Developer', age: 35 }
21223 $scope.columnDefs = [
21230 <file name="main.css">
21236 <file name="index.html">
21237 <div ng-controller="MainCtrl">
21238 <div class="grid" ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-move-columns></div>
21243 module.directive('uiGridMoveColumns', ['uiGridMoveColumnService', function (uiGridMoveColumnService) {
21247 require: '^uiGrid',
21249 compile: function () {
21251 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
21252 uiGridMoveColumnService.initializeGrid(uiGridCtrl.grid);
21254 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21263 * @name ui.grid.moveColumns.directive:uiGridHeaderCell
21267 * @description Stacks on top of ui.grid.uiGridHeaderCell to provide capability to be able to move it to reposition column.
21269 * On receiving mouseDown event headerCell is cloned, now as the mouse moves the cloned header cell also moved in the grid.
21270 * In case the moving cloned header cell reaches the left or right extreme of grid, grid scrolling is triggered (if horizontal scroll exists).
21271 * On mouseUp event column is repositioned at position where mouse is released and cloned header cell is removed.
21273 * Events that invoke cloning of header cell:
21276 * Events that invoke movement of cloned header cell:
21279 * Events that invoke repositioning of column:
21282 module.directive('uiGridHeaderCell', ['$q', 'gridUtil', 'uiGridMoveColumnService', '$document', '$log', 'uiGridConstants', 'ScrollEvent',
21283 function ($q, gridUtil, uiGridMoveColumnService, $document, $log, uiGridConstants, ScrollEvent) {
21286 require: '^uiGrid',
21287 compile: function () {
21289 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21291 if ($scope.col.colDef.enableColumnMoving) {
21294 * Our general approach to column move is that we listen to a touchstart or mousedown
21295 * event over the column header. When we hear one, then we wait for a move of the same type
21296 * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for
21297 * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move,
21298 * and we instead get a mouseup or a touchend, then we just drop out again and do nothing.
21301 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
21304 var previousMouseX;
21305 var totalMouseMovement;
21306 var rightMoveLimit;
21307 var elmCloned = false;
21310 var moveOccurred = false;
21312 var downFn = function( event ){
21313 //Setting some variables required for calculations.
21314 gridLeft = $scope.grid.element[0].getBoundingClientRect().left;
21315 if ( $scope.grid.hasLeftContainer() ){
21316 gridLeft += $scope.grid.renderContainers.left.header[0].getBoundingClientRect().width;
21319 previousMouseX = event.pageX;
21320 totalMouseMovement = 0;
21321 rightMoveLimit = gridLeft + $scope.grid.getViewportWidth();
21323 if ( event.type === 'mousedown' ){
21324 $document.on('mousemove', moveFn);
21325 $document.on('mouseup', upFn);
21326 } else if ( event.type === 'touchstart' ){
21327 $document.on('touchmove', moveFn);
21328 $document.on('touchend', upFn);
21332 var moveFn = function( event ) {
21333 var changeValue = event.pageX - previousMouseX;
21334 if ( changeValue === 0 ){ return; }
21335 //Disable text selection in Chrome during column move
21336 document.onselectstart = function() { return false; };
21338 moveOccurred = true;
21343 else if (elmCloned) {
21344 moveElement(changeValue);
21345 previousMouseX = event.pageX;
21349 var upFn = function( event ){
21350 //Re-enable text selection after column move
21351 document.onselectstart = null;
21353 //Remove the cloned element on mouse up.
21355 movingElm.remove();
21362 if (!moveOccurred){
21366 var columns = $scope.grid.columns;
21367 var columnIndex = 0;
21368 for (var i = 0; i < columns.length; i++) {
21369 if (columns[i].colDef.name !== $scope.col.colDef.name) {
21377 //Case where column should be moved to a position on its left
21378 if (totalMouseMovement < 0) {
21379 var totalColumnsLeftWidth = 0;
21380 for (var il = columnIndex - 1; il >= 0; il--) {
21381 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
21382 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
21383 if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) {
21384 uiGridMoveColumnService.redrawColumnAtPosition
21385 ($scope.grid, columnIndex, il + 1);
21390 //Case where column should be moved to beginning of the grid.
21391 if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) {
21392 uiGridMoveColumnService.redrawColumnAtPosition
21393 ($scope.grid, columnIndex, 0);
21397 //Case where column should be moved to a position on its right
21398 else if (totalMouseMovement > 0) {
21399 var totalColumnsRightWidth = 0;
21400 for (var ir = columnIndex + 1; ir < columns.length; ir++) {
21401 if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) {
21402 totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width;
21403 if (totalColumnsRightWidth > totalMouseMovement) {
21404 uiGridMoveColumnService.redrawColumnAtPosition
21405 ($scope.grid, columnIndex, ir - 1);
21410 //Case where column should be moved to end of the grid.
21411 if (totalColumnsRightWidth < totalMouseMovement) {
21412 uiGridMoveColumnService.redrawColumnAtPosition
21413 ($scope.grid, columnIndex, columns.length - 1);
21418 var onDownEvents = function(){
21419 $contentsElm.on('touchstart', downFn);
21420 $contentsElm.on('mousedown', downFn);
21423 var offAllEvents = function() {
21424 $contentsElm.off('touchstart', downFn);
21425 $contentsElm.off('mousedown', downFn);
21427 $document.off('mousemove', moveFn);
21428 $document.off('touchmove', moveFn);
21430 $document.off('mouseup', upFn);
21431 $document.off('touchend', upFn);
21437 var cloneElement = function () {
21440 //Cloning header cell and appending to current header cell.
21441 movingElm = $elm.clone();
21442 $elm.parent().append(movingElm);
21444 //Left of cloned element should be aligned to original header cell.
21445 movingElm.addClass('movingColumn');
21446 var movingElementStyles = {};
21447 movingElementStyles.left = $elm[0].offsetLeft + 'px';
21448 var gridRight = $scope.grid.element[0].getBoundingClientRect().right;
21449 var elmRight = $elm[0].getBoundingClientRect().right;
21450 if (elmRight > gridRight) {
21451 reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight);
21452 movingElementStyles.width = reducedWidth + 'px';
21454 movingElm.css(movingElementStyles);
21457 var moveElement = function (changeValue) {
21458 //Calculate total column width
21459 var columns = $scope.grid.columns;
21460 var totalColumnWidth = 0;
21461 for (var i = 0; i < columns.length; i++) {
21462 if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) {
21463 totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width;
21467 //Calculate new position of left of column
21468 var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1;
21469 var currentElmRight = movingElm[0].getBoundingClientRect().right;
21470 var newElementLeft;
21472 newElementLeft = currentElmLeft - gridLeft + changeValue;
21473 newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit;
21475 //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid
21476 if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) {
21477 movingElm.css({visibility: 'visible', 'left': (movingElm[0].offsetLeft +
21478 (newElementLeft < rightMoveLimit ? changeValue : (rightMoveLimit - currentElmLeft))) + 'px'});
21480 else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) {
21482 var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement');
21483 scrollEvent.x = {pixels: changeValue};
21484 scrollEvent.grid.scrollContainers('',scrollEvent);
21487 //Calculate total width of columns on the left of the moving column and the mouse movement
21488 var totalColumnsLeftWidth = 0;
21489 for (var il = 0; il < columns.length; il++) {
21490 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
21491 if (columns[il].colDef.name !== $scope.col.colDef.name) {
21492 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
21499 if ($scope.newScrollLeft === undefined) {
21500 totalMouseMovement += changeValue;
21503 totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth;
21506 //Increase width of moving column, in case the rightmost column was moved and its width was
21507 //decreased because of overflow
21508 if (reducedWidth < $scope.col.drawnWidth) {
21509 reducedWidth += Math.abs(changeValue);
21510 movingElm.css({'width': reducedWidth + 'px'});
21526 * @name ui.grid.pagination
21530 * # ui.grid.pagination
21532 * <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>
21534 * This module provides pagination support to ui-grid
21536 var module = angular.module('ui.grid.pagination', ['ng', 'ui.grid']);
21540 * @name ui.grid.pagination.service:uiGridPaginationService
21542 * @description Service for the pagination feature
21544 module.service('uiGridPaginationService', ['gridUtil',
21545 function (gridUtil) {
21549 * @name initializeGrid
21550 * @methodOf ui.grid.pagination.service:uiGridPaginationService
21551 * @description Attaches the service to a certain grid
21552 * @param {Grid} grid The grid we want to work with
21554 initializeGrid: function (grid) {
21555 service.defaultGridOptions(grid.options);
21559 * @name ui.grid.pagination.api:PublicAPI
21561 * @description Public API for the pagination feature
21568 * @name paginationChanged
21569 * @eventOf ui.grid.pagination.api:PublicAPI
21570 * @description This event fires when the pageSize or currentPage changes
21571 * @param {int} currentPage requested page number
21572 * @param {int} pageSize requested page size
21574 paginationChanged: function (currentPage, pageSize) { }
21582 * @methodOf ui.grid.pagination.api:PublicAPI
21583 * @description Returns the number of the current page
21585 getPage: function () {
21586 return grid.options.enablePagination ? grid.options.paginationCurrentPage : null;
21590 * @name getTotalPages
21591 * @methodOf ui.grid.pagination.api:PublicAPI
21592 * @description Returns the total number of pages
21594 getTotalPages: function () {
21595 if (!grid.options.enablePagination) {
21599 return (grid.options.totalItems === 0) ? 1 : Math.ceil(grid.options.totalItems / grid.options.paginationPageSize);
21604 * @methodOf ui.grid.pagination.api:PublicAPI
21605 * @description Moves to the next page, if possible
21607 nextPage: function () {
21608 if (!grid.options.enablePagination) {
21612 if (grid.options.totalItems > 0) {
21613 grid.options.paginationCurrentPage = Math.min(
21614 grid.options.paginationCurrentPage + 1,
21615 publicApi.methods.pagination.getTotalPages()
21618 grid.options.paginationCurrentPage++;
21623 * @name previousPage
21624 * @methodOf ui.grid.pagination.api:PublicAPI
21625 * @description Moves to the previous page, if we're not on the first page
21627 previousPage: function () {
21628 if (!grid.options.enablePagination) {
21632 grid.options.paginationCurrentPage = Math.max(grid.options.paginationCurrentPage - 1, 1);
21637 * @methodOf ui.grid.pagination.api:PublicAPI
21638 * @description Moves to the requested page
21639 * @param {int} page The number of the page that should be displayed
21641 seek: function (page) {
21642 if (!grid.options.enablePagination) {
21645 if (!angular.isNumber(page) || page < 1) {
21646 throw 'Invalid page number: ' + page;
21649 grid.options.paginationCurrentPage = Math.min(page, publicApi.methods.pagination.getTotalPages());
21655 grid.api.registerEventsFromObject(publicApi.events);
21656 grid.api.registerMethodsFromObject(publicApi.methods);
21658 var processPagination = function( renderableRows ){
21659 if (grid.options.useExternalPagination || !grid.options.enablePagination) {
21660 return renderableRows;
21662 //client side pagination
21663 var pageSize = parseInt(grid.options.paginationPageSize, 10);
21664 var currentPage = parseInt(grid.options.paginationCurrentPage, 10);
21666 var visibleRows = renderableRows.filter(function (row) { return row.visible; });
21667 grid.options.totalItems = visibleRows.length;
21669 var firstRow = (currentPage - 1) * pageSize;
21670 if (firstRow > visibleRows.length) {
21671 currentPage = grid.options.paginationCurrentPage = 1;
21672 firstRow = (currentPage - 1) * pageSize;
21674 return visibleRows.slice(firstRow, firstRow + pageSize);
21677 grid.registerRowsProcessor(processPagination, 900 );
21680 defaultGridOptions: function (gridOptions) {
21683 * @name ui.grid.pagination.api:GridOptions
21685 * @description GridOptions for the pagination feature, these are available to be
21686 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21691 * @name enablePagination
21692 * @propertyOf ui.grid.pagination.api:GridOptions
21693 * @description Enables pagination. Defaults to true.
21695 gridOptions.enablePagination = gridOptions.enablePagination !== false;
21698 * @name enablePaginationControls
21699 * @propertyOf ui.grid.pagination.api:GridOptions
21700 * @description Enables the paginator at the bottom of the grid. Turn this off if you want to implement your
21701 * own controls outside the grid.
21703 gridOptions.enablePaginationControls = gridOptions.enablePaginationControls !== false;
21706 * @name useExternalPagination
21707 * @propertyOf ui.grid.pagination.api:GridOptions
21708 * @description Disables client side pagination. When true, handle the paginationChanged event and set data
21709 * and totalItems. Defaults to `false`
21711 gridOptions.useExternalPagination = gridOptions.useExternalPagination === true;
21715 * @propertyOf ui.grid.pagination.api:GridOptions
21716 * @description Total number of items, set automatically when using client side pagination, but needs set by user
21717 * for server side pagination
21719 if (gridUtil.isNullOrUndefined(gridOptions.totalItems)) {
21720 gridOptions.totalItems = 0;
21724 * @name paginationPageSizes
21725 * @propertyOf ui.grid.pagination.api:GridOptions
21726 * @description Array of page sizes, defaults to `[250, 500, 1000]`
21728 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSizes)) {
21729 gridOptions.paginationPageSizes = [250, 500, 1000];
21733 * @name paginationPageSize
21734 * @propertyOf ui.grid.pagination.api:GridOptions
21735 * @description Page size, defaults to the first item in paginationPageSizes, or 0 if paginationPageSizes is empty
21737 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSize)) {
21738 if (gridOptions.paginationPageSizes.length > 0) {
21739 gridOptions.paginationPageSize = gridOptions.paginationPageSizes[0];
21741 gridOptions.paginationPageSize = 0;
21746 * @name paginationCurrentPage
21747 * @propertyOf ui.grid.pagination.api:GridOptions
21748 * @description Current page number, defaults to 1
21750 if (gridUtil.isNullOrUndefined(gridOptions.paginationCurrentPage)) {
21751 gridOptions.paginationCurrentPage = 1;
21756 * @name paginationTemplate
21757 * @propertyOf ui.grid.pagination.api:GridOptions
21758 * @description A custom template for the pager, defaults to `ui-grid/pagination`
21760 if (gridUtil.isNullOrUndefined(gridOptions.paginationTemplate)) {
21761 gridOptions.paginationTemplate = 'ui-grid/pagination';
21766 * @methodOf ui.grid.pagination.service:uiGridPaginationService
21767 * @name uiGridPaginationService
21768 * @description Raises paginationChanged and calls refresh for client side pagination
21769 * @param {Grid} grid the grid for which the pagination changed
21770 * @param {int} currentPage requested page number
21771 * @param {int} pageSize requested page size
21773 onPaginationChanged: function (grid, currentPage, pageSize) {
21774 grid.api.pagination.raise.paginationChanged(currentPage, pageSize);
21775 if (!grid.options.useExternalPagination) {
21776 grid.queueGridRefresh(); //client side pagination
21786 * @name ui.grid.pagination.directive:uiGridPagination
21790 * @description Adds pagination features to grid
21792 <example module="app">
21793 <file name="app.js">
21794 var app = angular.module('app', ['ui.grid', 'ui.grid.pagination']);
21796 app.controller('MainCtrl', ['$scope', function ($scope) {
21798 { name: 'Alex', car: 'Toyota' },
21799 { name: 'Sam', car: 'Lexus' },
21800 { name: 'Joe', car: 'Dodge' },
21801 { name: 'Bob', car: 'Buick' },
21802 { name: 'Cindy', car: 'Ford' },
21803 { name: 'Brian', car: 'Audi' },
21804 { name: 'Malcom', car: 'Mercedes Benz' },
21805 { name: 'Dave', car: 'Ford' },
21806 { name: 'Stacey', car: 'Audi' },
21807 { name: 'Amy', car: 'Acura' },
21808 { name: 'Scott', car: 'Toyota' },
21809 { name: 'Ryan', car: 'BMW' },
21812 $scope.gridOptions = {
21814 paginationPageSizes: [5, 10, 25],
21815 paginationPageSize: 5,
21823 <file name="index.html">
21824 <div ng-controller="MainCtrl">
21825 <div ui-grid="gridOptions" ui-grid-pagination></div>
21830 module.directive('uiGridPagination', ['gridUtil', 'uiGridPaginationService',
21831 function (gridUtil, uiGridPaginationService) {
21837 pre: function ($scope, $elm, $attr, uiGridCtrl) {
21838 uiGridPaginationService.initializeGrid(uiGridCtrl.grid);
21840 gridUtil.getTemplate(uiGridCtrl.grid.options.paginationTemplate)
21841 .then(function (contents) {
21842 var template = angular.element(contents);
21843 $elm.append(template);
21844 uiGridCtrl.innerCompile(template);
21854 * @name ui.grid.pagination.directive:uiGridPager
21857 * @description Panel for handling pagination
21859 module.directive('uiGridPager', ['uiGridPaginationService', 'uiGridConstants', 'gridUtil', 'i18nService',
21860 function (uiGridPaginationService, uiGridConstants, gridUtil, i18nService) {
21864 require: '^uiGrid',
21865 link: function ($scope, $elm, $attr, uiGridCtrl) {
21866 var defaultFocusElementSelector = '.ui-grid-pager-control-input';
21867 $scope.aria = i18nService.getSafeText('pagination.aria'); //Returns an object with all of the aria labels
21869 $scope.paginationApi = uiGridCtrl.grid.api.pagination;
21870 $scope.sizesLabel = i18nService.getSafeText('pagination.sizes');
21871 $scope.totalItemsLabel = i18nService.getSafeText('pagination.totalItems');
21872 $scope.paginationOf = i18nService.getSafeText('pagination.of');
21873 $scope.paginationThrough = i18nService.getSafeText('pagination.through');
21875 var options = uiGridCtrl.grid.options;
21877 uiGridCtrl.grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
21878 adjustment.height = adjustment.height - gridUtil.elementHeight($elm, "padding");
21882 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback(function (grid) {
21883 if (!grid.options.useExternalPagination) {
21884 grid.options.totalItems = grid.rows.length;
21886 }, [uiGridConstants.dataChange.ROW]);
21888 $scope.$on('$destroy', dataChangeDereg);
21890 var setShowing = function () {
21891 $scope.showingLow = ((options.paginationCurrentPage - 1) * options.paginationPageSize) + 1;
21892 $scope.showingHigh = Math.min(options.paginationCurrentPage * options.paginationPageSize, options.totalItems);
21895 var deregT = $scope.$watch('grid.options.totalItems + grid.options.paginationPageSize', setShowing);
21897 var deregP = $scope.$watch('grid.options.paginationCurrentPage + grid.options.paginationPageSize', function (newValues, oldValues) {
21898 if (newValues === oldValues || oldValues === undefined) {
21902 if (!angular.isNumber(options.paginationCurrentPage) || options.paginationCurrentPage < 1) {
21903 options.paginationCurrentPage = 1;
21907 if (options.totalItems > 0 && options.paginationCurrentPage > $scope.paginationApi.getTotalPages()) {
21908 options.paginationCurrentPage = $scope.paginationApi.getTotalPages();
21913 uiGridPaginationService.onPaginationChanged($scope.grid, options.paginationCurrentPage, options.paginationPageSize);
21917 $scope.$on('$destroy', function() {
21922 $scope.cantPageForward = function () {
21923 if (options.totalItems > 0) {
21924 return options.paginationCurrentPage >= $scope.paginationApi.getTotalPages();
21926 return options.data.length < 1;
21930 $scope.cantPageToLast = function () {
21931 if (options.totalItems > 0) {
21932 return $scope.cantPageForward();
21938 $scope.cantPageBackward = function () {
21939 return options.paginationCurrentPage <= 1;
21942 var focusToInputIf = function(condition){
21944 gridUtil.focus.bySelector($elm, defaultFocusElementSelector);
21948 //Takes care of setting focus to the middle element when focus is lost
21949 $scope.pageFirstPageClick = function () {
21950 $scope.paginationApi.seek(1);
21951 focusToInputIf($scope.cantPageBackward());
21954 $scope.pagePreviousPageClick = function () {
21955 $scope.paginationApi.previousPage();
21956 focusToInputIf($scope.cantPageBackward());
21959 $scope.pageNextPageClick = function () {
21960 $scope.paginationApi.nextPage();
21961 focusToInputIf($scope.cantPageForward());
21964 $scope.pageLastPageClick = function () {
21965 $scope.paginationApi.seek($scope.paginationApi.getTotalPages());
21966 focusToInputIf($scope.cantPageToLast());
21980 * @name ui.grid.pinning
21983 * # ui.grid.pinning
21985 * <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>
21987 * This module provides column pinning to the end user via menu options in the column header
21989 * <div doc-module-components="ui.grid.pinning"></div>
21992 var module = angular.module('ui.grid.pinning', ['ui.grid']);
21994 module.constant('uiGridPinningConstants', {
22002 module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', 'uiGridPinningConstants', function (gridUtil, GridRenderContainer, i18nService, uiGridPinningConstants) {
22005 initializeGrid: function (grid) {
22006 service.defaultGridOptions(grid.options);
22008 // Register a column builder to add new menu items for pinning left and right
22009 grid.registerColumnBuilder(service.pinningColumnBuilder);
22013 * @name ui.grid.pinning.api:PublicApi
22015 * @description Public Api for pinning feature
22023 * @eventOf ui.grid.pinning.api:PublicApi
22024 * @description raised when column pin state has changed
22026 * gridApi.pinning.on.columnPinned(scope, function(colDef){})
22028 * @param {object} colDef the column that was changed
22029 * @param {string} container the render container the column is in ('left', 'right', '')
22031 columnPinned: function(colDef, container) {
22040 * @methodOf ui.grid.pinning.api:PublicApi
22041 * @description pin column left, right, or none
22043 * gridApi.pinning.pinColumn(col, uiGridPinningConstants.container.LEFT)
22045 * @param {gridColumn} col the column being pinned
22046 * @param {string} container one of the recognised types
22047 * from uiGridPinningConstants
22049 pinColumn: function(col, container) {
22050 service.pinColumn(grid, col, container);
22056 grid.api.registerEventsFromObject(publicApi.events);
22057 grid.api.registerMethodsFromObject(publicApi.methods);
22060 defaultGridOptions: function (gridOptions) {
22061 //default option to true unless it was explicitly set to false
22064 * @name ui.grid.pinning.api:GridOptions
22066 * @description GridOptions for pinning feature, these are available to be
22067 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
22072 * @name enablePinning
22073 * @propertyOf ui.grid.pinning.api:GridOptions
22074 * @description Enable pinning for the entire grid.
22075 * <br/>Defaults to true
22077 gridOptions.enablePinning = gridOptions.enablePinning !== false;
22081 pinningColumnBuilder: function (colDef, col, gridOptions) {
22082 //default to true unless gridOptions or colDef is explicitly false
22086 * @name ui.grid.pinning.api:ColumnDef
22088 * @description ColumnDef for pinning feature, these are available to be
22089 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
22094 * @name enablePinning
22095 * @propertyOf ui.grid.pinning.api:ColumnDef
22096 * @description Enable pinning for the individual column.
22097 * <br/>Defaults to true
22099 colDef.enablePinning = colDef.enablePinning === undefined ? gridOptions.enablePinning : colDef.enablePinning;
22105 * @propertyOf ui.grid.pinning.api:ColumnDef
22106 * @description Column is pinned left when grid is rendered
22107 * <br/>Defaults to false
22112 * @name pinnedRight
22113 * @propertyOf ui.grid.pinning.api:ColumnDef
22114 * @description Column is pinned right when grid is rendered
22115 * <br/>Defaults to false
22117 if (colDef.pinnedLeft) {
22118 col.renderContainer = 'left';
22119 col.grid.createLeftContainer();
22121 else if (colDef.pinnedRight) {
22122 col.renderContainer = 'right';
22123 col.grid.createRightContainer();
22126 if (!colDef.enablePinning) {
22130 var pinColumnLeftAction = {
22131 name: 'ui.grid.pinning.pinLeft',
22132 title: i18nService.get().pinning.pinLeft,
22133 icon: 'ui-grid-icon-left-open',
22134 shown: function () {
22135 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'left';
22137 action: function () {
22138 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.LEFT);
22142 var pinColumnRightAction = {
22143 name: 'ui.grid.pinning.pinRight',
22144 title: i18nService.get().pinning.pinRight,
22145 icon: 'ui-grid-icon-right-open',
22146 shown: function () {
22147 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'right';
22149 action: function () {
22150 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.RIGHT);
22154 var removePinAction = {
22155 name: 'ui.grid.pinning.unpin',
22156 title: i18nService.get().pinning.unpin,
22157 icon: 'ui-grid-icon-cancel',
22158 shown: function () {
22159 return typeof(this.context.col.renderContainer) !== 'undefined' && this.context.col.renderContainer !== null && this.context.col.renderContainer !== 'body';
22161 action: function () {
22162 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.UNPIN);
22166 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinLeft')) {
22167 col.menuItems.push(pinColumnLeftAction);
22169 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinRight')) {
22170 col.menuItems.push(pinColumnRightAction);
22172 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.unpin')) {
22173 col.menuItems.push(removePinAction);
22177 pinColumn: function(grid, col, container) {
22178 if (container === uiGridPinningConstants.container.NONE) {
22179 col.renderContainer = null;
22182 col.renderContainer = container;
22183 if (container === uiGridPinningConstants.container.LEFT) {
22184 grid.createLeftContainer();
22186 else if (container === uiGridPinningConstants.container.RIGHT) {
22187 grid.createRightContainer();
22193 grid.api.pinning.raise.columnPinned( col.colDef, container );
22201 module.directive('uiGridPinning', ['gridUtil', 'uiGridPinningService',
22202 function (gridUtil, uiGridPinningService) {
22206 compile: function () {
22208 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
22209 uiGridPinningService.initializeGrid(uiGridCtrl.grid);
22211 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22226 * @name ui.grid.resizeColumns
22229 * # ui.grid.resizeColumns
22231 * <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>
22233 * This module allows columns to be resized.
22235 var module = angular.module('ui.grid.resizeColumns', ['ui.grid']);
22237 module.service('uiGridResizeColumnsService', ['gridUtil', '$q', '$timeout',
22238 function (gridUtil, $q, $timeout) {
22241 defaultGridOptions: function(gridOptions){
22242 //default option to true unless it was explicitly set to false
22245 * @name ui.grid.resizeColumns.api:GridOptions
22247 * @description GridOptions for resizeColumns feature, these are available to be
22248 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
22253 * @name enableColumnResizing
22254 * @propertyOf ui.grid.resizeColumns.api:GridOptions
22255 * @description Enable column resizing on the entire grid
22256 * <br/>Defaults to true
22258 gridOptions.enableColumnResizing = gridOptions.enableColumnResizing !== false;
22261 //use old name if it is explicitly false
22262 if (gridOptions.enableColumnResize === false){
22263 gridOptions.enableColumnResizing = false;
22267 colResizerColumnBuilder: function (colDef, col, gridOptions) {
22272 * @name ui.grid.resizeColumns.api:ColumnDef
22274 * @description ColumnDef for resizeColumns feature, these are available to be
22275 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
22280 * @name enableColumnResizing
22281 * @propertyOf ui.grid.resizeColumns.api:ColumnDef
22282 * @description Enable column resizing on an individual column
22283 * <br/>Defaults to GridOptions.enableColumnResizing
22285 //default to true unless gridOptions or colDef is explicitly false
22286 colDef.enableColumnResizing = colDef.enableColumnResizing === undefined ? gridOptions.enableColumnResizing : colDef.enableColumnResizing;
22289 //legacy support of old option name
22290 if (colDef.enableColumnResize === false){
22291 colDef.enableColumnResizing = false;
22294 return $q.all(promises);
22297 registerPublicApi: function (grid) {
22300 * @name ui.grid.resizeColumns.api:PublicApi
22301 * @description Public Api for column resize feature.
22307 * @name columnSizeChanged
22308 * @eventOf ui.grid.resizeColumns.api:PublicApi
22309 * @description raised when column is resized
22311 * gridApi.colResizable.on.columnSizeChanged(scope,function(colDef, deltaChange){})
22313 * @param {object} colDef the column that was resized
22314 * @param {integer} delta of the column size change
22317 columnSizeChanged: function (colDef, deltaChange) {
22322 grid.api.registerEventsFromObject(publicApi.events);
22325 fireColumnSizeChanged: function (grid, colDef, deltaChange) {
22326 $timeout(function () {
22327 if ( grid.api.colResizable ){
22328 grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange);
22330 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.");
22335 // get either this column, or the column next to this column, to resize,
22336 // returns the column we're going to resize
22337 findTargetCol: function(col, position, rtlMultiplier){
22338 var renderContainer = col.getRenderContainer();
22340 if (position === 'left') {
22341 // Get the column to the left of this one
22342 var colIndex = renderContainer.visibleColumnCache.indexOf(col);
22343 return renderContainer.visibleColumnCache[colIndex - 1 * rtlMultiplier];
22358 * @name ui.grid.resizeColumns.directive:uiGridResizeColumns
22362 * 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
22363 * option to false. This prevents resizing for the entire grid, regardless of individual columnDef options.
22366 <doc:example module="app">
22369 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
22371 app.controller('MainCtrl', ['$scope', function ($scope) {
22372 $scope.gridOpts = {
22374 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
22375 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
22376 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
22377 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
22383 <div ng-controller="MainCtrl">
22384 <div class="testGrid" ui-grid="gridOpts" ui-grid-resize-columns ></div>
22392 module.directive('uiGridResizeColumns', ['gridUtil', 'uiGridResizeColumnsService', function (gridUtil, uiGridResizeColumnsService) {
22396 require: '^uiGrid',
22398 compile: function () {
22400 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
22401 uiGridResizeColumnsService.defaultGridOptions(uiGridCtrl.grid.options);
22402 uiGridCtrl.grid.registerColumnBuilder( uiGridResizeColumnsService.colResizerColumnBuilder);
22403 uiGridResizeColumnsService.registerPublicApi(uiGridCtrl.grid);
22405 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22412 // Extend the uiGridHeaderCell directive
22413 module.directive('uiGridHeaderCell', ['gridUtil', '$templateCache', '$compile', '$q', 'uiGridResizeColumnsService', 'uiGridConstants', '$timeout', function (gridUtil, $templateCache, $compile, $q, uiGridResizeColumnsService, uiGridConstants, $timeout) {
22415 // Run after the original uiGridHeaderCell
22417 require: '^uiGrid',
22419 compile: function() {
22421 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22422 var grid = uiGridCtrl.grid;
22424 if (grid.options.enableColumnResizing) {
22425 var columnResizerElm = $templateCache.get('ui-grid/columnResizer');
22427 var rtlMultiplier = 1;
22428 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
22429 if (grid.isRTL()) {
22430 $scope.position = 'left';
22431 rtlMultiplier = -1;
22434 var displayResizers = function(){
22436 // remove any existing resizers.
22437 var resizers = $elm[0].getElementsByClassName('ui-grid-column-resizer');
22438 for ( var i = 0; i < resizers.length; i++ ){
22439 angular.element(resizers[i]).remove();
22442 // get the target column for the left resizer
22443 var otherCol = uiGridResizeColumnsService.findTargetCol($scope.col, 'left', rtlMultiplier);
22444 var renderContainer = $scope.col.getRenderContainer();
22446 // Don't append the left resizer if this is the first column or the column to the left of this one has resizing disabled
22447 if (otherCol && renderContainer.visibleColumnCache.indexOf($scope.col) !== 0 && otherCol.colDef.enableColumnResizing !== false) {
22448 var resizerLeft = angular.element(columnResizerElm).clone();
22449 resizerLeft.attr('position', 'left');
22451 $elm.prepend(resizerLeft);
22452 $compile(resizerLeft)($scope);
22455 // Don't append the right resizer if this column has resizing disabled
22456 if ($scope.col.colDef.enableColumnResizing !== false) {
22457 var resizerRight = angular.element(columnResizerElm).clone();
22458 resizerRight.attr('position', 'right');
22460 $elm.append(resizerRight);
22461 $compile(resizerRight)($scope);
22467 var waitDisplay = function(){
22468 $timeout(displayResizers);
22471 var dataChangeDereg = grid.registerDataChangeCallback( waitDisplay, [uiGridConstants.dataChange.COLUMN] );
22473 $scope.$on( '$destroy', dataChangeDereg );
22485 * @name ui.grid.resizeColumns.directive:uiGridColumnResizer
22490 * Draggable handle that controls column resizing.
22493 <doc:example module="app">
22496 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
22498 app.controller('MainCtrl', ['$scope', function ($scope) {
22499 $scope.gridOpts = {
22500 enableColumnResizing: true,
22502 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
22503 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
22504 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
22505 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
22511 <div ng-controller="MainCtrl">
22512 <div class="testGrid" ui-grid="gridOpts"></div>
22516 // TODO: e2e specs?
22518 // TODO: post-resize a horizontal scroll event should be fired
22522 module.directive('uiGridColumnResizer', ['$document', 'gridUtil', 'uiGridConstants', 'uiGridResizeColumnsService', function ($document, gridUtil, uiGridConstants, uiGridResizeColumnsService) {
22523 var resizeOverlay = angular.element('<div class="ui-grid-resize-overlay"></div>');
22532 require: '?^uiGrid',
22533 link: function ($scope, $elm, $attrs, uiGridCtrl) {
22539 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
22540 if (uiGridCtrl.grid.isRTL()) {
22541 $scope.position = 'left';
22542 rtlMultiplier = -1;
22545 if ($scope.position === 'left') {
22546 $elm.addClass('left');
22548 else if ($scope.position === 'right') {
22549 $elm.addClass('right');
22552 // Refresh the grid canvas
22553 // takes an argument representing the diff along the X-axis that the resize had
22554 function refreshCanvas(xDiff) {
22555 // Then refresh the grid canvas, rebuilding the styles so that the scrollbar updates its size
22556 uiGridCtrl.grid.refreshCanvas(true).then( function() {
22557 uiGridCtrl.grid.queueGridRefresh();
22561 // Check that the requested width isn't wider than the maxWidth, or narrower than the minWidth
22562 // Returns the new recommended with, after constraints applied
22563 function constrainWidth(col, width){
22564 var newWidth = width;
22566 // If the new width would be less than the column's allowably minimum width, don't allow it
22567 if (col.minWidth && newWidth < col.minWidth) {
22568 newWidth = col.minWidth;
22570 else if (col.maxWidth && newWidth > col.maxWidth) {
22571 newWidth = col.maxWidth;
22579 * Our approach to event handling aims to deal with both touch devices and mouse devices
22580 * We register down handlers on both touch and mouse. When a touchstart or mousedown event
22581 * occurs, we register the corresponding touchmove/touchend, or mousemove/mouseend events.
22583 * This way we can listen for both without worrying about the fact many touch devices also emulate
22584 * mouse events - basically whichever one we hear first is what we'll go with.
22586 function moveFunction(event, args) {
22587 if (event.originalEvent) { event = event.originalEvent; }
22588 event.preventDefault();
22590 x = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22592 if (x < 0) { x = 0; }
22593 else if (x > uiGridCtrl.grid.gridWidth) { x = uiGridCtrl.grid.gridWidth; }
22595 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22597 // Don't resize if it's disabled on this column
22598 if (col.colDef.enableColumnResizing === false) {
22602 if (!uiGridCtrl.grid.element.hasClass('column-resizing')) {
22603 uiGridCtrl.grid.element.addClass('column-resizing');
22606 // Get the diff along the X axis
22607 var xDiff = x - startX;
22609 // Get the width that this mouse would give the column
22610 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22612 // check we're not outside the allowable bounds for this column
22613 x = x + ( constrainWidth(col, newWidth) - newWidth ) * rtlMultiplier;
22615 resizeOverlay.css({ left: x + 'px' });
22617 uiGridCtrl.fireEvent(uiGridConstants.events.ITEM_DRAGGING);
22621 function upFunction(event, args) {
22622 if (event.originalEvent) { event = event.originalEvent; }
22623 event.preventDefault();
22625 uiGridCtrl.grid.element.removeClass('column-resizing');
22627 resizeOverlay.remove();
22629 // Resize the column
22630 x = (event.changedTouches ? event.changedTouches[0] : event).clientX - gridLeft;
22631 var xDiff = x - startX;
22634 // no movement, so just reset event handlers, including turning back on both
22635 // down events - we turned one off when this event started
22641 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22643 // Don't resize if it's disabled on this column
22644 if (col.colDef.enableColumnResizing === false) {
22648 // Get the new width
22649 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22651 // check we're not outside the allowable bounds for this column
22652 col.width = constrainWidth(col, newWidth);
22653 col.hasCustomWidth = true;
22655 refreshCanvas(xDiff);
22657 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff);
22659 // stop listening of up and move events - wait for next down
22660 // reset the down events - we will have turned one off when this event started
22666 var downFunction = function(event, args) {
22667 if (event.originalEvent) { event = event.originalEvent; }
22668 event.stopPropagation();
22670 // Get the left offset of the grid
22671 // gridLeft = uiGridCtrl.grid.element[0].offsetLeft;
22672 gridLeft = uiGridCtrl.grid.element[0].getBoundingClientRect().left;
22674 // Get the starting X position, which is the X coordinate of the click minus the grid's offset
22675 startX = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22677 // Append the resizer overlay
22678 uiGridCtrl.grid.element.append(resizeOverlay);
22680 // Place the resizer overlay at the start position
22681 resizeOverlay.css({ left: startX });
22683 // Add handlers for move and up events - if we were mousedown then we listen for mousemove and mouseup, if
22684 // we were touchdown then we listen for touchmove and touchup. Also remove the handler for the equivalent
22685 // down event - so if we're touchdown, then remove the mousedown handler until this event is over, if we're
22686 // mousedown then remove the touchdown handler until this event is over, this avoids processing duplicate events
22687 if ( event.type === 'touchstart' ){
22688 $document.on('touchend', upFunction);
22689 $document.on('touchmove', moveFunction);
22690 $elm.off('mousedown', downFunction);
22692 $document.on('mouseup', upFunction);
22693 $document.on('mousemove', moveFunction);
22694 $elm.off('touchstart', downFunction);
22698 var onDownEvents = function() {
22699 $elm.on('mousedown', downFunction);
22700 $elm.on('touchstart', downFunction);
22703 var offAllEvents = function() {
22704 $document.off('mouseup', upFunction);
22705 $document.off('touchend', upFunction);
22706 $document.off('mousemove', moveFunction);
22707 $document.off('touchmove', moveFunction);
22708 $elm.off('mousedown', downFunction);
22709 $elm.off('touchstart', downFunction);
22715 // On doubleclick, resize to fit all rendered cells
22716 var dblClickFn = function(event, args){
22717 event.stopPropagation();
22719 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22721 // Don't resize if it's disabled on this column
22722 if (col.colDef.enableColumnResizing === false) {
22726 // Go through the rendered rows and find out the max size for the data in this column
22730 // Get the parent render container element
22731 var renderContainerElm = gridUtil.closestElm($elm, '.ui-grid-render-container');
22733 // 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
22734 var cells = renderContainerElm.querySelectorAll('.' + uiGridConstants.COL_CLASS_PREFIX + col.uid + ' .ui-grid-cell-contents');
22735 Array.prototype.forEach.call(cells, function (cell) {
22736 // Get the cell width
22737 // gridUtil.logDebug('width', gridUtil.elementWidth(cell));
22739 // Account for the menu button if it exists
22741 if (angular.element(cell).parent().hasClass('ui-grid-header-cell')) {
22742 menuButton = angular.element(cell).parent()[0].querySelectorAll('.ui-grid-column-menu-button');
22745 gridUtil.fakeElement(cell, {}, function(newElm) {
22746 // Make the element float since it's a div and can expand to fill its container
22747 var e = angular.element(newElm);
22748 e.attr('style', 'float: left');
22750 var width = gridUtil.elementWidth(e);
22753 var menuButtonWidth = gridUtil.elementWidth(menuButton);
22754 width = width + menuButtonWidth;
22757 if (width > maxWidth) {
22759 xDiff = maxWidth - width;
22764 // check we're not outside the allowable bounds for this column
22765 col.width = constrainWidth(col, maxWidth);
22766 col.hasCustomWidth = true;
22768 refreshCanvas(xDiff);
22770 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); };
22771 $elm.on('dblclick', dblClickFn);
22773 $elm.on('$destroy', function() {
22774 $elm.off('dblclick', dblClickFn);
22790 * @name ui.grid.rowEdit
22793 * # ui.grid.rowEdit
22795 * <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>
22797 * This module extends the edit feature to provide tracking and saving of rows
22798 * of data. The tutorial provides more information on how this feature is best
22799 * used {@link tutorial/205_row_editable here}.
22801 * This feature depends on usage of the ui-grid-edit feature, and also benefits
22802 * from use of ui-grid-cellNav to provide the full spreadsheet-like editing
22807 var module = angular.module('ui.grid.rowEdit', ['ui.grid', 'ui.grid.edit', 'ui.grid.cellNav']);
22811 * @name ui.grid.rowEdit.constant:uiGridRowEditConstants
22813 * @description constants available in row edit module
22815 module.constant('uiGridRowEditConstants', {
22820 * @name ui.grid.rowEdit.service:uiGridRowEditService
22822 * @description Services for row editing features
22824 module.service('uiGridRowEditService', ['$interval', '$q', 'uiGridConstants', 'uiGridRowEditConstants', 'gridUtil',
22825 function ($interval, $q, uiGridConstants, uiGridRowEditConstants, gridUtil) {
22829 initializeGrid: function (scope, grid) {
22832 * @name ui.grid.rowEdit.api:PublicApi
22834 * @description Public Api for rowEdit feature
22844 * @eventOf ui.grid.rowEdit.api:PublicApi
22846 * @description raised when a row is ready for saving. Once your
22847 * row has saved you may need to use angular.extend to update the
22848 * data entity with any changed data from your save (for example,
22849 * lock version information if you're using optimistic locking,
22850 * or last update time/user information).
22852 * Your method should call setSavePromise somewhere in the body before
22853 * returning control. The feature will then wait, with the gridRow greyed out
22854 * whilst this promise is being resolved.
22857 * gridApi.rowEdit.on.saveRow(scope,function(rowEntity){})
22859 * and somewhere within the event handler:
22861 * gridApi.rowEdit.setSavePromise( rowEntity, savePromise)
22863 * @param {object} rowEntity the options.data element that was edited
22864 * @returns {promise} Your saveRow method should return a promise, the
22865 * promise should either be resolved (implying successful save), or
22866 * rejected (implying an error).
22868 saveRow: function (rowEntity) {
22876 * @methodOf ui.grid.rowEdit.api:PublicApi
22877 * @name setSavePromise
22878 * @description Sets the promise associated with the row save, mandatory that
22879 * the saveRow event handler calls this method somewhere before returning.
22881 * gridApi.rowEdit.setSavePromise(rowEntity, savePromise)
22883 * @param {object} rowEntity a data row from the grid for which a save has
22885 * @param {promise} savePromise the promise that will be resolved when the
22886 * save is successful, or rejected if the save fails
22889 setSavePromise: function ( rowEntity, savePromise) {
22890 service.setSavePromise(grid, rowEntity, savePromise);
22894 * @methodOf ui.grid.rowEdit.api:PublicApi
22895 * @name getDirtyRows
22896 * @description Returns all currently dirty rows
22898 * gridApi.rowEdit.getDirtyRows(grid)
22900 * @returns {array} An array of gridRows that are currently dirty
22903 getDirtyRows: function () {
22904 return grid.rowEdit.dirtyRows ? grid.rowEdit.dirtyRows : [];
22908 * @methodOf ui.grid.rowEdit.api:PublicApi
22909 * @name getErrorRows
22910 * @description Returns all currently errored rows
22912 * gridApi.rowEdit.getErrorRows(grid)
22914 * @returns {array} An array of gridRows that are currently in error
22917 getErrorRows: function () {
22918 return grid.rowEdit.errorRows ? grid.rowEdit.errorRows : [];
22922 * @methodOf ui.grid.rowEdit.api:PublicApi
22923 * @name flushDirtyRows
22924 * @description Triggers a save event for all currently dirty rows, could
22925 * be used where user presses a save button or navigates away from the page
22927 * gridApi.rowEdit.flushDirtyRows(grid)
22929 * @returns {promise} a promise that represents the aggregate of all
22930 * of the individual save promises - i.e. it will be resolved when all
22931 * the individual save promises have been resolved.
22934 flushDirtyRows: function () {
22935 return service.flushDirtyRows(grid);
22940 * @methodOf ui.grid.rowEdit.api:PublicApi
22941 * @name setRowsDirty
22942 * @description Sets each of the rows passed in dataRows
22943 * to be dirty. note that if you have only just inserted the
22944 * rows into your data you will need to wait for a $digest cycle
22945 * before the gridRows are present - so often you would wrap this
22946 * call in a $interval or $timeout
22948 * $interval( function() {
22949 * gridApi.rowEdit.setRowsDirty(myDataRows);
22952 * @param {array} dataRows the data entities for which the gridRows
22953 * should be set dirty.
22956 setRowsDirty: function ( dataRows) {
22957 service.setRowsDirty(grid, dataRows);
22962 * @methodOf ui.grid.rowEdit.api:PublicApi
22963 * @name setRowsClean
22964 * @description Sets each of the rows passed in dataRows
22965 * to be clean, removing them from the dirty cache and the error cache,
22966 * and clearing the error flag and the dirty flag
22968 * var gridRows = $scope.gridApi.rowEdit.getDirtyRows();
22969 * var dataRows = gridRows.map( function( gridRow ) { return gridRow.entity; });
22970 * $scope.gridApi.rowEdit.setRowsClean( dataRows );
22972 * @param {array} dataRows the data entities for which the gridRows
22973 * should be set clean.
22976 setRowsClean: function ( dataRows) {
22977 service.setRowsClean(grid, dataRows);
22983 grid.api.registerEventsFromObject(publicApi.events);
22984 grid.api.registerMethodsFromObject(publicApi.methods);
22986 grid.api.core.on.renderingComplete( scope, function ( gridApi ) {
22987 grid.api.edit.on.afterCellEdit( scope, service.endEditCell );
22988 grid.api.edit.on.beginCellEdit( scope, service.beginEditCell );
22989 grid.api.edit.on.cancelCellEdit( scope, service.cancelEditCell );
22991 if ( grid.api.cellNav ) {
22992 grid.api.cellNav.on.navigate( scope, service.navigate );
22998 defaultGridOptions: function (gridOptions) {
23002 * @name ui.grid.rowEdit.api:GridOptions
23004 * @description Options for configuring the rowEdit feature, these are available to be
23005 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
23013 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23015 * @description Returns a function that saves the specified row from the grid,
23016 * and returns a promise
23017 * @param {object} grid the grid for which dirty rows should be flushed
23018 * @param {GridRow} gridRow the row that should be saved
23019 * @returns {function} the saveRow function returns a function. That function
23020 * in turn, when called, returns a promise relating to the save callback
23022 saveRow: function ( grid, gridRow ) {
23025 return function() {
23026 gridRow.isSaving = true;
23028 if ( gridRow.rowEditSavePromise ){
23029 // don't save the row again if it's already saving - that causes stale object exceptions
23030 return gridRow.rowEditSavePromise;
23033 var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity );
23035 if ( gridRow.rowEditSavePromise ){
23036 gridRow.rowEditSavePromise.then( self.processSuccessPromise( grid, gridRow ), self.processErrorPromise( grid, gridRow ));
23038 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' );
23047 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23048 * @name setSavePromise
23049 * @description Sets the promise associated with the row save, mandatory that
23050 * the saveRow event handler calls this method somewhere before returning.
23052 * gridApi.rowEdit.setSavePromise(grid, rowEntity)
23054 * @param {object} grid the grid for which dirty rows should be returned
23055 * @param {object} rowEntity a data row from the grid for which a save has
23057 * @param {promise} savePromise the promise that will be resolved when the
23058 * save is successful, or rejected if the save fails
23061 setSavePromise: function (grid, rowEntity, savePromise) {
23062 var gridRow = grid.getRow( rowEntity );
23063 gridRow.rowEditSavePromise = savePromise;
23069 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23070 * @name processSuccessPromise
23071 * @description Returns a function that processes the successful
23072 * resolution of a save promise
23073 * @param {object} grid the grid for which the promise should be processed
23074 * @param {GridRow} gridRow the row that has been saved
23075 * @returns {function} the success handling function
23077 processSuccessPromise: function ( grid, gridRow ) {
23080 return function() {
23081 delete gridRow.isSaving;
23082 delete gridRow.isDirty;
23083 delete gridRow.isError;
23084 delete gridRow.rowEditSaveTimer;
23085 delete gridRow.rowEditSavePromise;
23086 self.removeRow( grid.rowEdit.errorRows, gridRow );
23087 self.removeRow( grid.rowEdit.dirtyRows, gridRow );
23094 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23095 * @name processErrorPromise
23096 * @description Returns a function that processes the failed
23097 * resolution of a save promise
23098 * @param {object} grid the grid for which the promise should be processed
23099 * @param {GridRow} gridRow the row that is now in error
23100 * @returns {function} the error handling function
23102 processErrorPromise: function ( grid, gridRow ) {
23103 return function() {
23104 delete gridRow.isSaving;
23105 delete gridRow.rowEditSaveTimer;
23106 delete gridRow.rowEditSavePromise;
23108 gridRow.isError = true;
23110 if (!grid.rowEdit.errorRows){
23111 grid.rowEdit.errorRows = [];
23113 if (!service.isRowPresent( grid.rowEdit.errorRows, gridRow ) ){
23114 grid.rowEdit.errorRows.push( gridRow );
23122 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23124 * @description Removes a row from a cache of rows - either
23125 * grid.rowEdit.errorRows or grid.rowEdit.dirtyRows. If the row
23126 * is not present silently does nothing.
23127 * @param {array} rowArray the array from which to remove the row
23128 * @param {GridRow} gridRow the row that should be removed
23130 removeRow: function( rowArray, removeGridRow ){
23131 if (typeof(rowArray) === 'undefined' || rowArray === null){
23135 rowArray.forEach( function( gridRow, index ){
23136 if ( gridRow.uid === removeGridRow.uid ){
23137 rowArray.splice( index, 1);
23145 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23146 * @name isRowPresent
23147 * @description Checks whether a row is already present
23148 * in the given array
23149 * @param {array} rowArray the array in which to look for the row
23150 * @param {GridRow} gridRow the row that should be looked for
23152 isRowPresent: function( rowArray, removeGridRow ){
23153 var present = false;
23154 rowArray.forEach( function( gridRow, index ){
23155 if ( gridRow.uid === removeGridRow.uid ){
23165 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23166 * @name flushDirtyRows
23167 * @description Triggers a save event for all currently dirty rows, could
23168 * be used where user presses a save button or navigates away from the page
23170 * gridApi.rowEdit.flushDirtyRows(grid)
23172 * @param {object} grid the grid for which dirty rows should be flushed
23173 * @returns {promise} a promise that represents the aggregate of all
23174 * of the individual save promises - i.e. it will be resolved when all
23175 * the individual save promises have been resolved.
23178 flushDirtyRows: function(grid){
23180 grid.api.rowEdit.getDirtyRows().forEach( function( gridRow ){
23181 service.saveRow( grid, gridRow )();
23182 promises.push( gridRow.rowEditSavePromise );
23185 return $q.all( promises );
23191 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23192 * @name endEditCell
23193 * @description Receives an afterCellEdit event from the edit function,
23194 * and sets flags as appropriate. Only the rowEntity parameter
23195 * is processed, although other params are available. Grid
23196 * is automatically provided by the gridApi.
23197 * @param {object} rowEntity the data entity for which the cell
23200 endEditCell: function( rowEntity, colDef, newValue, previousValue ){
23201 var grid = this.grid;
23202 var gridRow = grid.getRow( rowEntity );
23203 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, dirty flag cannot be set' ); return; }
23205 if ( newValue !== previousValue || gridRow.isDirty ){
23206 if ( !grid.rowEdit.dirtyRows ){
23207 grid.rowEdit.dirtyRows = [];
23210 if ( !gridRow.isDirty ){
23211 gridRow.isDirty = true;
23212 grid.rowEdit.dirtyRows.push( gridRow );
23215 delete gridRow.isError;
23217 service.considerSetTimer( grid, gridRow );
23224 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23225 * @name beginEditCell
23226 * @description Receives a beginCellEdit event from the edit function,
23227 * and cancels any rowEditSaveTimers if present, as the user is still editing
23228 * this row. Only the rowEntity parameter
23229 * is processed, although other params are available. Grid
23230 * is automatically provided by the gridApi.
23231 * @param {object} rowEntity the data entity for which the cell
23232 * editing has commenced
23234 beginEditCell: function( rowEntity, colDef ){
23235 var grid = this.grid;
23236 var gridRow = grid.getRow( rowEntity );
23237 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be cancelled' ); return; }
23239 service.cancelTimer( grid, gridRow );
23245 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23246 * @name cancelEditCell
23247 * @description Receives a cancelCellEdit event from the edit function,
23248 * and if the row was already dirty, restarts the save timer. If the row
23249 * was not already dirty, then it's not dirty now either and does nothing.
23251 * Only the rowEntity parameter
23252 * is processed, although other params are available. Grid
23253 * is automatically provided by the gridApi.
23255 * @param {object} rowEntity the data entity for which the cell
23256 * editing was cancelled
23258 cancelEditCell: function( rowEntity, colDef ){
23259 var grid = this.grid;
23260 var gridRow = grid.getRow( rowEntity );
23261 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be set' ); return; }
23263 service.considerSetTimer( grid, gridRow );
23269 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23271 * @description cellNav tells us that the selected cell has changed. If
23272 * the new row had a timer running, then stop it similar to in a beginCellEdit
23273 * call. If the old row is dirty and not the same as the new row, then
23274 * start a timer on it.
23275 * @param {object} newRowCol the row and column that were selected
23276 * @param {object} oldRowCol the row and column that was left
23279 navigate: function( newRowCol, oldRowCol ){
23280 var grid = this.grid;
23281 if ( newRowCol.row.rowEditSaveTimer ){
23282 service.cancelTimer( grid, newRowCol.row );
23285 if ( oldRowCol && oldRowCol.row && oldRowCol.row !== newRowCol.row ){
23286 service.considerSetTimer( grid, oldRowCol.row );
23293 * @propertyOf ui.grid.rowEdit.api:GridOptions
23294 * @name rowEditWaitInterval
23295 * @description How long the grid should wait for another change on this row
23296 * before triggering a save (in milliseconds). If set to -1, then saves are
23297 * never triggered by timer (implying that the user will call flushDirtyRows()
23301 * Setting the wait interval to 4 seconds
23303 * $scope.gridOptions = { rowEditWaitInterval: 4000 }
23309 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23310 * @name considerSetTimer
23311 * @description Consider setting a timer on this row (if it is dirty). if there is a timer running
23312 * on the row and the row isn't currently saving, cancel it, using cancelTimer, then if the row is
23313 * dirty and not currently saving then set a new timer
23314 * @param {object} grid the grid for which we are processing
23315 * @param {GridRow} gridRow the row for which the timer should be adjusted
23318 considerSetTimer: function( grid, gridRow ){
23319 service.cancelTimer( grid, gridRow );
23321 if ( gridRow.isDirty && !gridRow.isSaving ){
23322 if ( grid.options.rowEditWaitInterval !== -1 ){
23323 var waitTime = grid.options.rowEditWaitInterval ? grid.options.rowEditWaitInterval : 2000;
23324 gridRow.rowEditSaveTimer = $interval( service.saveRow( grid, gridRow ), waitTime, 1);
23332 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23333 * @name cancelTimer
23334 * @description cancel the $interval for any timer running on this row
23335 * then delete the timer itself
23336 * @param {object} grid the grid for which we are processing
23337 * @param {GridRow} gridRow the row for which the timer should be adjusted
23340 cancelTimer: function( grid, gridRow ){
23341 if ( gridRow.rowEditSaveTimer && !gridRow.isSaving ){
23342 $interval.cancel(gridRow.rowEditSaveTimer);
23343 delete gridRow.rowEditSaveTimer;
23350 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23351 * @name setRowsDirty
23352 * @description Sets each of the rows passed in dataRows
23353 * to be dirty. note that if you have only just inserted the
23354 * rows into your data you will need to wait for a $digest cycle
23355 * before the gridRows are present - so often you would wrap this
23356 * call in a $interval or $timeout
23358 * $interval( function() {
23359 * gridApi.rowEdit.setRowsDirty( myDataRows);
23362 * @param {object} grid the grid for which rows should be set dirty
23363 * @param {array} dataRows the data entities for which the gridRows
23364 * should be set dirty.
23367 setRowsDirty: function( grid, myDataRows ) {
23369 myDataRows.forEach( function( value, index ){
23370 gridRow = grid.getRow( value );
23372 if ( !grid.rowEdit.dirtyRows ){
23373 grid.rowEdit.dirtyRows = [];
23376 if ( !gridRow.isDirty ){
23377 gridRow.isDirty = true;
23378 grid.rowEdit.dirtyRows.push( gridRow );
23381 delete gridRow.isError;
23383 service.considerSetTimer( grid, gridRow );
23385 gridUtil.logError( "requested row not found in rowEdit.setRowsDirty, row was: " + value );
23393 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
23394 * @name setRowsClean
23395 * @description Sets each of the rows passed in dataRows
23396 * to be clean, clearing the dirty flag and the error flag, and removing
23397 * the rows from the dirty and error caches.
23398 * @param {object} grid the grid for which rows should be set clean
23399 * @param {array} dataRows the data entities for which the gridRows
23400 * should be set clean.
23403 setRowsClean: function( grid, myDataRows ) {
23406 myDataRows.forEach( function( value, index ){
23407 gridRow = grid.getRow( value );
23409 delete gridRow.isDirty;
23410 service.removeRow( grid.rowEdit.dirtyRows, gridRow );
23411 service.cancelTimer( grid, gridRow );
23413 delete gridRow.isError;
23414 service.removeRow( grid.rowEdit.errorRows, gridRow );
23416 gridUtil.logError( "requested row not found in rowEdit.setRowsClean, row was: " + value );
23429 * @name ui.grid.rowEdit.directive:uiGridEdit
23433 * @description Adds row editing features to the ui-grid-edit directive.
23436 module.directive('uiGridRowEdit', ['gridUtil', 'uiGridRowEditService', 'uiGridEditConstants',
23437 function (gridUtil, uiGridRowEditService, uiGridEditConstants) {
23441 require: '^uiGrid',
23443 compile: function () {
23445 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
23446 uiGridRowEditService.initializeGrid($scope, uiGridCtrl.grid);
23448 post: function ($scope, $elm, $attrs, uiGridCtrl) {
23458 * @name ui.grid.rowEdit.directive:uiGridViewport
23461 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
23462 * for the grid row to allow coloring of saving and error rows
23464 module.directive('uiGridViewport',
23465 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
23466 function ($compile, uiGridConstants, gridUtil, $parse) {
23468 priority: -200, // run after default directive
23470 compile: function ($elm, $attrs) {
23471 var rowRepeatDiv = angular.element($elm.children().children()[0]);
23473 var existingNgClass = rowRepeatDiv.attr("ng-class");
23474 var newNgClass = '';
23475 if ( existingNgClass ) {
23476 newNgClass = existingNgClass.slice(0, -1) + ", 'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
23478 newNgClass = "{'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
23480 rowRepeatDiv.attr("ng-class", newNgClass);
23483 pre: function ($scope, $elm, $attrs, controllers) {
23486 post: function ($scope, $elm, $attrs, controllers) {
23500 * @name ui.grid.saveState
23503 * # ui.grid.saveState
23505 * <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>
23507 * This module provides the ability to save the grid state, and restore
23508 * it when the user returns to the page.
23510 * No UI is provided, the caller should provide their own UI/buttons
23511 * as appropriate. Usually the navigate events would be used to save
23512 * the grid state and restore it.
23517 * <div doc-module-components="ui.grid.save-state"></div>
23520 var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning', 'ui.grid.treeView']);
23524 * @name ui.grid.saveState.constant:uiGridSaveStateConstants
23526 * @description constants available in save state module
23529 module.constant('uiGridSaveStateConstants', {
23530 featureName: 'saveState'
23535 * @name ui.grid.saveState.service:uiGridSaveStateService
23537 * @description Services for saveState feature
23539 module.service('uiGridSaveStateService', ['$q', 'uiGridSaveStateConstants', 'gridUtil', '$compile', '$interval', 'uiGridConstants',
23540 function ($q, uiGridSaveStateConstants, gridUtil, $compile, $interval, uiGridConstants ) {
23544 initializeGrid: function (grid) {
23546 //add feature namespace and any properties to grid for needed state
23547 grid.saveState = {};
23548 this.defaultGridOptions(grid.options);
23552 * @name ui.grid.saveState.api:PublicApi
23554 * @description Public Api for saveState feature
23566 * @methodOf ui.grid.saveState.api:PublicApi
23567 * @description Packages the current state of the grid into
23568 * an object, and provides it to the user for saving
23569 * @returns {object} the state as a javascript object that can be saved
23571 save: function () {
23572 return service.save(grid);
23577 * @methodOf ui.grid.saveState.api:PublicApi
23578 * @description Restores the provided state into the grid
23579 * @param {scope} $scope a scope that we can broadcast on
23580 * @param {object} state the state that should be restored into the grid
23582 restore: function ( $scope, state) {
23583 service.restore(grid, $scope, state);
23589 grid.api.registerEventsFromObject(publicApi.events);
23591 grid.api.registerMethodsFromObject(publicApi.methods);
23595 defaultGridOptions: function (gridOptions) {
23596 //default option to true unless it was explicitly set to false
23599 * @name ui.grid.saveState.api:GridOptions
23601 * @description GridOptions for saveState feature, these are available to be
23602 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
23607 * @propertyOf ui.grid.saveState.api:GridOptions
23608 * @description Save the current column widths. Note that unless
23609 * you've provided the user with some way to resize their columns (say
23610 * the resize columns feature), then this makes little sense.
23611 * <br/>Defaults to true
23613 gridOptions.saveWidths = gridOptions.saveWidths !== false;
23617 * @propertyOf ui.grid.saveState.api:GridOptions
23618 * @description Restore the current column order. Note that unless
23619 * you've provided the user with some way to reorder their columns (for
23620 * example the move columns feature), this makes little sense.
23621 * <br/>Defaults to true
23623 gridOptions.saveOrder = gridOptions.saveOrder !== false;
23627 * @propertyOf ui.grid.saveState.api:GridOptions
23628 * @description Save the current scroll position. Note that this
23629 * is saved as the percentage of the grid scrolled - so if your
23630 * user returns to a grid with a significantly different number of
23631 * rows (perhaps some data has been deleted) then the scroll won't
23632 * actually show the same rows as before. If you want to scroll to
23633 * a specific row then you should instead use the saveFocus option, which
23636 * Note that this element will only be saved if the cellNav feature is
23638 * <br/>Defaults to false
23640 gridOptions.saveScroll = gridOptions.saveScroll === true;
23644 * @propertyOf ui.grid.saveState.api:GridOptions
23645 * @description Save the current focused cell. On returning
23646 * to this focused cell we'll also scroll. This option is
23647 * preferred to the saveScroll option, so is set to true by
23648 * default. If saveScroll is set to true then this option will
23651 * By default this option saves the current row number and column
23652 * number, and returns to that row and column. However, if you define
23653 * a saveRowIdentity function, then it will return you to the currently
23654 * selected column within that row (in a business sense - so if some
23655 * rows have been deleted, it will still find the same data, presuming it
23656 * still exists in the list. If it isn't in the list then it will instead
23657 * return to the same row number - i.e. scroll percentage)
23659 * Note that this option will do nothing if the cellNav
23660 * feature is not enabled.
23662 * <br/>Defaults to true (unless saveScroll is true)
23664 gridOptions.saveFocus = gridOptions.saveScroll !== true && gridOptions.saveFocus !== false;
23667 * @name saveRowIdentity
23668 * @propertyOf ui.grid.saveState.api:GridOptions
23669 * @description A function that can be called, passing in a rowEntity,
23670 * and that will return a unique id for that row. This might simply
23671 * return the `id` field from that row (if you have one), or it might
23672 * concatenate some fields within the row to make a unique value.
23674 * This value will be used to find the same row again and set the focus
23675 * to it, if it exists when we return.
23677 * <br/>Defaults to undefined
23681 * @name saveVisible
23682 * @propertyOf ui.grid.saveState.api:GridOptions
23683 * @description Save whether or not columns are visible.
23685 * <br/>Defaults to true
23687 gridOptions.saveVisible = gridOptions.saveVisible !== false;
23691 * @propertyOf ui.grid.saveState.api:GridOptions
23692 * @description Save the current sort state for each column
23694 * <br/>Defaults to true
23696 gridOptions.saveSort = gridOptions.saveSort !== false;
23700 * @propertyOf ui.grid.saveState.api:GridOptions
23701 * @description Save the current filter state for each column
23703 * <br/>Defaults to true
23705 gridOptions.saveFilter = gridOptions.saveFilter !== false;
23708 * @name saveSelection
23709 * @propertyOf ui.grid.saveState.api:GridOptions
23710 * @description Save the currently selected rows. If the `saveRowIdentity` callback
23711 * is defined, then it will save the id of the row and select that. If not, then
23712 * it will attempt to select the rows by row number, which will give the wrong results
23713 * if the data set has changed in the mean-time.
23715 * Note that this option only does anything
23716 * if the selection feature is enabled.
23718 * <br/>Defaults to true
23720 gridOptions.saveSelection = gridOptions.saveSelection !== false;
23723 * @name saveGrouping
23724 * @propertyOf ui.grid.saveState.api:GridOptions
23725 * @description Save the grouping configuration. If set to true and the
23726 * grouping feature is not enabled then does nothing.
23728 * <br/>Defaults to true
23730 gridOptions.saveGrouping = gridOptions.saveGrouping !== false;
23733 * @name saveGroupingExpandedStates
23734 * @propertyOf ui.grid.saveState.api:GridOptions
23735 * @description Save the grouping row expanded states. If set to true and the
23736 * grouping feature is not enabled then does nothing.
23738 * This can be quite a bit of data, in many cases you wouldn't want to save this
23741 * <br/>Defaults to false
23743 gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true;
23746 * @name savePinning
23747 * @propertyOf ui.grid.saveState.api:GridOptions
23748 * @description Save pinning state for columns.
23750 * <br/>Defaults to true
23752 gridOptions.savePinning = gridOptions.savePinning !== false;
23755 * @name saveTreeView
23756 * @propertyOf ui.grid.saveState.api:GridOptions
23757 * @description Save the treeView configuration. If set to true and the
23758 * treeView feature is not enabled then does nothing.
23760 * <br/>Defaults to true
23762 gridOptions.saveTreeView = gridOptions.saveTreeView !== false;
23770 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23771 * @description Saves the current grid state into an object, and
23772 * passes that object back to the caller
23773 * @param {Grid} grid the grid whose state we'd like to save
23774 * @returns {object} the state ready to be saved
23776 save: function (grid) {
23777 var savedState = {};
23779 savedState.columns = service.saveColumns( grid );
23780 savedState.scrollFocus = service.saveScrollFocus( grid );
23781 savedState.selection = service.saveSelection( grid );
23782 savedState.grouping = service.saveGrouping( grid );
23783 savedState.treeView = service.saveTreeView( grid );
23792 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23793 * @description Applies the provided state to the grid
23795 * @param {Grid} grid the grid whose state we'd like to restore
23796 * @param {scope} $scope a scope that we can broadcast on
23797 * @param {object} state the state we'd like to restore
23799 restore: function( grid, $scope, state ){
23800 if ( state.columns ) {
23801 service.restoreColumns( grid, state.columns );
23804 if ( state.scrollFocus ){
23805 service.restoreScrollFocus( grid, $scope, state.scrollFocus );
23808 if ( state.selection ){
23809 service.restoreSelection( grid, state.selection );
23812 if ( state.grouping ){
23813 service.restoreGrouping( grid, state.grouping );
23816 if ( state.treeView ){
23817 service.restoreTreeView( grid, state.treeView );
23826 * @name saveColumns
23827 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23828 * @description Saves the column setup, including sort, filters, ordering,
23829 * pinning and column widths.
23831 * Works through the current columns, storing them in order. Stores the
23832 * column name, then the visible flag, width, sort and filters for each column.
23834 * @param {Grid} grid the grid whose state we'd like to save
23835 * @returns {array} the columns state ready to be saved
23837 saveColumns: function( grid ) {
23839 grid.getOnlyDataColumns().forEach( function( column ) {
23840 var savedColumn = {};
23841 savedColumn.name = column.name;
23843 if ( grid.options.saveVisible ){
23844 savedColumn.visible = column.visible;
23847 if ( grid.options.saveWidths ){
23848 savedColumn.width = column.width;
23851 // these two must be copied, not just pointed too - otherwise our saved state is pointing to the same object as current state
23852 if ( grid.options.saveSort ){
23853 savedColumn.sort = angular.copy( column.sort );
23856 if ( grid.options.saveFilter ){
23857 savedColumn.filters = [];
23858 column.filters.forEach( function( filter ){
23859 var copiedFilter = {};
23860 angular.forEach( filter, function( value, key) {
23861 if ( key !== 'condition' && key !== '$$hashKey' && key !== 'placeholder'){
23862 copiedFilter[key] = value;
23865 savedColumn.filters.push(copiedFilter);
23869 if ( !!grid.api.pinning && grid.options.savePinning ){
23870 savedColumn.pinned = column.renderContainer ? column.renderContainer : '';
23873 columns.push( savedColumn );
23882 * @name saveScrollFocus
23883 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23884 * @description Saves the currently scroll or focus.
23886 * If cellNav isn't present then does nothing - we can't return
23887 * to the scroll position without cellNav anyway.
23889 * If the cellNav module is present, and saveFocus is true, then
23890 * it saves the currently focused cell. If rowIdentity is present
23891 * then saves using rowIdentity, otherwise saves visibleRowNum.
23893 * If the cellNav module is not present, and saveScroll is true, then
23894 * it approximates the current scroll row and column, and saves that.
23896 * @param {Grid} grid the grid whose state we'd like to save
23897 * @returns {object} the selection state ready to be saved
23899 saveScrollFocus: function( grid ){
23900 if ( !grid.api.cellNav ){
23904 var scrollFocus = {};
23905 if ( grid.options.saveFocus ){
23906 scrollFocus.focus = true;
23907 var rowCol = grid.api.cellNav.getFocusedCell();
23908 if ( rowCol !== null ) {
23909 if ( rowCol.col !== null ){
23910 scrollFocus.colName = rowCol.col.colDef.name;
23912 if ( rowCol.row !== null ){
23913 scrollFocus.rowVal = service.getRowVal( grid, rowCol.row );
23918 if ( grid.options.saveScroll || grid.options.saveFocus && !scrollFocus.colName && !scrollFocus.rowVal ) {
23919 scrollFocus.focus = false;
23920 if ( grid.renderContainers.body.prevRowScrollIndex ){
23921 scrollFocus.rowVal = service.getRowVal( grid, grid.renderContainers.body.visibleRowCache[ grid.renderContainers.body.prevRowScrollIndex ]);
23924 if ( grid.renderContainers.body.prevColScrollIndex ){
23925 scrollFocus.colName = grid.renderContainers.body.visibleColumnCache[ grid.renderContainers.body.prevColScrollIndex ].name;
23929 return scrollFocus;
23935 * @name saveSelection
23936 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23937 * @description Saves the currently selected rows, if the selection feature is enabled
23938 * @param {Grid} grid the grid whose state we'd like to save
23939 * @returns {array} the selection state ready to be saved
23941 saveSelection: function( grid ){
23942 if ( !grid.api.selection || !grid.options.saveSelection ){
23946 var selection = grid.api.selection.getSelectedGridRows().map( function( gridRow ) {
23947 return service.getRowVal( grid, gridRow );
23956 * @name saveGrouping
23957 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23958 * @description Saves the grouping state, if the grouping feature is enabled
23959 * @param {Grid} grid the grid whose state we'd like to save
23960 * @returns {object} the grouping state ready to be saved
23962 saveGrouping: function( grid ){
23963 if ( !grid.api.grouping || !grid.options.saveGrouping ){
23967 return grid.api.grouping.getGrouping( grid.options.saveGroupingExpandedStates );
23973 * @name saveTreeView
23974 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23975 * @description Saves the tree view state, if the tree feature is enabled
23976 * @param {Grid} grid the grid whose state we'd like to save
23977 * @returns {object} the tree view state ready to be saved
23979 saveTreeView: function( grid ){
23980 if ( !grid.api.treeView || !grid.options.saveTreeView ){
23984 return grid.api.treeView.getTreeView();
23991 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23992 * @description Helper function that gets either the rowNum or
23993 * the saveRowIdentity, given a gridRow
23994 * @param {Grid} grid the grid the row is in
23995 * @param {GridRow} gridRow the row we want the rowNum for
23996 * @returns {object} an object containing { identity: true/false, row: rowNumber/rowIdentity }
23999 getRowVal: function( grid, gridRow ){
24005 if ( grid.options.saveRowIdentity ){
24006 rowVal.identity = true;
24007 rowVal.row = grid.options.saveRowIdentity( gridRow.entity );
24009 rowVal.identity = false;
24010 rowVal.row = grid.renderContainers.body.visibleRowCache.indexOf( gridRow );
24018 * @name restoreColumns
24019 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24020 * @description Restores the columns, including order, visible, width,
24021 * pinning, sort and filters.
24023 * @param {Grid} grid the grid whose state we'd like to restore
24024 * @param {object} columnsState the list of columns we had before, with their state
24026 restoreColumns: function( grid, columnsState ){
24027 var isSortChanged = false;
24029 columnsState.forEach( function( columnState, index ) {
24030 var currentCol = grid.getColumn( columnState.name );
24032 if ( currentCol && !grid.isRowHeaderColumn(currentCol) ){
24033 if ( grid.options.saveVisible &&
24034 ( currentCol.visible !== columnState.visible ||
24035 currentCol.colDef.visible !== columnState.visible ) ){
24036 currentCol.visible = columnState.visible;
24037 currentCol.colDef.visible = columnState.visible;
24038 grid.api.core.raise.columnVisibilityChanged(currentCol);
24041 if ( grid.options.saveWidths ){
24042 currentCol.width = columnState.width;
24045 if ( grid.options.saveSort &&
24046 !angular.equals(currentCol.sort, columnState.sort) &&
24047 !( currentCol.sort === undefined && angular.isEmpty(columnState.sort) ) ){
24048 currentCol.sort = angular.copy( columnState.sort );
24049 isSortChanged = true;
24052 if ( grid.options.saveFilter &&
24053 !angular.equals(currentCol.filters, columnState.filters ) ){
24054 columnState.filters.forEach( function( filter, index ){
24055 angular.extend( currentCol.filters[index], filter );
24056 if ( typeof(filter.term) === 'undefined' || filter.term === null ){
24057 delete currentCol.filters[index].term;
24060 grid.api.core.raise.filterChanged();
24063 if ( !!grid.api.pinning && grid.options.savePinning && currentCol.renderContainer !== columnState.pinned ){
24064 grid.api.pinning.pinColumn(currentCol, columnState.pinned);
24067 var currentIndex = grid.getOnlyDataColumns().indexOf( currentCol );
24068 if (currentIndex !== -1) {
24069 if (grid.options.saveOrder && currentIndex !== index) {
24070 var column = grid.columns.splice(currentIndex + grid.rowHeaderColumns.length, 1)[0];
24071 grid.columns.splice(index + grid.rowHeaderColumns.length, 0, column);
24077 if ( isSortChanged ) {
24078 grid.api.core.raise.sortChanged( grid, grid.getColumnSorting() );
24085 * @name restoreScrollFocus
24086 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24087 * @description Scrolls to the position that was saved. If focus is true, then
24088 * sets focus to the specified row/col. If focus is false, then scrolls to the
24089 * specified row/col.
24091 * @param {Grid} grid the grid whose state we'd like to restore
24092 * @param {scope} $scope a scope that we can broadcast on
24093 * @param {object} scrollFocusState the scroll/focus state ready to be restored
24095 restoreScrollFocus: function( grid, $scope, scrollFocusState ){
24096 if ( !grid.api.cellNav ){
24101 if ( scrollFocusState.colName ){
24102 var colDefs = grid.options.columnDefs.filter( function( colDef ) { return colDef.name === scrollFocusState.colName; });
24103 if ( colDefs.length > 0 ){
24104 colDef = colDefs[0];
24108 if ( scrollFocusState.rowVal && scrollFocusState.rowVal.row ){
24109 if ( scrollFocusState.rowVal.identity ){
24110 row = service.findRowByIdentity( grid, scrollFocusState.rowVal );
24112 row = grid.renderContainers.body.visibleRowCache[ scrollFocusState.rowVal.row ];
24116 var entity = row && row.entity ? row.entity : null ;
24118 if ( colDef || entity ) {
24119 if (scrollFocusState.focus ){
24120 grid.api.cellNav.scrollToFocus( entity, colDef );
24122 grid.scrollTo( entity, colDef );
24130 * @name restoreSelection
24131 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24132 * @description Selects the rows that are provided in the selection
24133 * state. If you are using `saveRowIdentity` and more than one row matches the identity
24134 * function then only the first is selected.
24135 * @param {Grid} grid the grid whose state we'd like to restore
24136 * @param {object} selectionState the selection state ready to be restored
24138 restoreSelection: function( grid, selectionState ){
24139 if ( !grid.api.selection ){
24143 grid.api.selection.clearSelectedRows();
24145 selectionState.forEach( function( rowVal ) {
24146 if ( rowVal.identity ){
24147 var foundRow = service.findRowByIdentity( grid, rowVal );
24150 grid.api.selection.selectRow( foundRow.entity );
24154 grid.api.selection.selectRowByVisibleIndex( rowVal.row );
24162 * @name restoreGrouping
24163 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24164 * @description Restores the grouping configuration, if the grouping feature
24166 * @param {Grid} grid the grid whose state we'd like to restore
24167 * @param {object} groupingState the grouping state ready to be restored
24169 restoreGrouping: function( grid, groupingState ){
24170 if ( !grid.api.grouping || typeof(groupingState) === 'undefined' || groupingState === null || angular.equals(groupingState, {}) ){
24174 grid.api.grouping.setGrouping( groupingState );
24179 * @name restoreTreeView
24180 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24181 * @description Restores the tree view configuration, if the tree view feature
24183 * @param {Grid} grid the grid whose state we'd like to restore
24184 * @param {object} treeViewState the tree view state ready to be restored
24186 restoreTreeView: function( grid, treeViewState ){
24187 if ( !grid.api.treeView || typeof(treeViewState) === 'undefined' || treeViewState === null || angular.equals(treeViewState, {}) ){
24191 grid.api.treeView.setTreeView( treeViewState );
24196 * @name findRowByIdentity
24197 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
24198 * @description Finds a row given it's identity value, returns the first found row
24199 * if any are found, otherwise returns null if no rows are found.
24200 * @param {Grid} grid the grid whose state we'd like to restore
24201 * @param {object} rowVal the row we'd like to find
24202 * @returns {gridRow} the found row, or null if none found
24204 findRowByIdentity: function( grid, rowVal ){
24205 if ( !grid.options.saveRowIdentity ){
24209 var filteredRows = grid.rows.filter( function( gridRow ) {
24210 if ( grid.options.saveRowIdentity( gridRow.entity ) === rowVal.row ){
24217 if ( filteredRows.length > 0 ){
24218 return filteredRows[0];
24232 * @name ui.grid.saveState.directive:uiGridSaveState
24236 * @description Adds saveState features to grid
24239 <example module="app">
24240 <file name="app.js">
24241 var app = angular.module('app', ['ui.grid', 'ui.grid.saveState']);
24243 app.controller('MainCtrl', ['$scope', function ($scope) {
24245 { name: 'Bob', title: 'CEO' },
24246 { name: 'Frank', title: 'Lowly Developer' }
24249 $scope.gridOptions = {
24252 {name: 'title', enableCellEdit: true}
24258 <file name="index.html">
24259 <div ng-controller="MainCtrl">
24260 <div ui-grid="gridOptions" ui-grid-save-state></div>
24265 module.directive('uiGridSaveState', ['uiGridSaveStateConstants', 'uiGridSaveStateService', 'gridUtil', '$compile',
24266 function (uiGridSaveStateConstants, uiGridSaveStateService, gridUtil, $compile) {
24270 require: '^uiGrid',
24272 link: function ($scope, $elm, $attrs, uiGridCtrl) {
24273 uiGridSaveStateService.initializeGrid(uiGridCtrl.grid);
24285 * @name ui.grid.selection
24288 * # ui.grid.selection
24289 * This module provides row selection
24291 * <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>
24293 * <div doc-module-components="ui.grid.selection"></div>
24296 var module = angular.module('ui.grid.selection', ['ui.grid']);
24300 * @name ui.grid.selection.constant:uiGridSelectionConstants
24302 * @description constants available in selection module
24304 module.constant('uiGridSelectionConstants', {
24305 featureName: "selection",
24306 selectionRowHeaderColName: 'selectionRowHeaderCol'
24309 //add methods to GridRow
24310 angular.module('ui.grid').config(['$provide', function($provide) {
24311 $provide.decorator('GridRow', ['$delegate', function($delegate) {
24315 * @name ui.grid.selection.api:GridRow
24317 * @description GridRow prototype functions added for selection
24322 * @name enableSelection
24323 * @propertyOf ui.grid.selection.api:GridRow
24324 * @description Enable row selection for this row, only settable by internal code.
24326 * The grouping feature, for example, might set group header rows to not be selectable.
24327 * <br/>Defaults to true
24333 * @propertyOf ui.grid.selection.api:GridRow
24334 * @description Selected state of row. Should be readonly. Make any changes to selected state using setSelected().
24335 * <br/>Defaults to false
24341 * @name setSelected
24342 * @methodOf ui.grid.selection.api:GridRow
24343 * @description Sets the isSelected property and updates the selectedCount
24344 * Changes to isSelected state should only be made via this function
24345 * @param {bool} selected value to set
24347 $delegate.prototype.setSelected = function(selected) {
24348 this.isSelected = selected;
24350 this.grid.selection.selectedCount++;
24353 this.grid.selection.selectedCount--;
24363 * @name ui.grid.selection.service:uiGridSelectionService
24365 * @description Services for selection features
24367 module.service('uiGridSelectionService', ['$q', '$templateCache', 'uiGridSelectionConstants', 'gridUtil',
24368 function ($q, $templateCache, uiGridSelectionConstants, gridUtil) {
24372 initializeGrid: function (grid) {
24374 //add feature namespace and any properties to grid for needed
24377 * @name ui.grid.selection.grid:selection
24379 * @description Grid properties and functions added for selection
24381 grid.selection = {};
24382 grid.selection.lastSelectedRow = null;
24383 grid.selection.selectAll = false;
24388 * @name selectedCount
24389 * @propertyOf ui.grid.selection.grid:selection
24390 * @description Current count of selected rows
24392 * var count = grid.selection.selectedCount
24394 grid.selection.selectedCount = 0;
24396 service.defaultGridOptions(grid.options);
24400 * @name ui.grid.selection.api:PublicApi
24402 * @description Public Api for selection feature
24409 * @name rowSelectionChanged
24410 * @eventOf ui.grid.selection.api:PublicApi
24411 * @description is raised after the row.isSelected state is changed
24412 * @param {GridRow} row the row that was selected/deselected
24413 * @param {Event} event object if raised from an event
24415 rowSelectionChanged: function (scope, row, evt) {
24419 * @name rowSelectionChangedBatch
24420 * @eventOf ui.grid.selection.api:PublicApi
24421 * @description is raised after the row.isSelected state is changed
24422 * in bulk, if the `enableSelectionBatchEvent` option is set to true
24423 * (which it is by default). This allows more efficient processing
24425 * @param {array} rows the rows that were selected/deselected
24426 * @param {Event} event object if raised from an event
24428 rowSelectionChangedBatch: function (scope, rows, evt) {
24436 * @name toggleRowSelection
24437 * @methodOf ui.grid.selection.api:PublicApi
24438 * @description Toggles data row as selected or unselected
24439 * @param {object} rowEntity gridOptions.data[] array instance
24440 * @param {Event} event object if raised from an event
24442 toggleRowSelection: function (rowEntity, evt) {
24443 var row = grid.getRow(rowEntity);
24444 if (row !== null) {
24445 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24451 * @methodOf ui.grid.selection.api:PublicApi
24452 * @description Select the data row
24453 * @param {object} rowEntity gridOptions.data[] array instance
24454 * @param {Event} event object if raised from an event
24456 selectRow: function (rowEntity, evt) {
24457 var row = grid.getRow(rowEntity);
24458 if (row !== null && !row.isSelected) {
24459 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24464 * @name selectRowByVisibleIndex
24465 * @methodOf ui.grid.selection.api:PublicApi
24466 * @description Select the specified row by visible index (i.e. if you
24467 * specify row 0 you'll get the first visible row selected). In this context
24468 * visible means of those rows that are theoretically visible (i.e. not filtered),
24469 * rather than rows currently rendered on the screen.
24470 * @param {number} index index within the rowsVisible array
24471 * @param {Event} event object if raised from an event
24473 selectRowByVisibleIndex: function ( rowNum, evt ) {
24474 var row = grid.renderContainers.body.visibleRowCache[rowNum];
24475 if (row !== null && typeof(row) !== 'undefined' && !row.isSelected) {
24476 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24481 * @name unSelectRow
24482 * @methodOf ui.grid.selection.api:PublicApi
24483 * @description UnSelect the data row
24484 * @param {object} rowEntity gridOptions.data[] array instance
24485 * @param {Event} event object if raised from an event
24487 unSelectRow: function (rowEntity, evt) {
24488 var row = grid.getRow(rowEntity);
24489 if (row !== null && row.isSelected) {
24490 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
24495 * @name selectAllRows
24496 * @methodOf ui.grid.selection.api:PublicApi
24497 * @description Selects all rows. Does nothing if multiSelect = false
24498 * @param {Event} event object if raised from an event
24500 selectAllRows: function (evt) {
24501 if (grid.options.multiSelect === false) {
24505 var changedRows = [];
24506 grid.rows.forEach(function (row) {
24507 if ( !row.isSelected && row.enableSelection !== false ){
24508 row.setSelected(true);
24509 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24512 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24513 grid.selection.selectAll = true;
24517 * @name selectAllVisibleRows
24518 * @methodOf ui.grid.selection.api:PublicApi
24519 * @description Selects all visible rows. Does nothing if multiSelect = false
24520 * @param {Event} event object if raised from an event
24522 selectAllVisibleRows: function (evt) {
24523 if (grid.options.multiSelect === false) {
24527 var changedRows = [];
24528 grid.rows.forEach(function (row) {
24530 if (!row.isSelected && row.enableSelection !== false){
24531 row.setSelected(true);
24532 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24535 if (row.isSelected){
24536 row.setSelected(false);
24537 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24541 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24542 grid.selection.selectAll = true;
24546 * @name clearSelectedRows
24547 * @methodOf ui.grid.selection.api:PublicApi
24548 * @description Unselects all rows
24549 * @param {Event} event object if raised from an event
24551 clearSelectedRows: function (evt) {
24552 service.clearSelectedRows(grid, evt);
24556 * @name getSelectedRows
24557 * @methodOf ui.grid.selection.api:PublicApi
24558 * @description returns all selectedRow's entity references
24560 getSelectedRows: function () {
24561 return service.getSelectedRows(grid).map(function (gridRow) {
24562 return gridRow.entity;
24567 * @name getSelectedGridRows
24568 * @methodOf ui.grid.selection.api:PublicApi
24569 * @description returns all selectedRow's as gridRows
24571 getSelectedGridRows: function () {
24572 return service.getSelectedRows(grid);
24576 * @name getSelectedCount
24577 * @methodOf ui.grid.selection.api:PublicApi
24578 * @description returns the number of rows selected
24580 getSelectedCount: function () {
24581 return grid.selection.selectedCount;
24585 * @name setMultiSelect
24586 * @methodOf ui.grid.selection.api:PublicApi
24587 * @description Sets the current gridOption.multiSelect to true or false
24588 * @param {bool} multiSelect true to allow multiple rows
24590 setMultiSelect: function (multiSelect) {
24591 grid.options.multiSelect = multiSelect;
24595 * @name setModifierKeysToMultiSelect
24596 * @methodOf ui.grid.selection.api:PublicApi
24597 * @description Sets the current gridOption.modifierKeysToMultiSelect to true or false
24598 * @param {bool} modifierKeysToMultiSelect true to only allow multiple rows when using ctrlKey or shiftKey is used
24600 setModifierKeysToMultiSelect: function (modifierKeysToMultiSelect) {
24601 grid.options.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
24605 * @name getSelectAllState
24606 * @methodOf ui.grid.selection.api:PublicApi
24607 * @description Returns whether or not the selectAll checkbox is currently ticked. The
24608 * grid doesn't automatically select rows when you add extra data - so when you add data
24609 * you need to explicitly check whether the selectAll is set, and then call setVisible rows
24612 getSelectAllState: function () {
24613 return grid.selection.selectAll;
24620 grid.api.registerEventsFromObject(publicApi.events);
24622 grid.api.registerMethodsFromObject(publicApi.methods);
24626 defaultGridOptions: function (gridOptions) {
24627 //default option to true unless it was explicitly set to false
24630 * @name ui.grid.selection.api:GridOptions
24632 * @description GridOptions for selection feature, these are available to be
24633 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
24638 * @name enableRowSelection
24639 * @propertyOf ui.grid.selection.api:GridOptions
24640 * @description Enable row selection for entire grid.
24641 * <br/>Defaults to true
24643 gridOptions.enableRowSelection = gridOptions.enableRowSelection !== false;
24646 * @name multiSelect
24647 * @propertyOf ui.grid.selection.api:GridOptions
24648 * @description Enable multiple row selection for entire grid
24649 * <br/>Defaults to true
24651 gridOptions.multiSelect = gridOptions.multiSelect !== false;
24655 * @propertyOf ui.grid.selection.api:GridOptions
24656 * @description Prevent a row from being unselected. Works in conjunction
24657 * with `multiselect = false` and `gridApi.selection.selectRow()` to allow
24658 * you to create a single selection only grid - a row is always selected, you
24659 * can only select different rows, you can't unselect the row.
24660 * <br/>Defaults to false
24662 gridOptions.noUnselect = gridOptions.noUnselect === true;
24665 * @name modifierKeysToMultiSelect
24666 * @propertyOf ui.grid.selection.api:GridOptions
24667 * @description Enable multiple row selection only when using the ctrlKey or shiftKey. Requires multiSelect to be true.
24668 * <br/>Defaults to false
24670 gridOptions.modifierKeysToMultiSelect = gridOptions.modifierKeysToMultiSelect === true;
24673 * @name enableRowHeaderSelection
24674 * @propertyOf ui.grid.selection.api:GridOptions
24675 * @description Enable a row header to be used for selection
24676 * <br/>Defaults to true
24678 gridOptions.enableRowHeaderSelection = gridOptions.enableRowHeaderSelection !== false;
24681 * @name enableFullRowSelection
24682 * @propertyOf ui.grid.selection.api:GridOptions
24683 * @description Enable selection by clicking anywhere on the row. Defaults to
24684 * false if `enableRowHeaderSelection` is true, otherwise defaults to false.
24686 if ( typeof(gridOptions.enableFullRowSelection) === 'undefined' ){
24687 gridOptions.enableFullRowSelection = !gridOptions.enableRowHeaderSelection;
24691 * @name enableSelectAll
24692 * @propertyOf ui.grid.selection.api:GridOptions
24693 * @description Enable the select all checkbox at the top of the selectionRowHeader
24694 * <br/>Defaults to true
24696 gridOptions.enableSelectAll = gridOptions.enableSelectAll !== false;
24699 * @name enableSelectionBatchEvent
24700 * @propertyOf ui.grid.selection.api:GridOptions
24701 * @description If selected rows are changed in bulk, either via the API or
24702 * via the selectAll checkbox, then a separate event is fired. Setting this
24703 * option to false will cause the rowSelectionChanged event to be called multiple times
24705 * <br/>Defaults to true
24707 gridOptions.enableSelectionBatchEvent = gridOptions.enableSelectionBatchEvent !== false;
24710 * @name selectionRowHeaderWidth
24711 * @propertyOf ui.grid.selection.api:GridOptions
24712 * @description can be used to set a custom width for the row header selection column
24713 * <br/>Defaults to 30px
24715 gridOptions.selectionRowHeaderWidth = angular.isDefined(gridOptions.selectionRowHeaderWidth) ? gridOptions.selectionRowHeaderWidth : 30;
24719 * @name enableFooterTotalSelected
24720 * @propertyOf ui.grid.selection.api:GridOptions
24721 * @description Shows the total number of selected items in footer if true.
24722 * <br/>Defaults to true.
24723 * <br/>GridOptions.showGridFooter must also be set to true.
24725 gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false;
24729 * @name isRowSelectable
24730 * @propertyOf ui.grid.selection.api:GridOptions
24731 * @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property.
24734 gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop;
24739 * @name toggleRowSelection
24740 * @methodOf ui.grid.selection.service:uiGridSelectionService
24741 * @description Toggles row as selected or unselected
24742 * @param {Grid} grid grid object
24743 * @param {GridRow} row row to select or deselect
24744 * @param {Event} event object if resulting from event
24745 * @param {bool} multiSelect if false, only one row at time can be selected
24746 * @param {bool} noUnselect if true then rows cannot be unselected
24748 toggleRowSelection: function (grid, row, evt, multiSelect, noUnselect) {
24749 var selected = row.isSelected;
24751 if ( row.enableSelection === false && !selected ){
24756 if (!multiSelect && !selected) {
24757 service.clearSelectedRows(grid, evt);
24758 } else if (!multiSelect && selected) {
24759 selectedRows = service.getSelectedRows(grid);
24760 if (selectedRows.length > 1) {
24761 selected = false; // Enable reselect of the row
24762 service.clearSelectedRows(grid, evt);
24766 if (selected && noUnselect){
24767 // don't deselect the row
24769 row.setSelected(!selected);
24770 if (row.isSelected === true) {
24771 grid.selection.lastSelectedRow = row;
24774 selectedRows = service.getSelectedRows(grid);
24775 grid.selection.selectAll = grid.rows.length === selectedRows.length;
24777 grid.api.selection.raise.rowSelectionChanged(row, evt);
24782 * @name shiftSelect
24783 * @methodOf ui.grid.selection.service:uiGridSelectionService
24784 * @description selects a group of rows from the last selected row using the shift key
24785 * @param {Grid} grid grid object
24786 * @param {GridRow} clicked row
24787 * @param {Event} event object if raised from an event
24788 * @param {bool} multiSelect if false, does nothing this is for multiSelect only
24790 shiftSelect: function (grid, row, evt, multiSelect) {
24791 if (!multiSelect) {
24794 var selectedRows = service.getSelectedRows(grid);
24795 var fromRow = selectedRows.length > 0 ? grid.renderContainers.body.visibleRowCache.indexOf(grid.selection.lastSelectedRow) : 0;
24796 var toRow = grid.renderContainers.body.visibleRowCache.indexOf(row);
24797 //reverse select direction
24798 if (fromRow > toRow) {
24804 var changedRows = [];
24805 for (var i = fromRow; i <= toRow; i++) {
24806 var rowToSelect = grid.renderContainers.body.visibleRowCache[i];
24808 if ( !rowToSelect.isSelected && rowToSelect.enableSelection !== false ){
24809 rowToSelect.setSelected(true);
24810 grid.selection.lastSelectedRow = rowToSelect;
24811 service.decideRaiseSelectionEvent( grid, rowToSelect, changedRows, evt );
24815 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24819 * @name getSelectedRows
24820 * @methodOf ui.grid.selection.service:uiGridSelectionService
24821 * @description Returns all the selected rows
24822 * @param {Grid} grid grid object
24824 getSelectedRows: function (grid) {
24825 return grid.rows.filter(function (row) {
24826 return row.isSelected;
24832 * @name clearSelectedRows
24833 * @methodOf ui.grid.selection.service:uiGridSelectionService
24834 * @description Clears all selected rows
24835 * @param {Grid} grid grid object
24836 * @param {Event} event object if raised from an event
24838 clearSelectedRows: function (grid, evt) {
24839 var changedRows = [];
24840 service.getSelectedRows(grid).forEach(function (row) {
24841 if ( row.isSelected ){
24842 row.setSelected(false);
24843 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24846 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24847 grid.selection.selectAll = false;
24848 grid.selection.selectedCount = 0;
24853 * @name decideRaiseSelectionEvent
24854 * @methodOf ui.grid.selection.service:uiGridSelectionService
24855 * @description Decides whether to raise a single event or a batch event
24856 * @param {Grid} grid grid object
24857 * @param {GridRow} row row that has changed
24858 * @param {array} changedRows an array to which we can append the changed
24859 * @param {Event} event object if raised from an event
24860 * row if we're doing batch events
24862 decideRaiseSelectionEvent: function( grid, row, changedRows, evt ){
24863 if ( !grid.options.enableSelectionBatchEvent ){
24864 grid.api.selection.raise.rowSelectionChanged(row, evt);
24866 changedRows.push(row);
24872 * @name raiseSelectionEvent
24873 * @methodOf ui.grid.selection.service:uiGridSelectionService
24874 * @description Decides whether we need to raise a batch event, and
24875 * raises it if we do.
24876 * @param {Grid} grid grid object
24877 * @param {array} changedRows an array of changed rows, only populated
24878 * @param {Event} event object if raised from an event
24879 * if we're doing batch events
24881 decideRaiseSelectionBatchEvent: function( grid, changedRows, evt ){
24882 if ( changedRows.length > 0 ){
24883 grid.api.selection.raise.rowSelectionChangedBatch(changedRows, evt);
24894 * @name ui.grid.selection.directive:uiGridSelection
24898 * @description Adds selection features to grid
24901 <example module="app">
24902 <file name="app.js">
24903 var app = angular.module('app', ['ui.grid', 'ui.grid.selection']);
24905 app.controller('MainCtrl', ['$scope', function ($scope) {
24907 { name: 'Bob', title: 'CEO' },
24908 { name: 'Frank', title: 'Lowly Developer' }
24911 $scope.columnDefs = [
24912 {name: 'name', enableCellEdit: true},
24913 {name: 'title', enableCellEdit: true}
24917 <file name="index.html">
24918 <div ng-controller="MainCtrl">
24919 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-selection></div>
24924 module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', 'uiGridConstants',
24925 function (uiGridSelectionConstants, uiGridSelectionService, $templateCache, uiGridConstants) {
24929 require: '^uiGrid',
24931 compile: function () {
24933 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
24934 uiGridSelectionService.initializeGrid(uiGridCtrl.grid);
24935 if (uiGridCtrl.grid.options.enableRowHeaderSelection) {
24936 var selectionRowHeaderDef = {
24937 name: uiGridSelectionConstants.selectionRowHeaderColName,
24939 width: uiGridCtrl.grid.options.selectionRowHeaderWidth,
24941 cellTemplate: 'ui-grid/selectionRowHeader',
24942 headerCellTemplate: 'ui-grid/selectionHeaderCell',
24943 enableColumnResizing: false,
24944 enableColumnMenu: false,
24945 exporterSuppressExport: true,
24946 allowCellFocus: true
24949 uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef);
24952 var processorSet = false;
24954 var processSelectableRows = function( rows ){
24955 rows.forEach(function(row){
24956 row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row);
24961 var updateOptions = function(){
24962 if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) {
24963 uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500);
24964 processorSet = true;
24970 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback( updateOptions, [uiGridConstants.dataChange.OPTIONS] );
24972 $scope.$on( '$destroy', dataChangeDereg);
24974 post: function ($scope, $elm, $attrs, uiGridCtrl) {
24982 module.directive('uiGridSelectionRowHeaderButtons', ['$templateCache', 'uiGridSelectionService', 'gridUtil',
24983 function ($templateCache, uiGridSelectionService, gridUtil) {
24987 template: $templateCache.get('ui-grid/selectionRowHeaderButtons'),
24989 require: '^uiGrid',
24990 link: function($scope, $elm, $attrs, uiGridCtrl) {
24991 var self = uiGridCtrl.grid;
24992 $scope.selectButtonClick = selectButtonClick;
24994 // On IE, prevent mousedowns on the select button from starting a selection.
24995 // If this is not done and you shift+click on another row, the browser will select a big chunk of text
24996 if (gridUtil.detectBrowser() === 'ie') {
24997 $elm.on('mousedown', selectButtonMouseDown);
25001 function selectButtonClick(row, evt) {
25002 evt.stopPropagation();
25004 if (evt.shiftKey) {
25005 uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
25007 else if (evt.ctrlKey || evt.metaKey) {
25008 uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect);
25011 uiGridSelectionService.toggleRowSelection(self, row, evt, (self.options.multiSelect && !self.options.modifierKeysToMultiSelect), self.options.noUnselect);
25015 function selectButtonMouseDown(evt) {
25016 if (evt.ctrlKey || evt.shiftKey) {
25017 evt.target.onselectstart = function () { return false; };
25018 window.setTimeout(function () { evt.target.onselectstart = null; }, 0);
25025 module.directive('uiGridSelectionSelectAllButtons', ['$templateCache', 'uiGridSelectionService',
25026 function ($templateCache, uiGridSelectionService) {
25030 template: $templateCache.get('ui-grid/selectionSelectAllButtons'),
25032 link: function($scope, $elm, $attrs, uiGridCtrl) {
25033 var self = $scope.col.grid;
25035 $scope.headerButtonClick = function(row, evt) {
25036 if ( self.selection.selectAll ){
25037 uiGridSelectionService.clearSelectedRows(self, evt);
25038 if ( self.options.noUnselect ){
25039 self.api.selection.selectRowByVisibleIndex(0, evt);
25041 self.selection.selectAll = false;
25043 if ( self.options.multiSelect ){
25044 self.api.selection.selectAllVisibleRows(evt);
25045 self.selection.selectAll = true;
25055 * @name ui.grid.selection.directive:uiGridViewport
25058 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
25061 module.directive('uiGridViewport',
25062 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService',
25063 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService) {
25065 priority: -200, // run after default directive
25067 compile: function ($elm, $attrs) {
25068 var rowRepeatDiv = angular.element($elm.children().children()[0]);
25070 var existingNgClass = rowRepeatDiv.attr("ng-class");
25071 var newNgClass = '';
25072 if ( existingNgClass ) {
25073 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-row-selected': row.isSelected}";
25075 newNgClass = "{'ui-grid-row-selected': row.isSelected}";
25077 rowRepeatDiv.attr("ng-class", newNgClass);
25080 pre: function ($scope, $elm, $attrs, controllers) {
25083 post: function ($scope, $elm, $attrs, controllers) {
25092 * @name ui.grid.selection.directive:uiGridCell
25096 * @description Stacks on top of ui.grid.uiGridCell to provide selection feature
25098 module.directive('uiGridCell',
25099 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', '$timeout',
25100 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService, $timeout) {
25102 priority: -200, // run after default uiGridCell directive
25104 require: '?^uiGrid',
25106 link: function ($scope, $elm, $attrs, uiGridCtrl) {
25108 var touchStartTime = 0;
25109 var touchTimeout = 300;
25111 // Bind to keydown events in the render container
25112 if (uiGridCtrl.grid.api.cellNav) {
25114 uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
25115 if (rowCol === null ||
25116 rowCol.row !== $scope.row ||
25117 rowCol.col !== $scope.col) {
25121 if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
25122 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
25126 // uiGridCellNavService.scrollToIfNecessary(uiGridCtrl.grid, rowCol.row, rowCol.col);
25130 //$elm.bind('keydown', function (evt) {
25131 // if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
25132 // uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
25133 // $scope.$apply();
25137 var selectCells = function(evt){
25138 // if we get a click, then stop listening for touchend
25139 $elm.off('touchend', touchEnd);
25141 if (evt.shiftKey) {
25142 uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect);
25144 else if (evt.ctrlKey || evt.metaKey) {
25145 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect, $scope.grid.options.noUnselect);
25148 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
25152 // don't re-enable the touchend handler for a little while - some devices generate both, and it will
25153 // take a little while to move your hand from the mouse to the screen if you have both modes of input
25154 $timeout(function() {
25155 $elm.on('touchend', touchEnd);
25159 var touchStart = function(evt){
25160 touchStartTime = (new Date()).getTime();
25162 // if we get a touch event, then stop listening for click
25163 $elm.off('click', selectCells);
25166 var touchEnd = function(evt) {
25167 var touchEndTime = (new Date()).getTime();
25168 var touchTime = touchEndTime - touchStartTime;
25170 if (touchTime < touchTimeout ) {
25175 // don't re-enable the click handler for a little while - some devices generate both, and it will
25176 // take a little while to move your hand from the screen to the mouse if you have both modes of input
25177 $timeout(function() {
25178 $elm.on('click', selectCells);
25182 function registerRowSelectionEvents() {
25183 if ($scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection) {
25184 $elm.addClass('ui-grid-disable-selection');
25185 $elm.on('touchstart', touchStart);
25186 $elm.on('touchend', touchEnd);
25187 $elm.on('click', selectCells);
25189 $scope.registered = true;
25193 function deregisterRowSelectionEvents() {
25194 if ($scope.registered){
25195 $elm.removeClass('ui-grid-disable-selection');
25197 $elm.off('touchstart', touchStart);
25198 $elm.off('touchend', touchEnd);
25199 $elm.off('click', selectCells);
25201 $scope.registered = false;
25205 registerRowSelectionEvents();
25206 // register a dataChange callback so that we can change the selection configuration dynamically
25207 // if the user changes the options
25208 var dataChangeDereg = $scope.grid.registerDataChangeCallback( function() {
25209 if ( $scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection &&
25210 !$scope.registered ){
25211 registerRowSelectionEvents();
25212 } else if ( ( !$scope.grid.options.enableRowSelection || !$scope.grid.options.enableFullRowSelection ) &&
25213 $scope.registered ){
25214 deregisterRowSelectionEvents();
25216 }, [uiGridConstants.dataChange.OPTIONS] );
25218 $elm.on( '$destroy', dataChangeDereg);
25223 module.directive('uiGridGridFooter', ['$compile', 'uiGridConstants', 'gridUtil', function ($compile, uiGridConstants, gridUtil) {
25228 require: '^uiGrid',
25230 compile: function ($elm, $attrs) {
25232 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
25234 if (!uiGridCtrl.grid.options.showGridFooter) {
25239 gridUtil.getTemplate('ui-grid/gridFooterSelectedItems')
25240 .then(function (contents) {
25241 var template = angular.element(contents);
25243 var newElm = $compile(template)($scope);
25245 angular.element($elm[0].getElementsByClassName('ui-grid-grid-footer')[0]).append(newElm);
25249 post: function ($scope, $elm, $attrs, controllers) {
25264 * @name ui.grid.treeBase
25267 * # ui.grid.treeBase
25269 * <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>
25271 * This module provides base tree handling functions that are shared by other features, notably grouping
25272 * and treeView. It provides a tree view of the data, with nodes in that
25275 * Design information:
25276 * -------------------
25278 * The raw data that is provided must come with a $$treeLevel on any non-leaf node. Grouping will create
25279 * these on all the group header rows, treeView will expect these to be set in the raw data by the user.
25280 * TreeBase will run a rowsProcessor that:
25281 * - builds `treeBase.tree` out of the provided rows
25282 * - permits a recursive sort of the tree
25283 * - maintains the expand/collapse state of each node
25284 * - provides the expand/collapse all button and the expand/collapse buttons
25285 * - maintains the count of children for each node
25287 * Each row is updated with a link to the tree node that represents it. Refer {@link ui.grid.treeBase.grid:treeBase.tree tree documentation}
25290 * TreeBase adds information to the rows
25291 * - treeLevel: if present and > -1 tells us the level (level 0 is the top level)
25292 * - treeNode: pointer to the node in the grid.treeBase.tree that refers
25293 * to this row, allowing us to manipulate the state
25295 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
25296 * row order or filtering or anything like that is changed. We recall the expanded state
25297 * across invocations of the rowsProcessors by the reference to the treeNode on the individual
25298 * rows. We rebuild the tree itself quite frequently, when we do this we use the saved treeNodes to
25299 * get the state, but we overwrite the other data in that treeNode.
25301 * By default rows are collapsed, which means all data rows have their visible property
25302 * set to false, and only level 0 group rows are set to visible.
25304 * We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into
25305 * grid.treeBase.tree, then call refresh. This is because we can't easily change the visible
25306 * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as
25307 * well use it all the time.
25309 * Tree base provides sorting (on non-grouped columns).
25311 * Sorting works in two passes. The standard sorting is performed for any columns that are important to building
25312 * the tree (for example, any grouped columns). Then after the tree is built, a recursive tree sort is performed
25313 * for the remaining sort columns (including the original sort) - these columns are sorted within each tree level
25314 * (so all the level 1 nodes are sorted, then all the level 2 nodes within each level 1 node etc).
25316 * To achieve this we make use of the `ignoreSort` property on the sort configuration. The parent feature (treeView or grouping)
25317 * must provide a rowsProcessor that runs with very low priority (typically in the 60-65 range), and that sets
25318 * the `ignoreSort`on any sort that it wants to run on the tree. TreeBase will clear the ignoreSort on all sorts - so it
25319 * will turn on any sorts that haven't run. It will then call a recursive sort on the tree.
25321 * Tree base provides treeAggregation. It checks the treeAggregation configuration on each column, and aggregates based on
25322 * the logic provided as it builds the tree. Footer aggregation from the uiGrid core should not be used with treeBase aggregation,
25323 * since it operates on all visible rows, as opposed to to leaf nodes only. Setting `showColumnFooter: true` will show the
25324 * treeAggregations in the column footer. Aggregation information will be collected in the format:
25330 * label: 'count: ',
25331 * rendered: 'count: 4'
25335 * A callback is provided to format the value once it is finalised (aka a valueFilter).
25340 * <div doc-module-components="ui.grid.treeBase"></div>
25343 var module = angular.module('ui.grid.treeBase', ['ui.grid']);
25347 * @name ui.grid.treeBase.constant:uiGridTreeBaseConstants
25349 * @description constants available in treeBase module.
25351 * These constants are manually copied into grouping and treeView,
25352 * as I haven't found a way to simply include them, and it's not worth
25353 * investing time in for something that changes very infrequently.
25356 module.constant('uiGridTreeBaseConstants', {
25357 featureName: "treeBase",
25358 rowHeaderColName: 'treeBaseRowHeaderCol',
25359 EXPANDED: 'expanded',
25360 COLLAPSED: 'collapsed',
25372 * @name ui.grid.treeBase.service:uiGridTreeBaseService
25374 * @description Services for treeBase feature
25378 * @name ui.grid.treeBase.api:ColumnDef
25380 * @description ColumnDef for tree feature, these are available to be
25381 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
25384 module.service('uiGridTreeBaseService', ['$q', 'uiGridTreeBaseConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'rowSorter',
25385 function ($q, uiGridTreeBaseConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants, rowSorter) {
25389 initializeGrid: function (grid, $scope) {
25391 //add feature namespace and any properties to grid for needed
25394 * @name ui.grid.treeBase.grid:treeBase
25396 * @description Grid properties and functions added for treeBase
25398 grid.treeBase = {};
25402 * @propertyOf ui.grid.treeBase.grid:treeBase
25403 * @name numberLevels
25405 * @description Total number of tree levels currently used, calculated by the rowsProcessor by
25406 * retaining the highest tree level it sees
25408 grid.treeBase.numberLevels = 0;
25412 * @propertyOf ui.grid.treeBase.grid:treeBase
25415 * @description Whether or not the expandAll box is selected
25417 grid.treeBase.expandAll = false;
25421 * @propertyOf ui.grid.treeBase.grid:treeBase
25424 * @description Tree represented as a nested array that holds the state of each node, along with a
25425 * pointer to the row. The array order is material - we will display the children in the order
25426 * they are stored in the array
25428 * Each node stores:
25430 * - the state of this node
25431 * - an array of children of this node
25432 * - a pointer to the parent of this node (reverse pointer, allowing us to walk up the tree)
25433 * - the number of children of this node
25434 * - aggregation information calculated from the nodes
25438 * state: 'expanded',
25439 * row: <reference to row>,
25445 * label: 'count: ',
25446 * rendered: 'count: 2'
25450 * state: 'expanded',
25451 * row: <reference to row>,
25452 * parentRow: <reference to row>,
25457 * label: 'count: ',
25458 * rendered: 'count: 4'
25461 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
25462 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
25463 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
25464 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> }
25468 * state: 'collapsed',
25469 * row: <reference to row>,
25470 * parentRow: <reference to row>,
25475 * label: 'count: ',
25476 * rendered: 'count: 3'
25479 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
25480 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
25481 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> }
25485 * }, {<another level 0 node maybe>} ]
25487 * Missing state values are false - meaning they aren't expanded.
25489 * This is used because the rowProcessors run every time the grid is refreshed, so
25490 * we'd lose the expanded state every time the grid was refreshed. This instead gives
25491 * us a reliable lookup that persists across rowProcessors.
25493 * This tree is rebuilt every time we run the rowsProcessors. Since each row holds a pointer
25494 * to it's tree node we can persist expand/collapse state across calls to rowsProcessor, we discard
25495 * all transient information on the tree (children, childCount) and recalculate it
25498 grid.treeBase.tree = [];
25500 service.defaultGridOptions(grid.options);
25502 grid.registerRowsProcessor(service.treeRows, 410);
25504 grid.registerColumnBuilder( service.treeBaseColumnBuilder );
25506 service.createRowHeader( grid );
25510 * @name ui.grid.treeBase.api:PublicApi
25512 * @description Public Api for treeBase feature
25519 * @eventOf ui.grid.treeBase.api:PublicApi
25520 * @name rowExpanded
25521 * @description raised whenever a row is expanded. If you are dynamically
25522 * rendering your tree you can listen to this event, and then retrieve
25523 * the children of this row and load them into the grid data.
25525 * When the data is loaded the grid will automatically refresh to show these new rows
25528 * gridApi.treeBase.on.rowExpanded(scope,function(row){})
25530 * @param {gridRow} row the row that was expanded. You can also
25531 * retrieve the grid from this row with row.grid
25537 * @eventOf ui.grid.treeBase.api:PublicApi
25538 * @name rowCollapsed
25539 * @description raised whenever a row is collapsed. Doesn't really have
25540 * a purpose at the moment, included for symmetry
25543 * gridApi.treeBase.on.rowCollapsed(scope,function(row){})
25545 * @param {gridRow} row the row that was collapsed. You can also
25546 * retrieve the grid from this row with row.grid
25556 * @name expandAllRows
25557 * @methodOf ui.grid.treeBase.api:PublicApi
25558 * @description Expands all tree rows
25560 expandAllRows: function () {
25561 service.expandAllRows(grid);
25566 * @name collapseAllRows
25567 * @methodOf ui.grid.treeBase.api:PublicApi
25568 * @description collapse all tree rows
25570 collapseAllRows: function () {
25571 service.collapseAllRows(grid);
25576 * @name toggleRowTreeState
25577 * @methodOf ui.grid.treeBase.api:PublicApi
25578 * @description call expand if the row is collapsed, collapse if it is expanded
25579 * @param {gridRow} row the row you wish to toggle
25581 toggleRowTreeState: function (row) {
25582 service.toggleRowTreeState(grid, row);
25588 * @methodOf ui.grid.treeBase.api:PublicApi
25589 * @description expand the immediate children of the specified row
25590 * @param {gridRow} row the row you wish to expand
25592 expandRow: function (row) {
25593 service.expandRow(grid, row);
25598 * @name expandRowChildren
25599 * @methodOf ui.grid.treeBase.api:PublicApi
25600 * @description expand all children of the specified row
25601 * @param {gridRow} row the row you wish to expand
25603 expandRowChildren: function (row) {
25604 service.expandRowChildren(grid, row);
25609 * @name collapseRow
25610 * @methodOf ui.grid.treeBase.api:PublicApi
25611 * @description collapse the specified row. When
25612 * you expand the row again, all grandchildren will retain their state
25613 * @param {gridRow} row the row you wish to collapse
25615 collapseRow: function ( row ) {
25616 service.collapseRow(grid, row);
25621 * @name collapseRowChildren
25622 * @methodOf ui.grid.treeBase.api:PublicApi
25623 * @description collapse all children of the specified row. When
25624 * you expand the row again, all grandchildren will be collapsed
25625 * @param {gridRow} row the row you wish to collapse children for
25627 collapseRowChildren: function ( row ) {
25628 service.collapseRowChildren(grid, row);
25633 * @name getTreeState
25634 * @methodOf ui.grid.treeBase.api:PublicApi
25635 * @description Get the tree state for this grid,
25636 * used by the saveState feature
25637 * Returned treeState as an object
25638 * `{ expandedState: { uid: 'expanded', uid: 'collapsed' } }`
25639 * where expandedState is a hash of row uid and the current expanded state
25641 * @returns {object} tree state
25643 * TODO - this needs work - we need an identifier that persists across instantiations,
25644 * not uid. This really means we need a row identity defined, but that won't work for
25645 * grouping. Perhaps this needs to be moved up to treeView and grouping, rather than
25648 getTreeExpandedState: function () {
25649 return { expandedState: service.getTreeState(grid) };
25654 * @name setTreeState
25655 * @methodOf ui.grid.treeBase.api:PublicApi
25656 * @description Set the expanded states of the tree
25657 * @param {object} config the config you want to apply, in the format
25658 * provided by getTreeState
25660 setTreeState: function ( config ) {
25661 service.setTreeState( grid, config );
25666 * @name getRowChildren
25667 * @methodOf ui.grid.treeBase.api:PublicApi
25668 * @description Get the children of the specified row
25669 * @param {GridRow} row the row you want the children of
25670 * @returns {Array} array of children of this row, the children
25673 getRowChildren: function ( row ){
25674 return row.treeNode.children.map( function( childNode ){
25675 return childNode.row;
25682 grid.api.registerEventsFromObject(publicApi.events);
25684 grid.api.registerMethodsFromObject(publicApi.methods);
25688 defaultGridOptions: function (gridOptions) {
25689 //default option to true unless it was explicitly set to false
25692 * @name ui.grid.treeBase.api:GridOptions
25694 * @description GridOptions for treeBase feature, these are available to be
25695 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
25700 * @name treeRowHeaderBaseWidth
25701 * @propertyOf ui.grid.treeBase.api:GridOptions
25702 * @description Base width of the tree header, provides for a single level of tree. This
25703 * is incremented by `treeIndent` for each extra level
25704 * <br/>Defaults to 30
25706 gridOptions.treeRowHeaderBaseWidth = gridOptions.treeRowHeaderBaseWidth || 30;
25711 * @propertyOf ui.grid.treeBase.api:GridOptions
25712 * @description Number of pixels of indent for the icon at each tree level, wider indents are visually more pleasing,
25713 * but will make the tree row header wider
25714 * <br/>Defaults to 10
25716 gridOptions.treeIndent = gridOptions.treeIndent || 10;
25720 * @name showTreeRowHeader
25721 * @propertyOf ui.grid.treeBase.api:GridOptions
25722 * @description If set to false, don't create the row header. Youll need to programatically control the expand
25724 * <br/>Defaults to true
25726 gridOptions.showTreeRowHeader = gridOptions.showTreeRowHeader !== false;
25730 * @name showTreeExpandNoChildren
25731 * @propertyOf ui.grid.treeBase.api:GridOptions
25732 * @description If set to true, show the expand/collapse button even if there are no
25733 * children of a node. You'd use this if you're planning to dynamically load the children
25735 * <br/>Defaults to true, grouping overrides to false
25737 gridOptions.showTreeExpandNoChildren = gridOptions.showTreeExpandNoChildren !== false;
25741 * @name treeRowHeaderAlwaysVisible
25742 * @propertyOf ui.grid.treeBase.api:GridOptions
25743 * @description If set to true, row header even if there are no tree nodes
25745 * <br/>Defaults to true
25747 gridOptions.treeRowHeaderAlwaysVisible = gridOptions.treeRowHeaderAlwaysVisible !== false;
25751 * @name treeCustomAggregations
25752 * @propertyOf ui.grid.treeBase.api:GridOptions
25753 * @description Define custom aggregation functions. The properties of this object will be
25754 * aggregation types available for use on columnDef with {@link ui.grid.treeBase.api:ColumnDef treeAggregationType} or through the column menu.
25755 * If a function defined here uses the same name as one of the native aggregations, this one will take precedence.
25756 * The object format is:
25760 * aggregationName: {
25761 * label: (optional) string,
25762 * aggregationFn: function( aggregation, fieldValue, numValue, row ){...},
25763 * finalizerFn: (optional) function( aggregation ){...}
25767 * aggregationFn: function( aggregation, fieldValue, numValue ){
25768 * aggregation.count = (aggregation.count || 1) + 1;
25769 * aggregation.sum = (aggregation.sum || 0) + numValue;
25771 * finalizerFn: function( aggregation ){
25772 * aggregation.value = aggregation.sum / aggregation.count
25778 * <br/>The `finalizerFn` may be used to manipulate the value before rendering, or to
25779 * apply a custom rendered value. If `aggregation.rendered` is left undefined, the value will be
25780 * rendered. Note that the native aggregation functions use an `finalizerFn` to concatenate
25781 * the label and the value.
25783 * <br/>Defaults to {}
25785 gridOptions.treeCustomAggregations = gridOptions.treeCustomAggregations || {};
25791 * @name treeBaseColumnBuilder
25792 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25793 * @description Sets the tree defaults based on the columnDefs
25795 * @param {object} colDef columnDef we're basing on
25796 * @param {GridCol} col the column we're to update
25797 * @param {object} gridOptions the options we should use
25798 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
25800 treeBaseColumnBuilder: function (colDef, col, gridOptions) {
25805 * @name customTreeAggregationFn
25806 * @propertyOf ui.grid.treeBase.api:ColumnDef
25807 * @description A custom function that aggregates rows into some form of
25808 * total. Aggregations run row-by-row, the function needs to be capable of
25809 * creating a running total.
25811 * The function will be provided the aggregation item (in which you can store running
25812 * totals), the row value that is to be aggregated, and that same row value converted to
25813 * a number (most aggregations work on numbers)
25816 * customTreeAggregationFn = function ( aggregation, fieldValue, numValue, row ){
25817 * // calculates the average of the squares of the values
25818 * if ( typeof(aggregation.count) === 'undefined' ){
25819 * aggregation.count = 0;
25821 * aggregation.count++;
25823 * if ( !isNaN(numValue) ){
25824 * if ( typeof(aggregation.total) === 'undefined' ){
25825 * aggregation.total = 0;
25827 * aggregation.total = aggregation.total + numValue * numValue;
25830 * aggregation.value = aggregation.total / aggregation.count;
25833 * <br/>Defaults to undefined. May be overwritten by treeAggregationType, the two options should not be used together.
25835 if ( typeof(colDef.customTreeAggregationFn) !== 'undefined' ){
25836 col.treeAggregationFn = colDef.customTreeAggregationFn;
25841 * @name treeAggregationType
25842 * @propertyOf ui.grid.treeBase.api:ColumnDef
25843 * @description Use one of the native or grid-level aggregation methods for calculating aggregations on this column.
25844 * Native method are in the constants file and include: SUM, COUNT, MIN, MAX, AVG. This may also be the property the
25845 * name of an aggregation function defined with {@link ui.grid.treeBase.api:GridOptions treeCustomAggregations}.
25848 * treeAggregationType = uiGridTreeBaseConstants.aggregation.SUM,
25852 * If you are using aggregations you should either:
25854 * - also use grouping, in which case the aggregations are displayed in the group header, OR
25855 * - use treeView, in which case you can set `treeAggregationUpdateEntity: true` in the colDef, and
25856 * treeBase will store the aggregation information in the entity, or you can set `treeAggregationUpdateEntity: false`
25857 * in the colDef, and you need to manual retrieve the calculated aggregations from the row.treeNode.aggregations
25859 * <br/>Takes precendence over a treeAggregationFn, the two options should not be used together.
25860 * <br/>Defaults to undefined.
25862 if ( typeof(colDef.treeAggregationType) !== 'undefined' ){
25863 col.treeAggregation = { type: colDef.treeAggregationType };
25864 if ( typeof(gridOptions.treeCustomAggregations[colDef.treeAggregationType]) !== 'undefined' ){
25865 col.treeAggregationFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].aggregationFn;
25866 col.treeAggregationFinalizerFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].finalizerFn;
25867 col.treeAggregation.label = gridOptions.treeCustomAggregations[colDef.treeAggregationType].label;
25868 } else if ( typeof(service.nativeAggregations()[colDef.treeAggregationType]) !== 'undefined' ){
25869 col.treeAggregationFn = service.nativeAggregations()[colDef.treeAggregationType].aggregationFn;
25870 col.treeAggregation.label = service.nativeAggregations()[colDef.treeAggregationType].label;
25876 * @name treeAggregationLabel
25877 * @propertyOf ui.grid.treeBase.api:ColumnDef
25878 * @description A custom label to use for this aggregation. If provided we don't use native i18n.
25880 if ( typeof(colDef.treeAggregationLabel) !== 'undefined' ){
25881 if (typeof(col.treeAggregation) === 'undefined' ){
25882 col.treeAggregation = {};
25884 col.treeAggregation.label = colDef.treeAggregationLabel;
25889 * @name treeAggregationUpdateEntity
25890 * @propertyOf ui.grid.treeBase.api:ColumnDef
25891 * @description Store calculated aggregations into the entity, allowing them
25892 * to be displayed in the grid using a standard cellTemplate. This defaults to true,
25893 * if you are using grouping then you shouldn't set it to false, as then the aggregations won't
25896 * If you are using treeView in most cases you'll want to set this to true. This will result in
25897 * getCellValue returning the aggregation rather than whatever was stored in the cell attribute on
25898 * the entity. If you want to render the underlying entity value (and do something else with the aggregation)
25899 * then you could use a custom cellTemplate to display `row.entity.myAttribute`, rather than using getCellValue.
25901 * <br/>Defaults to true
25905 * gridOptions.columns = [{
25907 * treeAggregation: { type: uiGridTreeBaseConstants.aggregation.SUM },
25908 * treeAggregationUpdateEntity: true
25909 * cellTemplate: '<div>{{row.entity.myCol + " " + row.treeNode.aggregations[0].rendered}}</div>'
25913 col.treeAggregationUpdateEntity = colDef.treeAggregationUpdateEntity !== false;
25917 * @name customTreeAggregationFinalizerFn
25918 * @propertyOf ui.grid.treeBase.api:ColumnDef
25919 * @description A custom function that populates aggregation.rendered, this is called when
25920 * a particular aggregation has been fully calculated, and we want to render the value.
25922 * With the native aggregation options we just concatenate `aggregation.label` and
25923 * `aggregation.value`, but if you wanted to apply a filter or otherwise manipulate the label
25924 * or the value, you can do so with this function. This function will be called after the
25925 * the default `finalizerFn`.
25929 * customTreeAggregationFinalizerFn = function ( aggregation ){
25930 * aggregation.rendered = aggregation.label + aggregation.value / 100 + '%';
25933 * <br/>Defaults to undefined.
25935 if ( typeof(col.customTreeAggregationFinalizerFn) === 'undefined' ){
25936 col.customTreeAggregationFinalizerFn = colDef.customTreeAggregationFinalizerFn;
25944 * @name createRowHeader
25945 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25946 * @description Create the rowHeader. If treeRowHeaderAlwaysVisible then
25947 * set it to visible, otherwise set it to invisible
25949 * @param {Grid} grid grid object
25951 createRowHeader: function( grid ){
25952 var rowHeaderColumnDef = {
25953 name: uiGridTreeBaseConstants.rowHeaderColName,
25955 width: grid.options.treeRowHeaderBaseWidth,
25957 cellTemplate: 'ui-grid/treeBaseRowHeader',
25958 headerCellTemplate: 'ui-grid/treeBaseHeaderCell',
25959 enableColumnResizing: false,
25960 enableColumnMenu: false,
25961 exporterSuppressExport: true,
25962 allowCellFocus: true
25965 rowHeaderColumnDef.visible = grid.options.treeRowHeaderAlwaysVisible;
25966 grid.addRowHeaderColumn( rowHeaderColumnDef );
25972 * @name expandAllRows
25973 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25974 * @description Expands all nodes in the tree
25976 * @param {Grid} grid grid object
25978 expandAllRows: function (grid) {
25979 grid.treeBase.tree.forEach( function( node ) {
25980 service.setAllNodes( grid, node, uiGridTreeBaseConstants.EXPANDED);
25982 grid.treeBase.expandAll = true;
25983 grid.queueGridRefresh();
25989 * @name collapseAllRows
25990 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25991 * @description Collapses all nodes in the tree
25993 * @param {Grid} grid grid object
25995 collapseAllRows: function (grid) {
25996 grid.treeBase.tree.forEach( function( node ) {
25997 service.setAllNodes( grid, node, uiGridTreeBaseConstants.COLLAPSED);
25999 grid.treeBase.expandAll = false;
26000 grid.queueGridRefresh();
26006 * @name setAllNodes
26007 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26008 * @description Works through a subset of grid.treeBase.rowExpandedStates, setting
26009 * all child nodes (and their descendents) of the provided node to the given state.
26011 * Calls itself recursively on all nodes so as to achieve this.
26013 * @param {Grid} grid the grid we're operating on (so we can raise events)
26014 * @param {object} treeNode a node in the tree that we want to update
26015 * @param {string} targetState the state we want to set it to
26017 setAllNodes: function (grid, treeNode, targetState) {
26018 if ( typeof(treeNode.state) !== 'undefined' && treeNode.state !== targetState ){
26019 treeNode.state = targetState;
26021 if ( targetState === uiGridTreeBaseConstants.EXPANDED ){
26022 grid.api.treeBase.raise.rowExpanded(treeNode.row);
26024 grid.api.treeBase.raise.rowCollapsed(treeNode.row);
26028 // set all child nodes
26029 if ( treeNode.children ){
26030 treeNode.children.forEach(function( childNode ){
26031 service.setAllNodes(grid, childNode, targetState);
26039 * @name toggleRowTreeState
26040 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26041 * @description Toggles the expand or collapse state of this grouped row, if
26042 * it's a parent row
26044 * @param {Grid} grid grid object
26045 * @param {GridRow} row the row we want to toggle
26047 toggleRowTreeState: function ( grid, row ){
26048 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26052 if (row.treeNode.state === uiGridTreeBaseConstants.EXPANDED){
26053 service.collapseRow(grid, row);
26055 service.expandRow(grid, row);
26058 grid.queueGridRefresh();
26065 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26066 * @description Expands this specific row, showing only immediate children.
26068 * @param {Grid} grid grid object
26069 * @param {GridRow} row the row we want to expand
26071 expandRow: function ( grid, row ){
26072 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26076 if ( row.treeNode.state !== uiGridTreeBaseConstants.EXPANDED ){
26077 row.treeNode.state = uiGridTreeBaseConstants.EXPANDED;
26078 grid.api.treeBase.raise.rowExpanded(row);
26079 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
26080 grid.queueGridRefresh();
26087 * @name expandRowChildren
26088 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26089 * @description Expands this specific row, showing all children.
26091 * @param {Grid} grid grid object
26092 * @param {GridRow} row the row we want to expand
26094 expandRowChildren: function ( grid, row ){
26095 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26099 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.EXPANDED);
26100 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
26101 grid.queueGridRefresh();
26107 * @name collapseRow
26108 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26109 * @description Collapses this specific row
26111 * @param {Grid} grid grid object
26112 * @param {GridRow} row the row we want to collapse
26114 collapseRow: function( grid, row ){
26115 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26119 if ( row.treeNode.state !== uiGridTreeBaseConstants.COLLAPSED ){
26120 row.treeNode.state = uiGridTreeBaseConstants.COLLAPSED;
26121 grid.treeBase.expandAll = false;
26122 grid.api.treeBase.raise.rowCollapsed(row);
26123 grid.queueGridRefresh();
26130 * @name collapseRowChildren
26131 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26132 * @description Collapses this specific row and all children
26134 * @param {Grid} grid grid object
26135 * @param {GridRow} row the row we want to collapse
26137 collapseRowChildren: function( grid, row ){
26138 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
26142 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.COLLAPSED);
26143 grid.treeBase.expandAll = false;
26144 grid.queueGridRefresh();
26150 * @name allExpanded
26151 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26152 * @description Returns true if all rows are expanded, false
26153 * if they're not. Walks the tree to determine this. Used
26154 * to set the expandAll state.
26156 * If the node has no children, then return true (it's immaterial
26157 * whether it is expanded). If the node has children, then return
26158 * false if this node is collapsed, or if any child node is not all expanded
26160 * @param {object} tree the grid to check
26161 * @returns {boolean} whether or not the tree is all expanded
26163 allExpanded: function( tree ){
26164 var allExpanded = true;
26165 tree.forEach( function( node ){
26166 if ( !service.allExpandedInternal( node ) ){
26167 allExpanded = false;
26170 return allExpanded;
26173 allExpandedInternal: function( treeNode ){
26174 if ( treeNode.children && treeNode.children.length > 0 ){
26175 if ( treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
26178 var allExpanded = true;
26179 treeNode.children.forEach( function( node ){
26180 if ( !service.allExpandedInternal( node ) ){
26181 allExpanded = false;
26184 return allExpanded;
26194 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26195 * @description The rowProcessor that adds the nodes to the tree, and sets the visible
26196 * state of each row based on it's parent state
26198 * Assumes it is always called after the sorting processor, and the grouping processor if there is one.
26199 * Performs any tree sorts itself after having built the tree
26201 * Processes all the rows in order, setting the group level based on the $$treeLevel in the associated
26202 * entity, and setting the visible state based on the parent's state.
26204 * Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly
26207 * Aggregates if necessary along the way.
26209 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
26210 * @returns {array} the updated rows
26212 treeRows: function( renderableRows ) {
26213 if (renderableRows.length === 0){
26214 return renderableRows;
26218 var currentLevel = 0;
26219 var currentState = uiGridTreeBaseConstants.EXPANDED;
26222 grid.treeBase.tree = service.createTree( grid, renderableRows );
26223 service.updateRowHeaderWidth( grid );
26225 service.sortTree( grid );
26226 service.fixFilter( grid );
26228 return service.renderTree( grid.treeBase.tree );
26234 * @name createOrUpdateRowHeaderWidth
26235 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26236 * @description Calculates the rowHeader width.
26238 * If rowHeader is always present, updates the width.
26240 * If rowHeader is only sometimes present (`treeRowHeaderAlwaysVisible: false`), determines whether there
26241 * should be one, then creates or removes it as appropriate, with the created rowHeader having the
26244 * If there's never a rowHeader then never creates one: `showTreeRowHeader: false`
26246 * @param {Grid} grid the grid we want to set the row header on
26248 updateRowHeaderWidth: function( grid ){
26249 var rowHeader = grid.getColumn(uiGridTreeBaseConstants.rowHeaderColName);
26251 var newWidth = grid.options.treeRowHeaderBaseWidth + grid.options.treeIndent * Math.max(grid.treeBase.numberLevels - 1, 0);
26252 if ( rowHeader && newWidth !== rowHeader.width ){
26253 rowHeader.width = newWidth;
26254 grid.queueRefresh();
26257 var newVisibility = true;
26258 if ( grid.options.showTreeRowHeader === false ){
26259 newVisibility = false;
26261 if ( grid.options.treeRowHeaderAlwaysVisible === false && grid.treeBase.numberLevels <= 0 ){
26262 newVisibility = false;
26264 if ( rowHeader.visible !== newVisibility ) {
26265 rowHeader.visible = newVisibility;
26266 rowHeader.colDef.visible = newVisibility;
26267 grid.queueGridRefresh();
26275 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26276 * @description Creates an array of rows based on the tree, exporting only
26277 * the visible nodes and leaves
26279 * @param {array} nodeList the list of nodes - can be grid.treeBase.tree, or can be node.children when
26280 * we're calling recursively
26281 * @returns {array} renderable rows
26283 renderTree: function( nodeList ){
26284 var renderableRows = [];
26286 nodeList.forEach( function ( node ){
26287 if ( node.row.visible ){
26288 renderableRows.push( node.row );
26290 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
26291 renderableRows = renderableRows.concat( service.renderTree( node.children ) );
26294 return renderableRows;
26301 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26302 * @description Creates a tree from the renderableRows
26304 * @param {Grid} grid the grid
26305 * @param {array} renderableRows the rows we want to create a tree from
26306 * @returns {object} the tree we've build
26308 createTree: function( grid, renderableRows ) {
26309 var currentLevel = -1;
26312 grid.treeBase.tree = [];
26313 grid.treeBase.numberLevels = 0;
26314 var aggregations = service.getAggregations( grid );
26316 var createNode = function( row ){
26317 if ( typeof(row.entity.$$treeLevel) !== 'undefined' && row.treeLevel !== row.entity.$$treeLevel ){
26318 row.treeLevel = row.entity.$$treeLevel;
26321 if ( row.treeLevel <= currentLevel ){
26322 // pop any levels that aren't parents of this level, formatting the aggregation at the same time
26323 while ( row.treeLevel <= currentLevel ){
26324 var lastParent = parents.pop();
26325 service.finaliseAggregations( lastParent );
26329 // reset our current state based on the new parent, set to expanded if this is a level 0 node
26330 if ( parents.length > 0 ){
26331 currentState = service.setCurrentState(parents);
26333 currentState = uiGridTreeBaseConstants.EXPANDED;
26337 // aggregate if this is a leaf node
26338 if ( ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) && row.visible ){
26339 service.aggregate( grid, row, parents );
26342 // add this node to the tree
26343 service.addOrUseNode(grid, row, parents, aggregations);
26345 if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel !== null && row.treeLevel >= 0 ){
26348 currentState = service.setCurrentState(parents);
26351 // update the tree number of levels, so we can set header width if we need to
26352 if ( grid.treeBase.numberLevels < row.treeLevel + 1){
26353 grid.treeBase.numberLevels = row.treeLevel + 1;
26357 renderableRows.forEach( createNode );
26359 // finalise remaining aggregations
26360 while ( parents.length > 0 ){
26361 var lastParent = parents.pop();
26362 service.finaliseAggregations( lastParent );
26365 return grid.treeBase.tree;
26371 * @name addOrUseNode
26372 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26373 * @description Creates a tree node for this row. If this row already has a treeNode
26374 * recorded against it, preserves the state, but otherwise overwrites the data.
26376 * @param {grid} grid the grid we're operating on
26377 * @param {gridRow} row the row we want to set
26378 * @param {array} parents an array of the parents this row should have
26379 * @param {array} aggregationBase empty aggregation information
26380 * @returns {undefined} updates the parents array, updates the row to have a treeNode, and updates the
26381 * grid.treeBase.tree
26383 addOrUseNode: function( grid, row, parents, aggregationBase ){
26384 var newAggregations = [];
26385 aggregationBase.forEach( function(aggregation){
26386 newAggregations.push(service.buildAggregationObject(aggregation.col));
26389 var newNode = { state: uiGridTreeBaseConstants.COLLAPSED, row: row, parentRow: null, aggregations: newAggregations, children: [] };
26390 if ( row.treeNode ){
26391 newNode.state = row.treeNode.state;
26393 if ( parents.length > 0 ){
26394 newNode.parentRow = parents[parents.length - 1];
26396 row.treeNode = newNode;
26398 if ( parents.length === 0 ){
26399 grid.treeBase.tree.push( newNode );
26401 parents[parents.length - 1].treeNode.children.push( newNode );
26408 * @name setCurrentState
26409 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26410 * @description Looks at the parents array to determine our current state.
26411 * If any node in the hierarchy is collapsed, then return collapsed, otherwise return
26414 * @param {array} parents an array of the parents this row should have
26415 * @returns {string} the state we should be setting to any nodes we see
26417 setCurrentState: function( parents ){
26418 var currentState = uiGridTreeBaseConstants.EXPANDED;
26419 parents.forEach( function(parent){
26420 if ( parent.treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
26421 currentState = uiGridTreeBaseConstants.COLLAPSED;
26424 return currentState;
26431 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26432 * @description Performs a recursive sort on the tree nodes, sorting the
26433 * children of each node and putting them back into the children array.
26435 * Before doing this it turns back on all the sortIgnore - things that were previously
26436 * ignored we process now. Since we're sorting within the nodes, presumably anything
26437 * that was already sorted is how we derived the nodes, we can keep those sorts too.
26439 * We only sort tree nodes that are expanded - no point in wasting effort sorting collapsed
26442 * @param {Grid} grid the grid to get the aggregation information from
26443 * @returns {array} the aggregation information
26445 sortTree: function( grid ){
26446 grid.columns.forEach( function( column ) {
26447 if ( column.sort && column.sort.ignoreSort ){
26448 delete column.sort.ignoreSort;
26452 grid.treeBase.tree = service.sortInternal( grid, grid.treeBase.tree );
26455 sortInternal: function( grid, treeList ){
26456 var rows = treeList.map( function( node ){
26460 rows = rowSorter.sort( grid, rows, grid.columns );
26462 var treeNodes = rows.map( function( row ){
26463 return row.treeNode;
26466 treeNodes.forEach( function( node ){
26467 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
26468 node.children = service.sortInternal( grid, node.children );
26478 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26479 * @description After filtering has run, we need to go back through the tree
26480 * and make sure the parent rows are always visible if any of the child rows
26481 * are visible (filtering may make a child visible, but the parent may not
26482 * match the filter criteria)
26484 * This has a risk of being computationally expensive, we do it by walking
26485 * the tree and remembering whether there are any invisible nodes on the
26488 * @param {Grid} grid the grid to fix filters on
26490 fixFilter: function( grid ){
26491 var parentsVisible;
26493 grid.treeBase.tree.forEach( function( node ){
26494 if ( node.children && node.children.length > 0 ){
26495 parentsVisible = node.row.visible;
26496 service.fixFilterInternal( node.children, parentsVisible );
26501 fixFilterInternal: function( nodes, parentsVisible) {
26502 nodes.forEach( function( node ){
26503 if ( node.row.visible && !parentsVisible ){
26504 service.setParentsVisible( node );
26505 parentsVisible = true;
26508 if ( node.children && node.children.length > 0 ){
26509 if ( service.fixFilterInternal( node.children, ( parentsVisible && node.row.visible ) ) ) {
26510 parentsVisible = true;
26515 return parentsVisible;
26518 setParentsVisible: function( node ){
26519 while ( node.parentRow ){
26520 node.parentRow.visible = true;
26521 node = node.parentRow.treeNode;
26527 * @name buildAggregationObject
26528 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26529 * @description Build the object which is stored on the column for holding meta-data about the aggregation.
26530 * This method should only be called with columns which have an aggregation.
26532 * @param {Column} the column which this object relates to
26533 * @returns {object} {col: Column object, label: string, type: string (optional)}
26535 buildAggregationObject: function( column ){
26536 var newAggregation = { col: column };
26538 if ( column.treeAggregation && column.treeAggregation.type ){
26539 newAggregation.type = column.treeAggregation.type;
26542 if ( column.treeAggregation && column.treeAggregation.label ){
26543 newAggregation.label = column.treeAggregation.label;
26546 return newAggregation;
26551 * @name getAggregations
26552 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26553 * @description Looks through the grid columns to find those with aggregations,
26554 * and collates the aggregation information into an array, returns that array
26556 * @param {Grid} grid the grid to get the aggregation information from
26557 * @returns {array} the aggregation information
26559 getAggregations: function( grid ){
26560 var aggregateArray = [];
26562 grid.columns.forEach( function(column){
26563 if ( typeof(column.treeAggregationFn) !== 'undefined' ){
26564 aggregateArray.push( service.buildAggregationObject(column) );
26566 if ( grid.options.showColumnFooter && typeof(column.colDef.aggregationType) === 'undefined' && column.treeAggregation ){
26567 // Add aggregation object for footer
26568 column.treeFooterAggregation = service.buildAggregationObject(column);
26569 column.aggregationType = service.treeFooterAggregationType;
26573 return aggregateArray;
26580 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26581 * @description Accumulate the data from this row onto the aggregations for each parent
26583 * Iterate over the parents, then iterate over the aggregations for each of those parents,
26584 * and perform the aggregation for each individual aggregation
26586 * @param {Grid} grid grid object
26587 * @param {GridRow} row the row we want to set grouping visibility on
26588 * @param {array} parents the parents that we would want to aggregate onto
26590 aggregate: function( grid, row, parents ){
26591 if ( parents.length === 0 && row.treeNode && row.treeNode.aggregations ){
26592 row.treeNode.aggregations.forEach(function(aggregation){
26593 // Calculate aggregations for footer even if there are no grouped rows
26594 if ( typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ) {
26595 var fieldValue = grid.getCellValue(row, aggregation.col);
26596 var numValue = Number(fieldValue);
26597 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26602 parents.forEach( function( parent, index ){
26603 if ( parent.treeNode.aggregations ){
26604 parent.treeNode.aggregations.forEach( function( aggregation ){
26605 var fieldValue = grid.getCellValue(row, aggregation.col);
26606 var numValue = Number(fieldValue);
26607 aggregation.col.treeAggregationFn(aggregation, fieldValue, numValue, row);
26609 if ( index === 0 && typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ){
26610 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26618 // Aggregation routines - no doco needed as self evident
26619 nativeAggregations: function() {
26620 var nativeAggregations = {
26622 label: i18nService.get().aggregation.count,
26623 menuTitle: i18nService.get().grouping.aggregate_count,
26624 aggregationFn: function (aggregation, fieldValue, numValue) {
26625 if (typeof(aggregation.value) === 'undefined') {
26626 aggregation.value = 1;
26628 aggregation.value++;
26634 label: i18nService.get().aggregation.sum,
26635 menuTitle: i18nService.get().grouping.aggregate_sum,
26636 aggregationFn: function( aggregation, fieldValue, numValue ) {
26637 if (!isNaN(numValue)) {
26638 if (typeof(aggregation.value) === 'undefined') {
26639 aggregation.value = numValue;
26641 aggregation.value += numValue;
26648 label: i18nService.get().aggregation.min,
26649 menuTitle: i18nService.get().grouping.aggregate_min,
26650 aggregationFn: function( aggregation, fieldValue, numValue ) {
26651 if (typeof(aggregation.value) === 'undefined') {
26652 aggregation.value = fieldValue;
26654 if (typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)) {
26655 aggregation.value = fieldValue;
26662 label: i18nService.get().aggregation.max,
26663 menuTitle: i18nService.get().grouping.aggregate_max,
26664 aggregationFn: function( aggregation, fieldValue, numValue ){
26665 if ( typeof(aggregation.value) === 'undefined' ){
26666 aggregation.value = fieldValue;
26668 if ( typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue > aggregation.value || aggregation.value === null)){
26669 aggregation.value = fieldValue;
26676 label: i18nService.get().aggregation.avg,
26677 menuTitle: i18nService.get().grouping.aggregate_avg,
26678 aggregationFn: function( aggregation, fieldValue, numValue ){
26679 if ( typeof(aggregation.count) === 'undefined' ){
26680 aggregation.count = 1;
26682 aggregation.count++;
26685 if ( isNaN(numValue) ){
26689 if ( typeof(aggregation.value) === 'undefined' || typeof(aggregation.sum) === 'undefined' ){
26690 aggregation.value = numValue;
26691 aggregation.sum = numValue;
26693 aggregation.sum += numValue;
26694 aggregation.value = aggregation.sum / aggregation.count;
26699 return nativeAggregations;
26704 * @name finaliseAggregation
26705 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26706 * @description Helper function used to finalize aggregation nodes and footer cells
26708 * @param {gridRow} row the parent we're finalising
26709 * @param {aggregation} the aggregation object manipulated by the aggregationFn
26711 finaliseAggregation: function(row, aggregation){
26712 if ( aggregation.col.treeAggregationUpdateEntity && typeof(row) !== 'undefined' && typeof(row.entity[ '$$' + aggregation.col.uid ]) !== 'undefined' ){
26713 angular.extend( aggregation, row.entity[ '$$' + aggregation.col.uid ]);
26716 if ( typeof(aggregation.col.treeAggregationFinalizerFn) === 'function' ){
26717 aggregation.col.treeAggregationFinalizerFn( aggregation );
26719 if ( typeof(aggregation.col.customTreeAggregationFinalizerFn) === 'function' ){
26720 aggregation.col.customTreeAggregationFinalizerFn( aggregation );
26722 if ( typeof(aggregation.rendered) === 'undefined' ){
26723 aggregation.rendered = aggregation.label ? aggregation.label + aggregation.value : aggregation.value;
26729 * @name finaliseAggregations
26730 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26731 * @description Format the data from the aggregation into the rendered text
26732 * e.g. if we had label: 'sum: ' and value: 25, we'd create 'sum: 25'.
26734 * As part of this we call any formatting callback routines we've been provided.
26736 * We write our aggregation out to the row.entity if treeAggregationUpdateEntity is
26737 * set on the column - we don't overwrite any information that's already there, we append
26738 * to it so that grouping can have set the groupVal beforehand without us overwriting it.
26740 * We need to copy the data from the row.entity first before we finalise the aggregation,
26741 * we need that information for the finaliserFn
26743 * @param {gridRow} row the parent we're finalising
26745 finaliseAggregations: function( row ){
26746 if ( typeof(row.treeNode.aggregations) === 'undefined' ){
26750 row.treeNode.aggregations.forEach( function( aggregation ) {
26751 service.finaliseAggregation(row, aggregation);
26753 if ( aggregation.col.treeAggregationUpdateEntity ){
26754 var aggregationCopy = {};
26755 angular.forEach( aggregation, function( value, key ){
26756 if ( aggregation.hasOwnProperty(key) && key !== 'col' ){
26757 aggregationCopy[key] = value;
26761 row.entity[ '$$' + aggregation.col.uid ] = aggregationCopy;
26768 * @name treeFooterAggregationType
26769 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26770 * @description Uses the tree aggregation functions and finalizers to set the
26771 * column footer aggregations.
26773 * @param {rows} visible rows. not used, but accepted to match signature of GridColumn.aggregationType
26774 * @param {gridColumn} the column we are finalizing
26776 treeFooterAggregationType: function( rows, column ) {
26777 service.finaliseAggregation(undefined, column.treeFooterAggregation);
26778 if ( typeof(column.treeFooterAggregation.value) === 'undefined' || column.treeFooterAggregation.rendered === null ){
26779 // The was apparently no aggregation performed (perhaps this is a grouped column
26782 return column.treeFooterAggregation.rendered;
26793 * @name ui.grid.treeBase.directive:uiGridTreeRowHeaderButtons
26796 * @description Provides the expand/collapse button on rows
26798 module.directive('uiGridTreeBaseRowHeaderButtons', ['$templateCache', 'uiGridTreeBaseService',
26799 function ($templateCache, uiGridTreeBaseService) {
26803 template: $templateCache.get('ui-grid/treeBaseRowHeaderButtons'),
26805 require: '^uiGrid',
26806 link: function($scope, $elm, $attrs, uiGridCtrl) {
26807 var self = uiGridCtrl.grid;
26808 $scope.treeButtonClick = function(row, evt) {
26809 uiGridTreeBaseService.toggleRowTreeState(self, row, evt);
26818 * @name ui.grid.treeBase.directive:uiGridTreeBaseExpandAllButtons
26821 * @description Provides the expand/collapse all button
26823 module.directive('uiGridTreeBaseExpandAllButtons', ['$templateCache', 'uiGridTreeBaseService',
26824 function ($templateCache, uiGridTreeBaseService) {
26828 template: $templateCache.get('ui-grid/treeBaseExpandAllButtons'),
26830 link: function($scope, $elm, $attrs, uiGridCtrl) {
26831 var self = $scope.col.grid;
26833 $scope.headerButtonClick = function(row, evt) {
26834 if ( self.treeBase.expandAll ){
26835 uiGridTreeBaseService.collapseAllRows(self, evt);
26837 uiGridTreeBaseService.expandAllRows(self, evt);
26847 * @name ui.grid.treeBase.directive:uiGridViewport
26850 * @description Stacks on top of ui.grid.uiGridViewport to set formatting on a tree header row
26852 module.directive('uiGridViewport',
26853 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
26854 function ($compile, uiGridConstants, gridUtil, $parse) {
26856 priority: -200, // run after default directive
26858 compile: function ($elm, $attrs) {
26859 var rowRepeatDiv = angular.element($elm.children().children()[0]);
26861 var existingNgClass = rowRepeatDiv.attr("ng-class");
26862 var newNgClass = '';
26863 if ( existingNgClass ) {
26864 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-tree-header-row': row.treeLevel > -1}";
26866 newNgClass = "{'ui-grid-tree-header-row': row.treeLevel > -1}";
26868 rowRepeatDiv.attr("ng-class", newNgClass);
26871 pre: function ($scope, $elm, $attrs, controllers) {
26874 post: function ($scope, $elm, $attrs, controllers) {
26887 * @name ui.grid.treeView
26890 * # ui.grid.treeView
26892 * <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>
26894 * This module provides a tree view of the data that it is provided, with nodes in that
26895 * tree and leaves. Unlike grouping, the tree is an inherent property of the data and must
26896 * be provided with your data array.
26898 * Design information:
26899 * -------------------
26901 * TreeView uses treeBase for the underlying functionality, and is a very thin wrapper around
26902 * that logic. Most of the design information has now moved to treebase.
26906 * <div doc-module-components="ui.grid.treeView"></div>
26909 var module = angular.module('ui.grid.treeView', ['ui.grid', 'ui.grid.treeBase']);
26913 * @name ui.grid.treeView.constant:uiGridTreeViewConstants
26915 * @description constants available in treeView module, this includes
26916 * all the constants declared in the treeBase module (these are manually copied
26917 * as there isn't an easy way to include constants in another constants file, and
26918 * we don't want to make users include treeBase)
26921 module.constant('uiGridTreeViewConstants', {
26922 featureName: "treeView",
26923 rowHeaderColName: 'treeBaseRowHeaderCol',
26924 EXPANDED: 'expanded',
26925 COLLAPSED: 'collapsed',
26937 * @name ui.grid.treeView.service:uiGridTreeViewService
26939 * @description Services for treeView features
26941 module.service('uiGridTreeViewService', ['$q', 'uiGridTreeViewConstants', 'uiGridTreeBaseConstants', 'uiGridTreeBaseService', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants',
26942 function ($q, uiGridTreeViewConstants, uiGridTreeBaseConstants, uiGridTreeBaseService, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants) {
26946 initializeGrid: function (grid, $scope) {
26947 uiGridTreeBaseService.initializeGrid( grid, $scope );
26951 * @name ui.grid.treeView.grid:treeView
26953 * @description Grid properties and functions added for treeView
26955 grid.treeView = {};
26957 grid.registerRowsProcessor(service.adjustSorting, 60);
26961 * @name ui.grid.treeView.api:PublicApi
26963 * @description Public Api for treeView feature
26976 grid.api.registerEventsFromObject(publicApi.events);
26978 grid.api.registerMethodsFromObject(publicApi.methods);
26982 defaultGridOptions: function (gridOptions) {
26983 //default option to true unless it was explicitly set to false
26986 * @name ui.grid.treeView.api:GridOptions
26988 * @description GridOptions for treeView feature, these are available to be
26989 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
26991 * Many tree options are set on treeBase, make sure to look at that feature in
26992 * conjunction with these options.
26997 * @name enableTreeView
26998 * @propertyOf ui.grid.treeView.api:GridOptions
26999 * @description Enable row tree view for entire grid.
27000 * <br/>Defaults to true
27002 gridOptions.enableTreeView = gridOptions.enableTreeView !== false;
27009 * @name adjustSorting
27010 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
27011 * @description Trees cannot be sorted the same as flat lists of rows -
27012 * trees are sorted recursively within each level - so the children of each
27013 * node are sorted, but not the full set of rows.
27015 * To achieve this, we suppress the normal sorting by setting ignoreSort on
27016 * each of the sort columns. When the treeBase rowsProcessor runs it will then
27017 * unignore these, and will perform a recursive sort against the tree that it builds.
27019 * @param {array} renderableRows the rows that we need to pass on through
27020 * @returns {array} renderableRows that we passed on through
27022 adjustSorting: function( renderableRows ) {
27025 grid.columns.forEach( function( column ){
27026 if ( column.sort ){
27027 column.sort.ignoreSort = true;
27031 return renderableRows;
27042 * @name ui.grid.treeView.directive:uiGridTreeView
27046 * @description Adds treeView features to grid
27049 <example module="app">
27050 <file name="app.js">
27051 var app = angular.module('app', ['ui.grid', 'ui.grid.treeView']);
27053 app.controller('MainCtrl', ['$scope', function ($scope) {
27055 { name: 'Bob', title: 'CEO' },
27056 { name: 'Frank', title: 'Lowly Developer' }
27059 $scope.columnDefs = [
27060 {name: 'name', enableCellEdit: true},
27061 {name: 'title', enableCellEdit: true}
27064 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
27067 <file name="index.html">
27068 <div ng-controller="MainCtrl">
27069 <div ui-grid="gridOptions" ui-grid-tree-view></div>
27074 module.directive('uiGridTreeView', ['uiGridTreeViewConstants', 'uiGridTreeViewService', '$templateCache',
27075 function (uiGridTreeViewConstants, uiGridTreeViewService, $templateCache) {
27079 require: '^uiGrid',
27081 compile: function () {
27083 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
27084 if (uiGridCtrl.grid.options.enableTreeView !== false){
27085 uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope);
27088 post: function ($scope, $elm, $attrs, uiGridCtrl) {
27102 * @name ui.grid.validate
27105 * # ui.grid.validate
27107 * <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>
27109 * This module provides the ability to validate cells upon change.
27111 * Design information:
27112 * -------------------
27114 * Validation is not based on angularjs validation, since it would work only when editing the field.
27116 * Instead it adds custom properties to any field considered as invalid.
27121 * <div doc-module-components="ui.grid.expandable"></div>
27124 var module = angular.module('ui.grid.validate', ['ui.grid']);
27129 * @name ui.grid.validate.service:uiGridValidateService
27131 * @description Services for validation features
27133 module.service('uiGridValidateService', ['$sce', '$q', '$http', 'i18nService', 'uiGridConstants', function ($sce, $q, $http, i18nService, uiGridConstants) {
27139 * @name validatorFactories
27140 * @propertyOf ui.grid.validate.service:uiGridValidateService
27141 * @description object containing all the factories used to validate data.<br/>
27142 * These factories will be in the form <br/>
27145 * validatorFactory: function(argument) {
27146 * return function(newValue, oldValue, rowEntity, colDef) {
27147 * return true || false || promise
27150 * messageFunction: function(argument) {
27156 * Promises should return true or false as result according to the result of validation.
27158 validatorFactories: {},
27163 * @name setExternalFactoryFunction
27164 * @methodOf ui.grid.validate.service:uiGridValidateService
27165 * @description Adds a way to retrieve validators from an external service
27166 * <p>Validators from this external service have a higher priority than default
27168 * @param {function} externalFactoryFunction a function that accepts name and argument to pass to a
27169 * validator factory and that returns an object with the same properties as
27170 * you can see in {@link ui.grid.validate.service:uiGridValidateService#properties_validatorFactories validatorFactories}
27172 setExternalFactoryFunction: function(externalFactoryFunction) {
27173 service.externalFactoryFunction = externalFactoryFunction;
27178 * @name clearExternalFactory
27179 * @methodOf ui.grid.validate.service:uiGridValidateService
27180 * @description Removes any link to external factory from this service
27182 clearExternalFactory: function() {
27183 delete service.externalFactoryFunction;
27188 * @name getValidatorFromExternalFactory
27189 * @methodOf ui.grid.validate.service:uiGridValidateService
27190 * @description Retrieves a validator by executing a validatorFactory
27191 * stored in an external service.
27192 * @param {string} name the name of the validator to retrieve
27193 * @param {object} argument an argument to pass to the validator factory
27195 getValidatorFromExternalFactory: function(name, argument) {
27196 return service.externalFactoryFunction(name, argument).validatorFactory(argument);
27201 * @name getMessageFromExternalFactory
27202 * @methodOf ui.grid.validate.service:uiGridValidateService
27203 * @description Retrieves a message stored in an external service.
27204 * @param {string} name the name of the validator
27205 * @param {object} argument an argument to pass to the message function
27207 getMessageFromExternalFactory: function(name, argument) {
27208 return service.externalFactoryFunction(name, argument).messageFunction(argument);
27213 * @name setValidator
27214 * @methodOf ui.grid.validate.service:uiGridValidateService
27215 * @description Adds a new validator to the service
27216 * @param {string} name the name of the validator, must be unique
27217 * @param {function} validatorFactory a factory that return a validatorFunction
27218 * @param {function} messageFunction a function that return the error message
27220 setValidator: function(name, validatorFactory, messageFunction) {
27221 service.validatorFactories[name] = {
27222 validatorFactory: validatorFactory,
27223 messageFunction: messageFunction
27229 * @name getValidator
27230 * @methodOf ui.grid.validate.service:uiGridValidateService
27231 * @description Returns a validator registered to the service
27232 * or retrieved from the external factory
27233 * @param {string} name the name of the validator to retrieve
27234 * @param {object} argument an argument to pass to the validator factory
27235 * @returns {object} the validator function
27237 getValidator: function(name, argument) {
27238 if (service.externalFactoryFunction) {
27239 var validator = service.getValidatorFromExternalFactory(name, argument);
27244 if (!service.validatorFactories[name]) {
27245 throw ("Invalid validator name: " + name);
27247 return service.validatorFactories[name].validatorFactory(argument);
27253 * @methodOf ui.grid.validate.service:uiGridValidateService
27254 * @description Returns the error message related to the validator
27255 * @param {string} name the name of the validator
27256 * @param {object} argument an argument to pass to the message function
27257 * @returns {string} the error message related to the validator
27259 getMessage: function(name, argument) {
27260 if (service.externalFactoryFunction) {
27261 var message = service.getMessageFromExternalFactory(name, argument);
27266 return service.validatorFactories[name].messageFunction(argument);
27272 * @methodOf ui.grid.validate.service:uiGridValidateService
27273 * @description Returns true if the cell (identified by rowEntity, colDef) is invalid
27274 * @param {object} rowEntity the row entity of the cell
27275 * @param {object} colDef the colDef of the cell
27276 * @returns {boolean} true if the cell is invalid
27278 isInvalid: function (rowEntity, colDef) {
27279 return rowEntity['$$invalid'+colDef.name];
27285 * @methodOf ui.grid.validate.service:uiGridValidateService
27286 * @description Makes the cell invalid by adding the proper field to the entity
27287 * @param {object} rowEntity the row entity of the cell
27288 * @param {object} colDef the colDef of the cell
27290 setInvalid: function (rowEntity, colDef) {
27291 rowEntity['$$invalid'+colDef.name] = true;
27297 * @methodOf ui.grid.validate.service:uiGridValidateService
27298 * @description Makes the cell valid by removing the proper error field from the entity
27299 * @param {object} rowEntity the row entity of the cell
27300 * @param {object} colDef the colDef of the cell
27302 setValid: function (rowEntity, colDef) {
27303 delete rowEntity['$$invalid'+colDef.name];
27309 * @methodOf ui.grid.validate.service:uiGridValidateService
27310 * @description Adds the proper error to the entity errors field
27311 * @param {object} rowEntity the row entity of the cell
27312 * @param {object} colDef the colDef of the cell
27313 * @param {string} validatorName the name of the validator that is failing
27315 setError: function(rowEntity, colDef, validatorName) {
27316 if (!rowEntity['$$errors'+colDef.name]) {
27317 rowEntity['$$errors'+colDef.name] = {};
27319 rowEntity['$$errors'+colDef.name][validatorName] = true;
27325 * @methodOf ui.grid.validate.service:uiGridValidateService
27326 * @description Removes the proper error from the entity errors field
27327 * @param {object} rowEntity the row entity of the cell
27328 * @param {object} colDef the colDef of the cell
27329 * @param {string} validatorName the name of the validator that is failing
27331 clearError: function(rowEntity, colDef, validatorName) {
27332 if (!rowEntity['$$errors'+colDef.name]) {
27335 if (validatorName in rowEntity['$$errors'+colDef.name]) {
27336 delete rowEntity['$$errors'+colDef.name][validatorName];
27342 * @name getErrorMessages
27343 * @methodOf ui.grid.validate.service:uiGridValidateService
27344 * @description returns an array of i18n-ed error messages.
27345 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27346 * @param {object} colDef the column whose errors we are looking for
27347 * @returns {array} An array of strings containing all the error messages for the cell
27349 getErrorMessages: function(rowEntity, colDef) {
27352 if (!rowEntity['$$errors'+colDef.name] || Object.keys(rowEntity['$$errors'+colDef.name]).length === 0) {
27356 Object.keys(rowEntity['$$errors'+colDef.name]).sort().forEach(function(validatorName) {
27357 errors.push(service.getMessage(validatorName, colDef.validators[validatorName]));
27365 * @name getFormattedErrors
27366 * @methodOf ui.grid.validate.service:uiGridValidateService
27367 * @description returns the error i18n-ed and formatted in html to be shown inside the page.
27368 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27369 * @param {object} colDef the column whose errors we are looking for
27370 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27371 * message inside the page (i.e. inside a div)
27373 getFormattedErrors: function(rowEntity, colDef) {
27375 var msgString = "";
27377 var errors = service.getErrorMessages(rowEntity, colDef);
27379 if (!errors.length) {
27383 errors.forEach(function(errorMsg) {
27384 msgString += errorMsg + "<br/>";
27387 return $sce.trustAsHtml('<p><b>' + i18nService.getSafeText('validate.error') + '</b></p>' + msgString );
27392 * @name getTitleFormattedErrors
27393 * @methodOf ui.grid.validate.service:uiGridValidateService
27394 * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html
27396 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27397 * @param {object} colDef the column whose errors we are looking for
27398 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27399 * message inside an html title attribute
27401 getTitleFormattedErrors: function(rowEntity, colDef) {
27403 var newLine = "\n";
27405 var msgString = "";
27407 var errors = service.getErrorMessages(rowEntity, colDef);
27409 if (!errors.length) {
27413 errors.forEach(function(errorMsg) {
27414 msgString += errorMsg + newLine;
27417 return $sce.trustAsHtml(i18nService.getSafeText('validate.error') + newLine + msgString);
27422 * @name getTitleFormattedErrors
27423 * @methodOf ui.grid.validate.service:uiGridValidateService
27424 * @description Executes all validators on a cell (identified by row entity and column definition) and sets or clears errors
27425 * @param {object} rowEntity the row entity of the cell we want to run the validators on
27426 * @param {object} colDef the column definition of the cell we want to run the validators on
27427 * @param {object} newValue the value the user just entered
27428 * @param {object} oldValue the value the field had before
27430 runValidators: function(rowEntity, colDef, newValue, oldValue, grid) {
27432 if (newValue === oldValue) {
27433 // If the value has not changed we perform no validation
27437 if (typeof(colDef.name) === 'undefined' || !colDef.name) {
27438 throw new Error('colDef.name is required to perform validation');
27441 service.setValid(rowEntity, colDef);
27443 var validateClosureFactory = function(rowEntity, colDef, validatorName) {
27444 return function(value) {
27446 service.setInvalid(rowEntity, colDef);
27447 service.setError(rowEntity, colDef, validatorName);
27449 grid.api.validate.raise.validationFailed(rowEntity, colDef, newValue, oldValue);
27455 for (var validatorName in colDef.validators) {
27456 service.clearError(rowEntity, colDef, validatorName);
27458 var validatorFunction = service.getValidator(validatorName, colDef.validators[validatorName]);
27459 // We pass the arguments as oldValue, newValue so they are in the same order
27460 // as ng-model validators (modelValue, viewValue)
27461 $q.when(validatorFunction(oldValue, newValue, rowEntity, colDef))
27462 .then(validateClosureFactory(rowEntity, colDef, validatorName)
27469 * @name createDefaultValidators
27470 * @methodOf ui.grid.validate.service:uiGridValidateService
27471 * @description adds the basic validators to the list of service validators
27473 createDefaultValidators: function() {
27474 service.setValidator('minLength',
27475 function (argument) {
27476 return function (oldValue, newValue, rowEntity, colDef) {
27477 if (newValue === undefined || newValue === null || newValue === '') {
27480 return newValue.length >= argument;
27483 function(argument) {
27484 return i18nService.getSafeText('validate.minLength').replace('THRESHOLD', argument);
27487 service.setValidator('maxLength',
27488 function (argument) {
27489 return function (oldValue, newValue, rowEntity, colDef) {
27490 if (newValue === undefined || newValue === null || newValue === '') {
27493 return newValue.length <= argument;
27496 function(threshold) {
27497 return i18nService.getSafeText('validate.maxLength').replace('THRESHOLD', threshold);
27500 service.setValidator('required',
27501 function (argument) {
27502 return function (oldValue, newValue, rowEntity, colDef) {
27504 return !(newValue === undefined || newValue === null || newValue === '');
27509 function(argument) {
27510 return i18nService.getSafeText('validate.required');
27514 initializeGrid: function (scope, grid) {
27517 isInvalid: service.isInvalid,
27519 getFormattedErrors: service.getFormattedErrors,
27521 getTitleFormattedErrors: service.getTitleFormattedErrors,
27523 runValidators: service.runValidators
27528 * @name ui.grid.validate.api:PublicApi
27530 * @description Public Api for validation feature
27537 * @name validationFailed
27538 * @eventOf ui.grid.validate.api:PublicApi
27539 * @description raised when one or more failure happened during validation
27541 * gridApi.validate.on.validationFailed(scope, function(rowEntity, colDef, newValue, oldValue){...})
27543 * @param {object} rowEntity the options.data element whose validation failed
27544 * @param {object} colDef the column whose validation failed
27545 * @param {object} newValue new value
27546 * @param {object} oldValue old value
27548 validationFailed: function (rowEntity, colDef, newValue, oldValue) {
27557 * @methodOf ui.grid.validate.api:PublicApi
27558 * @description checks if a cell (identified by rowEntity, colDef) is invalid
27559 * @param {object} rowEntity gridOptions.data[] array instance we want to check
27560 * @param {object} colDef the column whose errors we want to check
27561 * @returns {boolean} true if the cell value is not valid
27563 isInvalid: function(rowEntity, colDef) {
27564 return grid.validate.isInvalid(rowEntity, colDef);
27568 * @name getErrorMessages
27569 * @methodOf ui.grid.validate.api:PublicApi
27570 * @description returns an array of i18n-ed error messages.
27571 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27572 * @param {object} colDef the column whose errors we are looking for
27573 * @returns {array} An array of strings containing all the error messages for the cell
27575 getErrorMessages: function (rowEntity, colDef) {
27576 return grid.validate.getErrorMessages(rowEntity, colDef);
27580 * @name getFormattedErrors
27581 * @methodOf ui.grid.validate.api:PublicApi
27582 * @description returns the error i18n-ed and formatted in html to be shown inside the page.
27583 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27584 * @param {object} colDef the column whose errors we are looking for
27585 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27586 * message inside the page (i.e. inside a div)
27588 getFormattedErrors: function (rowEntity, colDef) {
27589 return grid.validate.getFormattedErrors(rowEntity, colDef);
27593 * @name getTitleFormattedErrors
27594 * @methodOf ui.grid.validate.api:PublicApi
27595 * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html
27597 * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for
27598 * @param {object} colDef the column whose errors we are looking for
27599 * @returns {object} An object that can be used in a template (like a cellTemplate) to display the
27600 * message inside an html title attribute
27602 getTitleFormattedErrors: function (rowEntity, colDef) {
27603 return grid.validate.getTitleFormattedErrors(rowEntity, colDef);
27609 grid.api.registerEventsFromObject(publicApi.events);
27610 grid.api.registerMethodsFromObject(publicApi.methods);
27613 grid.api.edit.on.afterCellEdit(scope, function(rowEntity, colDef, newValue, oldValue) {
27614 grid.validate.runValidators(rowEntity, colDef, newValue, oldValue, grid);
27618 service.createDefaultValidators();
27629 * @name ui.grid.validate.directive:uiGridValidate
27632 * @description Adds validating features to the ui-grid directive.
27634 <example module="app">
27635 <file name="app.js">
27636 var app = angular.module('app', ['ui.grid', 'ui.grid.edit', 'ui.grid.validate']);
27638 app.controller('MainCtrl', ['$scope', function ($scope) {
27640 { name: 'Bob', title: 'CEO' },
27641 { name: 'Frank', title: 'Lowly Developer' }
27644 $scope.columnDefs = [
27645 {name: 'name', enableCellEdit: true, validators: {minLength: 3, maxLength: 9}, cellTemplate: 'ui-grid/cellTitleValidator'},
27646 {name: 'title', enableCellEdit: true, validators: {required: true}, cellTemplate: 'ui-grid/cellTitleValidator'}
27650 <file name="index.html">
27651 <div ng-controller="MainCtrl">
27652 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit ui-grid-validate></div>
27658 module.directive('uiGridValidate', ['gridUtil', 'uiGridValidateService', function (gridUtil, uiGridValidateService) {
27662 require: '^uiGrid',
27664 compile: function () {
27666 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
27667 uiGridValidateService.initializeGrid($scope, uiGridCtrl.grid);
27669 post: function ($scope, $elm, $attrs, uiGridCtrl) {
27676 angular.module('ui.grid').run(['$templateCache', function($templateCache) {
27679 $templateCache.put('ui-grid/ui-grid-filter',
27680 "<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>"
27684 $templateCache.put('ui-grid/ui-grid-footer',
27685 "<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>"
27689 $templateCache.put('ui-grid/ui-grid-grid-footer',
27690 "<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>"
27694 $templateCache.put('ui-grid/ui-grid-group-panel',
27695 "<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>"
27699 $templateCache.put('ui-grid/ui-grid-header',
27700 "<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>"
27704 $templateCache.put('ui-grid/ui-grid-menu-button',
27705 "<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>"
27709 $templateCache.put('ui-grid/ui-grid-no-header',
27710 "<div class=\"ui-grid-top-panel\"></div>"
27714 $templateCache.put('ui-grid/ui-grid-row',
27715 "<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>"
27719 $templateCache.put('ui-grid/ui-grid',
27720 "<div ui-i18n=\"en\" class=\"ui-grid\"><!-- TODO (c0bra): add \"scoped\" attr here, eventually? --><style ui-grid-style>.grid{{ grid.id }} {\n" +
27721 " /* Styles for the grid */\n" +
27724 " .grid{{ grid.id }} .ui-grid-row, .grid{{ grid.id }} .ui-grid-cell, .grid{{ grid.id }} .ui-grid-cell .ui-grid-vertical-bar {\n" +
27725 " height: {{ grid.options.rowHeight }}px;\n" +
27728 " .grid{{ grid.id }} .ui-grid-row:last-child .ui-grid-cell {\n" +
27729 " border-bottom-width: {{ ((grid.getTotalRowHeight() < grid.getViewportHeight()) && '1') || '0' }}px;\n" +
27732 " {{ grid.verticalScrollbarStyles }}\n" +
27733 " {{ grid.horizontalScrollbarStyles }}\n" +
27736 " .ui-grid[dir=rtl] .ui-grid-viewport {\n" +
27737 " padding-left: {{ grid.verticalScrollbarWidth }}px;\n" +
27741 " {{ 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>"
27745 $templateCache.put('ui-grid/uiGridCell',
27746 "<div class=\"ui-grid-cell-contents\" title=\"TOOLTIP\">{{COL_FIELD CUSTOM_FILTERS}}</div>"
27750 $templateCache.put('ui-grid/uiGridColumnMenu',
27751 "<div class=\"ui-grid-column-menu\"><div ui-grid-menu menu-items=\"menuItems\"><!-- <div class=\"ui-grid-column-menu\">\n" +
27752 " <div class=\"inner\" ng-show=\"menuShown\">\n" +
27754 " <div ng-show=\"grid.options.enableSorting\">\n" +
27755 " <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" +
27756 " <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" +
27757 " <li ng-show=\"col.sort.direction\" ng-click=\"unsortColumn()\"><i class=\"ui-grid-icon-cancel\"></i> Remove Sort</li>\n" +
27761 " </div> --></div></div>"
27765 $templateCache.put('ui-grid/uiGridFooterCell',
27766 "<div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><div>{{ col.getAggregationText() + ( col.getAggregationValue() CUSTOM_FILTERS ) }}</div></div>"
27770 $templateCache.put('ui-grid/uiGridHeaderCell',
27771 "<div role=\"columnheader\" ng-class=\"{ 'sortable': sortable }\" ui-grid-one-bind-aria-labelledby-grid=\"col.uid + '-header-text ' + col.uid + '-sortdir-text'\" aria-sort=\"{{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending' : (!col.sort.direction ? 'none' : 'other'))}}\"><div role=\"button\" tabindex=\"0\" class=\"ui-grid-cell-contents ui-grid-header-cell-primary-focus\" col-index=\"renderIndex\" title=\"TOOLTIP\"><span class=\"ui-grid-header-cell-label\" ui-grid-one-bind-id-grid=\"col.uid + '-header-text'\">{{ col.displayName CUSTOM_FILTERS }}</span> <span ui-grid-one-bind-id-grid=\"col.uid + '-sortdir-text'\" ui-grid-visible=\"col.sort.direction\" aria-label=\"{{getSortDirectionAriaLabel()}}\"><i ng-class=\"{ 'ui-grid-icon-up-dir': col.sort.direction == asc, 'ui-grid-icon-down-dir': col.sort.direction == desc, 'ui-grid-icon-blank': !col.sort.direction }\" title=\"{{isSortPriorityVisible() ? i18n.headerCell.priority + ' ' + col.sort.priority : null}}\" aria-hidden=\"true\"></i> <sub ui-grid-visible=\"isSortPriorityVisible()\" 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>"
27775 $templateCache.put('ui-grid/uiGridMenu',
27776 "<div class=\"ui-grid-menu\" ng-if=\"shown\"><style ui-grid-style>{{dynamicStyles}}</style><div class=\"ui-grid-menu-mid\" ng-show=\"shownMid\"><div class=\"ui-grid-menu-inner\"><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>"
27780 $templateCache.put('ui-grid/uiGridMenuItem',
27781 "<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>"
27785 $templateCache.put('ui-grid/uiGridRenderContainer',
27786 "<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>"
27790 $templateCache.put('ui-grid/uiGridViewport',
27791 "<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>"
27795 $templateCache.put('ui-grid/cellEditor',
27796 "<div><form name=\"inputForm\"><input type=\"INPUT_TYPE\" ng-class=\"'colt' + col.uid\" ui-grid-editor ng-model=\"MODEL_COL_FIELD\"></form></div>"
27800 $templateCache.put('ui-grid/dropdownEditor',
27801 "<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>"
27805 $templateCache.put('ui-grid/fileChooserEditor',
27806 "<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>"
27810 $templateCache.put('ui-grid/expandableRow',
27811 "<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>"
27815 $templateCache.put('ui-grid/expandableRowHeader',
27816 "<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>"
27820 $templateCache.put('ui-grid/expandableScrollFiller',
27821 "<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>"
27825 $templateCache.put('ui-grid/expandableTopRowHeader',
27826 "<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>"
27830 $templateCache.put('ui-grid/csvLink',
27831 "<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>"
27835 $templateCache.put('ui-grid/importerMenuItem',
27836 "<li class=\"ui-grid-menu-item\"><form><input class=\"ui-grid-importer-file-chooser\" type=\"file\" id=\"files\" name=\"files[]\"></form></li>"
27840 $templateCache.put('ui-grid/importerMenuItemContainer',
27841 "<div ui-grid-importer-menu-item></div>"
27845 $templateCache.put('ui-grid/pagination',
27846 "<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>"
27850 $templateCache.put('ui-grid/columnResizer',
27851 "<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>"
27855 $templateCache.put('ui-grid/gridFooterSelectedItems',
27856 "<span ng-if=\"grid.selection.selectedCount !== 0 && grid.options.enableFooterTotalSelected\">({{\"search.selectedItems\" | t}} {{grid.selection.selectedCount}})</span>"
27860 $templateCache.put('ui-grid/selectionHeaderCell',
27861 "<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>"
27865 $templateCache.put('ui-grid/selectionRowHeader',
27866 "<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>"
27870 $templateCache.put('ui-grid/selectionRowHeaderButtons',
27871 "<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>"
27875 $templateCache.put('ui-grid/selectionSelectAllButtons',
27876 "<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>"
27880 $templateCache.put('ui-grid/treeBaseExpandAllButtons',
27881 "<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>"
27885 $templateCache.put('ui-grid/treeBaseHeaderCell',
27886 "<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>"
27890 $templateCache.put('ui-grid/treeBaseRowHeader',
27891 "<div class=\"ui-grid-cell-contents\"><ui-grid-tree-base-row-header-buttons></ui-grid-tree-base-row-header-buttons></div>"
27895 $templateCache.put('ui-grid/treeBaseRowHeaderButtons',
27896 "<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>"
27900 $templateCache.put('ui-grid/cellTitleValidator',
27901 "<div class=\"ui-grid-cell-contents\" ng-class=\"{invalid:grid.validate.isInvalid(row.entity,col.colDef)}\" title=\"{{grid.validate.getTitleFormattedErrors(row.entity,col.colDef)}}\">{{COL_FIELD CUSTOM_FILTERS}}</div>"
27905 $templateCache.put('ui-grid/cellTooltipValidator',
27906 "<div class=\"ui-grid-cell-contents\" ng-class=\"{invalid:grid.validate.isInvalid(row.entity,col.colDef)}\" tooltip-html-unsafe=\"{{grid.validate.getFormattedErrors(row.entity,col.colDef)}}\" tooltip-enable=\"grid.validate.isInvalid(row.entity,col.colDef)\" tooltip-append-to-body=\"true\" tooltip-placement=\"top\" title=\"TOOLTIP\">{{COL_FIELD CUSTOM_FILTERS}}</div>"