/*! * ui-grid - v4.0.2 - 2016-12-30 * Copyright (c) 2016 ; License: MIT */ (function () { 'use strict'; angular.module('ui.grid.i18n', []); angular.module('ui.grid', ['ui.grid.i18n']); })(); (function () { 'use strict'; /** * @ngdoc object * @name ui.grid.service:uiGridConstants * @description Constants for use across many grid features * */ angular.module('ui.grid').constant('uiGridConstants', { LOG_DEBUG_MESSAGES: true, LOG_WARN_MESSAGES: true, LOG_ERROR_MESSAGES: true, CUSTOM_FILTERS: /CUSTOM_FILTERS/g, COL_FIELD: /COL_FIELD/g, MODEL_COL_FIELD: /MODEL_COL_FIELD/g, TOOLTIP: /title=\"TOOLTIP\"/g, DISPLAY_CELL_TEMPLATE: /DISPLAY_CELL_TEMPLATE/g, TEMPLATE_REGEXP: /<.+>/, FUNC_REGEXP: /(\([^)]*\))?$/, DOT_REGEXP: /\./g, APOS_REGEXP: /'/g, BRACKET_REGEXP: /^(.*)((?:\s*\[\s*\d+\s*\]\s*)|(?:\s*\[\s*"(?:[^"\\]|\\.)*"\s*\]\s*)|(?:\s*\[\s*'(?:[^'\\]|\\.)*'\s*\]\s*))(.*)$/, COL_CLASS_PREFIX: 'ui-grid-col', ENTITY_BINDING: '$$this', events: { GRID_SCROLL: 'uiGridScroll', COLUMN_MENU_SHOWN: 'uiGridColMenuShown', ITEM_DRAGGING: 'uiGridItemDragStart', // For any item being dragged COLUMN_HEADER_CLICK: 'uiGridColumnHeaderClick' }, // copied from http://www.lsauer.com/2011/08/javascript-keymap-keycodes-in-json.html keymap: { TAB: 9, STRG: 17, CAPSLOCK: 20, CTRL: 17, CTRLRIGHT: 18, CTRLR: 18, SHIFT: 16, RETURN: 13, ENTER: 13, BACKSPACE: 8, BCKSP: 8, ALT: 18, ALTR: 17, ALTRIGHT: 17, SPACE: 32, WIN: 91, MAC: 91, FN: null, PG_UP: 33, PG_DOWN: 34, UP: 38, DOWN: 40, LEFT: 37, RIGHT: 39, ESC: 27, DEL: 46, F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123 }, /** * @ngdoc object * @name ASC * @propertyOf ui.grid.service:uiGridConstants * @description Used in {@link ui.grid.class:GridOptions.columnDef#properties_sort columnDef.sort} and * {@link ui.grid.class:GridOptions.columnDef#properties_sortDirectionCycle columnDef.sortDirectionCycle} * to configure the sorting direction of the column */ ASC: 'asc', /** * @ngdoc object * @name DESC * @propertyOf ui.grid.service:uiGridConstants * @description Used in {@link ui.grid.class:GridOptions.columnDef#properties_sort columnDef.sort} and * {@link ui.grid.class:GridOptions.columnDef#properties_sortDirectionCycle columnDef.sortDirectionCycle} * to configure the sorting direction of the column */ DESC: 'desc', /** * @ngdoc object * @name filter * @propertyOf ui.grid.service:uiGridConstants * @description Used in {@link ui.grid.class:GridOptions.columnDef#properties_filter columnDef.filter} * to configure filtering on the column * * `SELECT` and `INPUT` are used with the `type` property of the filter, the rest are used to specify * one of the built-in conditions. * * Available `condition` options are: * - `uiGridConstants.filter.STARTS_WITH` * - `uiGridConstants.filter.ENDS_WITH` * - `uiGridConstants.filter.CONTAINS` * - `uiGridConstants.filter.GREATER_THAN` * - `uiGridConstants.filter.GREATER_THAN_OR_EQUAL` * - `uiGridConstants.filter.LESS_THAN` * - `uiGridConstants.filter.LESS_THAN_OR_EQUAL` * - `uiGridConstants.filter.NOT_EQUAL` * - `uiGridConstants.filter.STARTS_WITH` * * * Available `type` options are: * - `uiGridConstants.filter.SELECT` - use a dropdown box for the cell header filter field * - `uiGridConstants.filter.INPUT` - use a text box for the cell header filter field */ filter: { STARTS_WITH: 2, ENDS_WITH: 4, EXACT: 8, CONTAINS: 16, GREATER_THAN: 32, GREATER_THAN_OR_EQUAL: 64, LESS_THAN: 128, LESS_THAN_OR_EQUAL: 256, NOT_EQUAL: 512, SELECT: 'select', INPUT: 'input' }, /** * @ngdoc object * @name aggregationTypes * @propertyOf ui.grid.service:uiGridConstants * @description Used in {@link ui.grid.class:GridOptions.columnDef#properties_aggregationType columnDef.aggregationType} * to specify the type of built-in aggregation the column should use. * * Available options are: * - `uiGridConstants.aggregationTypes.sum` - add the values in this column to produce the aggregated value * - `uiGridConstants.aggregationTypes.count` - count the number of rows to produce the aggregated value * - `uiGridConstants.aggregationTypes.avg` - average the values in this column to produce the aggregated value * - `uiGridConstants.aggregationTypes.min` - use the minimum value in this column as the aggregated value * - `uiGridConstants.aggregationTypes.max` - use the maximum value in this column as the aggregated value */ aggregationTypes: { sum: 2, count: 4, avg: 8, min: 16, max: 32 }, /** * @ngdoc array * @name CURRENCY_SYMBOLS * @propertyOf ui.grid.service:uiGridConstants * @description A list of all presently circulating currency symbols that was copied from * https://en.wikipedia.org/wiki/Currency_symbol#List_of_presently-circulating_currency_symbols * * Can be used on {@link ui.grid.class:rowSorter} to create a number string regex that ignores currency symbols. */ CURRENCY_SYMBOLS: ['¤', '؋', 'Ar', 'Ƀ', '฿', 'B/.', 'Br', 'Bs.', 'Bs.F.', 'GH₵', '¢', 'c', 'Ch.', '₡', 'C$', 'D', 'ден', 'دج', '.د.ب', 'د.ع', 'JD', 'د.ك', 'ل.د', 'дин', 'د.ت', 'د.م.', 'د.إ', 'Db', '$', '₫', 'Esc', '€', 'ƒ', 'Ft', 'FBu', 'FCFA', 'CFA', 'Fr', 'FRw', 'G', 'gr', '₲', 'h', '₴', '₭', 'Kč', 'kr', 'kn', 'MK', 'ZK', 'Kz', 'K', 'L', 'Le', 'лв', 'E', 'lp', 'M', 'KM', 'MT', '₥', 'Nfk', '₦', 'Nu.', 'UM', 'T$', 'MOP$', '₱', 'Pt.', '£', 'ج.م.', 'LL', 'LS', 'P', 'Q', 'q', 'R', 'R$', 'ر.ع.', 'ر.ق', 'ر.س', '៛', 'RM', 'p', 'Rf.', '₹', '₨', 'SRe', 'Rp', '₪', 'Ksh', 'Sh.So.', 'USh', 'S/', 'SDR', 'сом', '৳ ', 'WS$', '₮', 'VT', '₩', '¥', 'zł'], /** * @ngdoc object * @name scrollDirection * @propertyOf ui.grid.service:uiGridConstants * @description Set on {@link ui.grid.class:Grid#properties_scrollDirection Grid.scrollDirection}, * to indicate the direction the grid is currently scrolling in * * Available options are: * - `uiGridConstants.scrollDirection.UP` - set when the grid is scrolling up * - `uiGridConstants.scrollDirection.DOWN` - set when the grid is scrolling down * - `uiGridConstants.scrollDirection.LEFT` - set when the grid is scrolling left * - `uiGridConstants.scrollDirection.RIGHT` - set when the grid is scrolling right * - `uiGridConstants.scrollDirection.NONE` - set when the grid is not scrolling, this is the default */ scrollDirection: { UP: 'up', DOWN: 'down', LEFT: 'left', RIGHT: 'right', NONE: 'none' }, /** * @ngdoc object * @name dataChange * @propertyOf ui.grid.service:uiGridConstants * @description Used with {@link ui.grid.core.api:PublicApi#methods_notifyDataChange PublicApi.notifyDataChange}, * {@link ui.grid.class:Grid#methods_callDataChangeCallbacks Grid.callDataChangeCallbacks}, * and {@link ui.grid.class:Grid#methods_registerDataChangeCallback Grid.registerDataChangeCallback} * to specify the type of the event(s). * * Available options are: * - `uiGridConstants.dataChange.ALL` - listeners fired on any of these events, fires listeners on all events. * - `uiGridConstants.dataChange.EDIT` - fired when the data in a cell is edited * - `uiGridConstants.dataChange.ROW` - fired when a row is added or removed * - `uiGridConstants.dataChange.COLUMN` - fired when the column definitions are modified * - `uiGridConstants.dataChange.OPTIONS` - fired when the grid options are modified */ dataChange: { ALL: 'all', EDIT: 'edit', ROW: 'row', COLUMN: 'column', OPTIONS: 'options' }, /** * @ngdoc object * @name scrollbars * @propertyOf ui.grid.service:uiGridConstants * @description Used with {@link ui.grid.class:GridOptions#properties_enableHorizontalScrollbar GridOptions.enableHorizontalScrollbar} * and {@link ui.grid.class:GridOptions#properties_enableVerticalScrollbar GridOptions.enableVerticalScrollbar} * to specify the scrollbar policy for that direction. * * Available options are: * - `uiGridConstants.scrollbars.NEVER` - never show scrollbars in this direction * - `uiGridConstants.scrollbars.ALWAYS` - always show scrollbars in this direction */ scrollbars: { NEVER: 0, ALWAYS: 1 //WHEN_NEEDED: 2 } }); })(); angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUtil', 'uiGridConstants', function ($compile, $parse, gridUtil, uiGridConstants) { var uiGridCell = { priority: 0, scope: false, require: '?^uiGrid', compile: function() { return { pre: function($scope, $elm, $attrs, uiGridCtrl) { function compileTemplate() { var compiledElementFn = $scope.col.compiledElementFn; compiledElementFn($scope, function(clonedElement, scope) { $elm.append(clonedElement); }); } // If the grid controller is present, use it to get the compiled cell template function if (uiGridCtrl && $scope.col.compiledElementFn) { compileTemplate(); } // No controller, compile the element manually (for unit tests) else { if ( uiGridCtrl && !$scope.col.compiledElementFn ){ // gridUtil.logError('Render has been called before pronapile. Please log a ui-grid issue'); $scope.col.getCompiledElementFn() .then(function (compiledElementFn) { compiledElementFn($scope, function(clonedElement, scope) { $elm.append(clonedElement); }); }); } else { var html = $scope.col.cellTemplate .replace(uiGridConstants.MODEL_COL_FIELD, 'row.entity.' + gridUtil.preEval($scope.col.field)) .replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)'); var cellElement = $compile(html)($scope); $elm.append(cellElement); } } }, post: function($scope, $elm, $attrs, uiGridCtrl) { var initColClass = $scope.col.getColClass(false); $elm.addClass(initColClass); var classAdded; var updateClass = function( grid ){ var contents = $elm; if ( classAdded ){ contents.removeClass( classAdded ); classAdded = null; } if (angular.isFunction($scope.col.cellClass)) { classAdded = $scope.col.cellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex); } else { classAdded = $scope.col.cellClass; } contents.addClass(classAdded); }; if ($scope.col.cellClass) { updateClass(); } // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN, uiGridConstants.dataChange.EDIT]); // watch the col and row to see if they change - which would indicate that we've scrolled or sorted or otherwise // changed the row/col that this cell relates to, and we need to re-evaluate cell classes and maybe other things var cellChangeFunction = function( n, o ){ if ( n !== o ) { if ( classAdded || $scope.col.cellClass ){ updateClass(); } // See if the column's internal class has changed var newColClass = $scope.col.getColClass(false); if (newColClass !== initColClass) { $elm.removeClass(initColClass); $elm.addClass(newColClass); initColClass = newColClass; } } }; // TODO(c0bra): Turn this into a deep array watch /* shouldn't be needed any more given track by col.name var colWatchDereg = $scope.$watch( 'col', cellChangeFunction ); */ var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction ); var deregisterFunction = function() { dataChangeDereg(); // colWatchDereg(); rowWatchDereg(); }; $scope.$on( '$destroy', deregisterFunction ); $elm.on( '$destroy', deregisterFunction ); } }; } }; return uiGridCell; }]); (function(){ angular.module('ui.grid') .service('uiGridColumnMenuService', [ 'i18nService', 'uiGridConstants', 'gridUtil', function ( i18nService, uiGridConstants, gridUtil ) { /** * @ngdoc service * @name ui.grid.service:uiGridColumnMenuService * * @description Services for working with column menus, factored out * to make the code easier to understand */ var service = { /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name initialize * @description Sets defaults, puts a reference to the $scope on * the uiGridController * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {controller} uiGridCtrl the uiGridController for the grid * we're on * */ initialize: function( $scope, uiGridCtrl ){ $scope.grid = uiGridCtrl.grid; // Store a reference to this link/controller in the main uiGrid controller // to allow showMenu later uiGridCtrl.columnMenuScope = $scope; // Save whether we're shown or not so the columns can check $scope.menuShown = false; }, /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name setColMenuItemWatch * @description Setup a watch on $scope.col.menuItems, and update * menuItems based on this. $scope.col needs to be set by the column * before calling the menu. * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {controller} uiGridCtrl the uiGridController for the grid * we're on * */ setColMenuItemWatch: function ( $scope ){ var deregFunction = $scope.$watch('col.menuItems', function (n) { if (typeof(n) !== 'undefined' && n && angular.isArray(n)) { n.forEach(function (item) { if (typeof(item.context) === 'undefined' || !item.context) { item.context = {}; } item.context.col = $scope.col; }); $scope.menuItems = $scope.defaultMenuItems.concat(n); } else { $scope.menuItems = $scope.defaultMenuItems; } }); $scope.$on( '$destroy', deregFunction ); }, /** * @ngdoc boolean * @name enableSorting * @propertyOf ui.grid.class:GridOptions.columnDef * @description (optional) True by default. When enabled, this setting adds sort * widgets to the column header, allowing sorting of the data in the individual column. */ /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name sortable * @description determines whether this column is sortable * @param {$scope} $scope the $scope from the uiGridColumnMenu * */ sortable: function( $scope ) { if ( $scope.grid.options.enableSorting && typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.enableSorting) { return true; } else { return false; } }, /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name isActiveSort * @description determines whether the requested sort direction is current active, to * allow highlighting in the menu * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {string} direction the direction that we'd have selected for us to be active * */ isActiveSort: function( $scope, direction ){ return (typeof($scope.col) !== 'undefined' && typeof($scope.col.sort) !== 'undefined' && typeof($scope.col.sort.direction) !== 'undefined' && $scope.col.sort.direction === direction); }, /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name suppressRemoveSort * @description determines whether we should suppress the removeSort option * @param {$scope} $scope the $scope from the uiGridColumnMenu * */ suppressRemoveSort: function( $scope ) { if ($scope.col && $scope.col.suppressRemoveSort) { return true; } else { return false; } }, /** * @ngdoc boolean * @name enableHiding * @propertyOf ui.grid.class:GridOptions.columnDef * @description (optional) True by default. When set to false, this setting prevents a user from hiding the column * using the column menu or the grid menu. */ /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name hideable * @description determines whether a column can be hidden, by checking the enableHiding columnDef option * @param {$scope} $scope the $scope from the uiGridColumnMenu * */ hideable: function( $scope ) { if (typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.colDef && $scope.col.colDef.enableHiding === false ) { return false; } else { return true; } }, /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name getDefaultMenuItems * @description returns the default menu items for a column menu * @param {$scope} $scope the $scope from the uiGridColumnMenu * */ getDefaultMenuItems: function( $scope ){ return [ { title: i18nService.getSafeText('sort.ascending'), icon: 'ui-grid-icon-sort-alt-up', action: function($event) { $event.stopPropagation(); $scope.sortColumn($event, uiGridConstants.ASC); }, shown: function () { return service.sortable( $scope ); }, active: function() { return service.isActiveSort( $scope, uiGridConstants.ASC); } }, { title: i18nService.getSafeText('sort.descending'), icon: 'ui-grid-icon-sort-alt-down', action: function($event) { $event.stopPropagation(); $scope.sortColumn($event, uiGridConstants.DESC); }, shown: function() { return service.sortable( $scope ); }, active: function() { return service.isActiveSort( $scope, uiGridConstants.DESC); } }, { title: i18nService.getSafeText('sort.remove'), icon: 'ui-grid-icon-cancel', action: function ($event) { $event.stopPropagation(); $scope.unsortColumn(); }, shown: function() { return service.sortable( $scope ) && typeof($scope.col) !== 'undefined' && (typeof($scope.col.sort) !== 'undefined' && typeof($scope.col.sort.direction) !== 'undefined') && $scope.col.sort.direction !== null && !service.suppressRemoveSort( $scope ); } }, { title: i18nService.getSafeText('column.hide'), icon: 'ui-grid-icon-cancel', shown: function() { return service.hideable( $scope ); }, action: function ($event) { $event.stopPropagation(); $scope.hideColumn(); } } ]; }, /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name getColumnElementPosition * @description gets the position information needed to place the column * menu below the column header * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {GridCol} column the column we want to position below * @param {element} $columnElement the column element we want to position below * @returns {hash} containing left, top, offset, height, width * */ getColumnElementPosition: function( $scope, column, $columnElement ){ var positionData = {}; positionData.left = $columnElement[0].offsetLeft; positionData.top = $columnElement[0].offsetTop; positionData.parentLeft = $columnElement[0].offsetParent.offsetLeft; // Get the grid scrollLeft positionData.offset = 0; if (column.grid.options.offsetLeft) { positionData.offset = column.grid.options.offsetLeft; } positionData.height = gridUtil.elementHeight($columnElement, true); positionData.width = gridUtil.elementWidth($columnElement, true); return positionData; }, /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name repositionMenu * @description Reposition the menu below the new column. If the menu has no child nodes * (i.e. it's not currently visible) then we guess it's width at 100, we'll be called again * later to fix it * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {GridCol} column the column we want to position below * @param {hash} positionData a hash containing left, top, offset, height, width * @param {element} $elm the column menu element that we want to reposition * @param {element} $columnElement the column element that we want to reposition underneath * */ repositionMenu: function( $scope, column, positionData, $elm, $columnElement ) { var menu = $elm[0].querySelectorAll('.ui-grid-menu'); // It's possible that the render container of the column we're attaching to is // offset from the grid (i.e. pinned containers), we need to get the difference in the offsetLeft // between the render container and the grid var renderContainerElm = gridUtil.closestElm($columnElement, '.ui-grid-render-container'); var renderContainerOffset = renderContainerElm.getBoundingClientRect().left - $scope.grid.element[0].getBoundingClientRect().left; var containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].scrollLeft; // default value the last width for _this_ column, otherwise last width for _any_ column, otherwise default to 170 var myWidth = column.lastMenuWidth ? column.lastMenuWidth : ( $scope.lastMenuWidth ? $scope.lastMenuWidth : 170); var paddingRight = column.lastMenuPaddingRight ? column.lastMenuPaddingRight : ( $scope.lastMenuPaddingRight ? $scope.lastMenuPaddingRight : 10); if ( menu.length !== 0 ){ var mid = menu[0].querySelectorAll('.ui-grid-menu-mid'); if ( mid.length !== 0 && !angular.element(mid).hasClass('ng-hide') ) { myWidth = gridUtil.elementWidth(menu, true); $scope.lastMenuWidth = myWidth; column.lastMenuWidth = myWidth; // TODO(c0bra): use padding-left/padding-right based on document direction (ltr/rtl), place menu on proper side // Get the column menu right padding paddingRight = parseInt(gridUtil.getStyles(angular.element(menu)[0])['paddingRight'], 10); $scope.lastMenuPaddingRight = paddingRight; column.lastMenuPaddingRight = paddingRight; } } var left = positionData.left + renderContainerOffset - containerScrollLeft + positionData.parentLeft + positionData.width - myWidth + paddingRight; if (left < positionData.offset){ left = positionData.offset; } $elm.css('left', left + 'px'); $elm.css('top', (positionData.top + positionData.height) + 'px'); } }; return service; }]) .directive('uiGridColumnMenu', ['$timeout', 'gridUtil', 'uiGridConstants', 'uiGridColumnMenuService', '$document', function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $document) { /** * @ngdoc directive * @name ui.grid.directive:uiGridColumnMenu * @description Provides the column menu framework, leverages uiGridMenu underneath * */ var uiGridColumnMenu = { priority: 0, scope: true, require: '^uiGrid', templateUrl: 'ui-grid/uiGridColumnMenu', replace: true, link: function ($scope, $elm, $attrs, uiGridCtrl) { uiGridColumnMenuService.initialize( $scope, uiGridCtrl ); $scope.defaultMenuItems = uiGridColumnMenuService.getDefaultMenuItems( $scope ); // Set the menu items for use with the column menu. The user can later add additional items via the watch $scope.menuItems = $scope.defaultMenuItems; uiGridColumnMenuService.setColMenuItemWatch( $scope ); /** * @ngdoc method * @methodOf ui.grid.directive:uiGridColumnMenu * @name showMenu * @description Shows the column menu. If the menu is already displayed it * calls the menu to ask it to hide (it will animate), then it repositions the menu * to the right place whilst hidden (it will make an assumption on menu width), * then it asks the menu to show (it will animate), then it repositions the menu again * once we can calculate it's size. * @param {GridCol} column the column we want to position below * @param {element} $columnElement the column element we want to position below */ $scope.showMenu = function(column, $columnElement, event) { // Swap to this column $scope.col = column; // Get the position information for the column element var colElementPosition = uiGridColumnMenuService.getColumnElementPosition( $scope, column, $columnElement ); if ($scope.menuShown) { // we want to hide, then reposition, then show, but we want to wait for animations // we set a variable, and then rely on the menu-hidden event to call the reposition and show $scope.colElement = $columnElement; $scope.colElementPosition = colElementPosition; $scope.hideThenShow = true; $scope.$broadcast('hide-menu', { originalEvent: event }); } else { $scope.menuShown = true; uiGridColumnMenuService.repositionMenu( $scope, column, colElementPosition, $elm, $columnElement ); $scope.colElement = $columnElement; $scope.colElementPosition = colElementPosition; $scope.$broadcast('show-menu', { originalEvent: event }); } }; /** * @ngdoc method * @methodOf ui.grid.directive:uiGridColumnMenu * @name hideMenu * @description Hides the column menu. * @param {boolean} broadcastTrigger true if we were triggered by a broadcast * from the menu itself - in which case don't broadcast again as we'll get * an infinite loop */ $scope.hideMenu = function( broadcastTrigger ) { $scope.menuShown = false; if ( !broadcastTrigger ){ $scope.$broadcast('hide-menu'); } }; $scope.$on('menu-hidden', function() { if ( $scope.hideThenShow ){ delete $scope.hideThenShow; uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement ); $scope.$broadcast('show-menu'); $scope.menuShown = true; } else { $scope.hideMenu( true ); if ($scope.col) { //Focus on the menu button gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false); } } }); $scope.$on('menu-shown', function() { $timeout( function() { uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement ); //Focus on the first item gridUtil.focus.bySelector($document, '.ui-grid-menu-items .ui-grid-menu-item', true); delete $scope.colElementPosition; delete $scope.columnElement; }, 200); }); /* Column methods */ $scope.sortColumn = function (event, dir) { event.stopPropagation(); $scope.grid.sortColumn($scope.col, dir, true) .then(function () { $scope.grid.refresh(); $scope.hideMenu(); }); }; $scope.unsortColumn = function () { $scope.col.unsort(); $scope.grid.refresh(); $scope.hideMenu(); }; //Since we are hiding this column the default hide action will fail so we need to focus somewhere else. var setFocusOnHideColumn = function(){ $timeout(function(){ // Get the UID of the first var focusToGridMenu = function(){ return gridUtil.focus.byId('grid-menu', $scope.grid); }; var thisIndex; $scope.grid.columns.some(function(element, index){ if (angular.equals(element, $scope.col)) { thisIndex = index; return true; } }); var previousVisibleCol; // Try and find the next lower or nearest column to focus on $scope.grid.columns.some(function(element, index){ if (!element.visible){ return false; } // This columns index is below the current column index else if ( index < thisIndex){ previousVisibleCol = element; } // This elements index is above this column index and we haven't found one that is lower else if ( index > thisIndex && !previousVisibleCol) { // This is the next best thing previousVisibleCol = element; // We've found one so use it. return true; } // We've reached an element with an index above this column and the previousVisibleCol variable has been set else if (index > thisIndex && previousVisibleCol) { // We are done. return true; } }); // If found then focus on it if (previousVisibleCol){ var colClass = previousVisibleCol.getColClass(); gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + colClass+ ' .ui-grid-header-cell-primary-focus', true).then(angular.noop, function(reason){ if (reason !== 'canceled'){ // If this is canceled then don't perform the action //The fallback action is to focus on the grid menu return focusToGridMenu(); } }); } else { // Fallback action to focus on the grid menu focusToGridMenu(); } }); }; $scope.hideColumn = function () { $scope.col.colDef.visible = false; $scope.col.visible = false; $scope.grid.queueGridRefresh(); $scope.hideMenu(); $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col ); // We are hiding so the default action of focusing on the button that opened this menu will fail. setFocusOnHideColumn(); }; }, controller: ['$scope', function ($scope) { var self = this; $scope.$watch('menuItems', function (n) { self.menuItems = n; }); }] }; return uiGridColumnMenu; }]); })(); (function(){ 'use strict'; angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', 'i18nService', 'gridUtil', function ($compile, $templateCache, i18nService, gridUtil) { return { compile: function() { return { pre: function ($scope, $elm, $attrs, controllers) { $scope.col.updateFilters = function( filterable ){ $elm.children().remove(); if ( filterable ){ var template = $scope.col.filterHeaderTemplate; $elm.append($compile(template)($scope)); } }; $scope.$on( '$destroy', function() { delete $scope.col.updateFilters; }); }, post: function ($scope, $elm, $attrs, controllers){ $scope.aria = i18nService.getSafeText('headerCell.aria'); $scope.removeFilter = function(colFilter, index){ colFilter.term = null; //Set the focus to the filter input after the action disables the button gridUtil.focus.bySelector($elm, '.ui-grid-filter-input-' + index); }; } }; } }; }]); })(); (function () { 'use strict'; angular.module('ui.grid').directive('uiGridFooterCell', ['$timeout', 'gridUtil', 'uiGridConstants', '$compile', function ($timeout, gridUtil, uiGridConstants, $compile) { var uiGridFooterCell = { priority: 0, scope: { col: '=', row: '=', renderIndex: '=' }, replace: true, require: '^uiGrid', compile: function compile(tElement, tAttrs, transclude) { return { pre: function ($scope, $elm, $attrs, uiGridCtrl) { var cellFooter = $compile($scope.col.footerCellTemplate)($scope); $elm.append(cellFooter); }, post: function ($scope, $elm, $attrs, uiGridCtrl) { //$elm.addClass($scope.col.getColClass(false)); $scope.grid = uiGridCtrl.grid; var initColClass = $scope.col.getColClass(false); $elm.addClass(initColClass); // apply any footerCellClass var classAdded; var updateClass = function( grid ){ var contents = $elm; if ( classAdded ){ contents.removeClass( classAdded ); classAdded = null; } if (angular.isFunction($scope.col.footerCellClass)) { classAdded = $scope.col.footerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex); } else { classAdded = $scope.col.footerCellClass; } contents.addClass(classAdded); }; if ($scope.col.footerCellClass) { updateClass(); } $scope.col.updateAggregationValue(); // Watch for column changes so we can alter the col cell class properly /* shouldn't be needed any more, given track by col.name $scope.$watch('col', function (n, o) { if (n !== o) { // See if the column's internal class has changed var newColClass = $scope.col.getColClass(false); if (newColClass !== initColClass) { $elm.removeClass(initColClass); $elm.addClass(newColClass); initColClass = newColClass; } } }); */ // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]); // listen for visible rows change and update aggregation values $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue ); $scope.grid.api.core.on.rowsRendered( $scope, updateClass ); $scope.$on( '$destroy', dataChangeDereg ); } }; } }; return uiGridFooterCell; }]); })(); (function () { 'use strict'; angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { return { restrict: 'EA', replace: true, // priority: 1000, require: ['^uiGrid', '^uiGridRenderContainer'], scope: true, compile: function ($elm, $attrs) { return { pre: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; $scope.grid = uiGridCtrl.grid; $scope.colContainer = containerCtrl.colContainer; containerCtrl.footer = $elm; var footerTemplate = $scope.grid.options.footerTemplate; gridUtil.getTemplate(footerTemplate) .then(function (contents) { var template = angular.element(contents); var newElm = $compile(template)($scope); $elm.append(newElm); if (containerCtrl) { // Inject a reference to the footer viewport (if it exists) into the grid controller for use in the horizontal scroll handler below var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0]; if (footerViewport) { containerCtrl.footerViewport = footerViewport; } } }); }, post: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; // gridUtil.logDebug('ui-grid-footer link'); var grid = uiGridCtrl.grid; // Don't animate footer cells gridUtil.disableAnimations($elm); containerCtrl.footer = $elm; var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0]; if (footerViewport) { containerCtrl.footerViewport = footerViewport; } } }; } }; }]); })(); (function () { 'use strict'; angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { return { restrict: 'EA', replace: true, // priority: 1000, require: '^uiGrid', scope: true, compile: function ($elm, $attrs) { return { pre: function ($scope, $elm, $attrs, uiGridCtrl) { $scope.grid = uiGridCtrl.grid; var footerTemplate = $scope.grid.options.gridFooterTemplate; gridUtil.getTemplate(footerTemplate) .then(function (contents) { var template = angular.element(contents); var newElm = $compile(template)($scope); $elm.append(newElm); }); }, post: function ($scope, $elm, $attrs, controllers) { } }; } }; }]); })(); (function(){ 'use strict'; angular.module('ui.grid').directive('uiGridGroupPanel', ["$compile", "uiGridConstants", "gridUtil", function($compile, uiGridConstants, gridUtil) { var defaultTemplate = 'ui-grid/ui-grid-group-panel'; return { restrict: 'EA', replace: true, require: '?^uiGrid', scope: false, compile: function($elm, $attrs) { return { pre: function ($scope, $elm, $attrs, uiGridCtrl) { var groupPanelTemplate = $scope.grid.options.groupPanelTemplate || defaultTemplate; gridUtil.getTemplate(groupPanelTemplate) .then(function (contents) { var template = angular.element(contents); var newElm = $compile(template)($scope); $elm.append(newElm); }); }, post: function ($scope, $elm, $attrs, uiGridCtrl) { $elm.bind('$destroy', function() { // scrollUnbinder(); }); } }; } }; }]); })(); (function(){ 'use strict'; angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService', function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) { // Do stuff after mouse has been down this many ms on the header cell var mousedownTimeout = 500; var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa var uiGridHeaderCell = { priority: 0, scope: { col: '=', row: '=', renderIndex: '=' }, require: ['^uiGrid', '^uiGridRenderContainer'], replace: true, compile: function() { return { pre: function ($scope, $elm, $attrs) { var cellHeader = $compile($scope.col.headerCellTemplate)($scope); $elm.append(cellHeader); }, post: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0]; var renderContainerCtrl = controllers[1]; $scope.i18n = { headerCell: i18nService.getSafeText('headerCell'), sort: i18nService.getSafeText('sort') }; $scope.isSortPriorityVisible = function() { //show sort priority if column is sorted and there is at least one other sorted column return angular.isNumber($scope.col.sort.priority) && $scope.grid.columns.some(function(element, index){ return angular.isNumber(element.sort.priority) && element !== $scope.col; }); }; $scope.getSortDirectionAriaLabel = function(){ var col = $scope.col; //Trying to recreate this sort of thing but it was getting messy having it in the template. //Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''} var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none); var label = sortDirectionText; if ($scope.isSortPriorityVisible()) { label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority; } return label; }; $scope.grid = uiGridCtrl.grid; $scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId]; var initColClass = $scope.col.getColClass(false); $elm.addClass(initColClass); // Hide the menu by default $scope.menuShown = false; // Put asc and desc sort directions in scope $scope.asc = uiGridConstants.ASC; $scope.desc = uiGridConstants.DESC; // Store a reference to menu element var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') ); var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') ); // apply any headerCellClass var classAdded; var previousMouseX; // filter watchers var filterDeregisters = []; /* * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart). * Once we have a down event, we need to work out whether we have a click, a drag, or a * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so * we ignore it. A hold would open the menu. * * So, on down event, we put in place handlers for move and up events, and a timer. If the * timer expires before we see a move or up, then we have a long press and hence a column menu open. * If the up happens before the timer, then we have a click, and we sort if the column is sortable. * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature * will handle it. * * To deal with touch enabled devices that also have mice, we only create our handlers when * we get the down event, and we create the corresponding handlers - if we're touchstart then * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup. * * We also suppress the click action whilst this is happening - otherwise after the mouseup there * will be a click event and that can cause the column menu to close * */ $scope.downFn = function( event ){ event.stopPropagation(); if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { event = event.originalEvent; } // Don't show the menu if it's not the left button if (event.button && event.button !== 0) { return; } previousMouseX = event.pageX; $scope.mousedownStartTime = (new Date()).getTime(); $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout); $scope.mousedownTimeout.then(function () { if ( $scope.colMenu ) { uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); } }); uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); $scope.offAllEvents(); if ( event.type === 'touchstart'){ $document.on('touchend', $scope.upFn); $document.on('touchmove', $scope.moveFn); } else if ( event.type === 'mousedown' ){ $document.on('mouseup', $scope.upFn); $document.on('mousemove', $scope.moveFn); } }; $scope.upFn = function( event ){ event.stopPropagation(); $timeout.cancel($scope.mousedownTimeout); $scope.offAllEvents(); $scope.onDownEvents(event.type); var mousedownEndTime = (new Date()).getTime(); var mousedownTime = mousedownEndTime - $scope.mousedownStartTime; if (mousedownTime > mousedownTimeout) { // long click, handled above with mousedown } else { // short click if ( $scope.sortable ){ $scope.handleClick(event); } } }; $scope.moveFn = function( event ){ // Chrome is known to fire some bogus move events. var changeValue = event.pageX - previousMouseX; if ( changeValue === 0 ){ return; } // we're a move, so do nothing and leave for column move (if enabled) to take over $timeout.cancel($scope.mousedownTimeout); $scope.offAllEvents(); $scope.onDownEvents(event.type); }; $scope.clickFn = function ( event ){ event.stopPropagation(); $contentsElm.off('click', $scope.clickFn); }; $scope.offAllEvents = function(){ $contentsElm.off('touchstart', $scope.downFn); $contentsElm.off('mousedown', $scope.downFn); $document.off('touchend', $scope.upFn); $document.off('mouseup', $scope.upFn); $document.off('touchmove', $scope.moveFn); $document.off('mousemove', $scope.moveFn); $contentsElm.off('click', $scope.clickFn); }; $scope.onDownEvents = function( type ){ // If there is a previous event, then wait a while before // activating the other mode - i.e. if the last event was a touch event then // don't enable mouse events for a wee while (500ms or so) // Avoids problems with devices that emulate mouse events when you have touch events switch (type){ case 'touchmove': case 'touchend': $contentsElm.on('click', $scope.clickFn); $contentsElm.on('touchstart', $scope.downFn); $timeout(function(){ $contentsElm.on('mousedown', $scope.downFn); }, changeModeTimeout); break; case 'mousemove': case 'mouseup': $contentsElm.on('click', $scope.clickFn); $contentsElm.on('mousedown', $scope.downFn); $timeout(function(){ $contentsElm.on('touchstart', $scope.downFn); }, changeModeTimeout); break; default: $contentsElm.on('click', $scope.clickFn); $contentsElm.on('touchstart', $scope.downFn); $contentsElm.on('mousedown', $scope.downFn); } }; var updateHeaderOptions = function( grid ){ var contents = $elm; if ( classAdded ){ contents.removeClass( classAdded ); classAdded = null; } if (angular.isFunction($scope.col.headerCellClass)) { classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex); } else { classAdded = $scope.col.headerCellClass; } contents.addClass(classAdded); $timeout(function (){ var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body']; $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] ); }); // Figure out whether this column is sortable or not if ($scope.col.enableSorting) { $scope.sortable = true; } else { $scope.sortable = false; } // Figure out whether this column is filterable or not var oldFilterable = $scope.filterable; if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { $scope.filterable = true; } else { $scope.filterable = false; } if ( oldFilterable !== $scope.filterable){ if ( typeof($scope.col.updateFilters) !== 'undefined' ){ $scope.col.updateFilters($scope.filterable); } // if column is filterable add a filter watcher if ($scope.filterable) { $scope.col.filters.forEach( function(filter, i) { filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { if (n !== o) { uiGridCtrl.grid.api.core.raise.filterChanged(); uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); uiGridCtrl.grid.queueGridRefresh(); } })); }); $scope.$on('$destroy', function() { filterDeregisters.forEach( function(filterDeregister) { filterDeregister(); }); }); } else { filterDeregisters.forEach( function(filterDeregister) { filterDeregister(); }); } } // figure out whether we support column menus if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false && $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){ $scope.colMenu = true; } else { $scope.colMenu = false; } /** * @ngdoc property * @name enableColumnMenu * @propertyOf ui.grid.class:GridOptions.columnDef * @description if column menus are enabled, controls the column menus for this specific * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus * using this option. If gridOptions.enableColumnMenus === false then you get no column * menus irrespective of the value of this option ). Defaults to true. * */ /** * @ngdoc property * @name enableColumnMenus * @propertyOf ui.grid.class:GridOptions.columnDef * @description Override for column menus everywhere - if set to false then you get no * column menus. Defaults to true. * */ $scope.offAllEvents(); if ($scope.sortable || $scope.colMenu) { $scope.onDownEvents(); $scope.$on('$destroy', function () { $scope.offAllEvents(); }); } }; /* $scope.$watch('col', function (n, o) { if (n !== o) { // See if the column's internal class has changed var newColClass = $scope.col.getColClass(false); if (newColClass !== initColClass) { $elm.removeClass(initColClass); $elm.addClass(newColClass); initColClass = newColClass; } } }); */ updateHeaderOptions(); // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]); $scope.$on( '$destroy', dataChangeDereg ); $scope.handleClick = function(event) { // If the shift key is being held down, add this column to the sort var add = false; if (event.shiftKey) { add = true; } // Sort this column then rebuild the grid's rows uiGridCtrl.grid.sortColumn($scope.col, add) .then(function () { if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); } uiGridCtrl.grid.refresh(); }); }; $scope.toggleMenu = function(event) { event.stopPropagation(); // If the menu is already showing... if (uiGridCtrl.columnMenuScope.menuShown) { // ... and we're the column the menu is on... if (uiGridCtrl.columnMenuScope.col === $scope.col) { // ... hide it uiGridCtrl.columnMenuScope.hideMenu(); } // ... and we're NOT the column the menu is on else { // ... move the menu to our column uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm); } } // If the menu is NOT showing else { // ... show it on our column uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm); } }; } }; } }; return uiGridHeaderCell; }]); })(); (function(){ 'use strict'; angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent', function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) { var defaultTemplate = 'ui-grid/ui-grid-header'; var emptyTemplate = 'ui-grid/ui-grid-no-header'; return { restrict: 'EA', // templateUrl: 'ui-grid/ui-grid-header', replace: true, // priority: 1000, require: ['^uiGrid', '^uiGridRenderContainer'], scope: true, compile: function($elm, $attrs) { return { pre: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; $scope.grid = uiGridCtrl.grid; $scope.colContainer = containerCtrl.colContainer; updateHeaderReferences(); var headerTemplate; if (!$scope.grid.options.showHeader) { headerTemplate = emptyTemplate; } else { headerTemplate = ($scope.grid.options.headerTemplate) ? $scope.grid.options.headerTemplate : defaultTemplate; } gridUtil.getTemplate(headerTemplate) .then(function (contents) { var template = angular.element(contents); var newElm = $compile(template)($scope); $elm.replaceWith(newElm); // And update $elm to be the new element $elm = newElm; updateHeaderReferences(); if (containerCtrl) { // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0]; if (headerViewport) { containerCtrl.headerViewport = headerViewport; angular.element(headerViewport).on('scroll', scrollHandler); $scope.$on('$destroy', function () { angular.element(headerViewport).off('scroll', scrollHandler); }); } } $scope.grid.queueRefresh(); }); function updateHeaderReferences() { containerCtrl.header = containerCtrl.colContainer.header = $elm; var headerCanvases = $elm[0].getElementsByClassName('ui-grid-header-canvas'); if (headerCanvases.length > 0) { containerCtrl.headerCanvas = containerCtrl.colContainer.headerCanvas = headerCanvases[0]; } else { containerCtrl.headerCanvas = null; } } function scrollHandler(evt) { if (uiGridCtrl.grid.isScrollingHorizontally) { return; } var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid); var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft); var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll); scrollEvent.newScrollLeft = newScrollLeft; if ( horizScrollPercentage > -1 ){ scrollEvent.x = { percentage: horizScrollPercentage }; } uiGridCtrl.grid.scrollContainers(null, scrollEvent); } }, post: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; // gridUtil.logDebug('ui-grid-header link'); var grid = uiGridCtrl.grid; // Don't animate header cells gridUtil.disableAnimations($elm); function updateColumnWidths() { // this styleBuilder always runs after the renderContainer, so we can rely on the column widths // already being populated correctly var columnCache = containerCtrl.colContainer.visibleColumnCache; // Build the CSS // uiGridCtrl.grid.columns.forEach(function (column) { var ret = ''; var canvasWidth = 0; columnCache.forEach(function (column) { ret = ret + column.getColClassDefinition(); canvasWidth += column.drawnWidth; }); containerCtrl.colContainer.canvasWidth = canvasWidth; // Return the styles back to buildStyles which pops them into the `customStyles` scope variable return ret; } containerCtrl.header = $elm; var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0]; if (headerViewport) { containerCtrl.headerViewport = headerViewport; } //todo: remove this if by injecting gridCtrl into unit tests if (uiGridCtrl) { uiGridCtrl.grid.registerStyleComputation({ priority: 15, func: updateColumnWidths }); } } }; } }; }]); })(); (function(){ angular.module('ui.grid') .service('uiGridGridMenuService', [ 'gridUtil', 'i18nService', 'uiGridConstants', function( gridUtil, i18nService, uiGridConstants ) { /** * @ngdoc service * @name ui.grid.gridMenuService * * @description Methods for working with the grid menu */ var service = { /** * @ngdoc method * @methodOf ui.grid.gridMenuService * @name initialize * @description Sets up the gridMenu. Most importantly, sets our * scope onto the grid object as grid.gridMenuScope, allowing us * to operate when passed only the grid. Second most importantly, * we register the 'addToGridMenu' and 'removeFromGridMenu' methods * on the core api. * @param {$scope} $scope the scope of this gridMenu * @param {Grid} grid the grid to which this gridMenu is associated */ initialize: function( $scope, grid ){ grid.gridMenuScope = $scope; $scope.grid = grid; $scope.registeredMenuItems = []; // not certain this is needed, but would be bad to create a memory leak $scope.$on('$destroy', function() { if ( $scope.grid && $scope.grid.gridMenuScope ){ $scope.grid.gridMenuScope = null; } if ( $scope.grid ){ $scope.grid = null; } if ( $scope.registeredMenuItems ){ $scope.registeredMenuItems = null; } }); $scope.registeredMenuItems = []; /** * @ngdoc function * @name addToGridMenu * @methodOf ui.grid.core.api:PublicApi * @description add items to the grid menu. Used by features * to add their menu items if they are enabled, can also be used by * end users to add menu items. This method has the advantage of allowing * remove again, which can simplify management of which items are included * in the menu when. (Noting that in most cases the shown and active functions * provide a better way to handle visibility of menu items) * @param {Grid} grid the grid on which we are acting * @param {array} items menu items in the format as described in the tutorial, with * the added note that if you want to use remove you must also specify an `id` field, * which is provided when you want to remove an item. The id should be unique. * */ grid.api.registerMethod( 'core', 'addToGridMenu', service.addToGridMenu ); /** * @ngdoc function * @name removeFromGridMenu * @methodOf ui.grid.core.api:PublicApi * @description Remove an item from the grid menu based on a provided id. Assumes * that the id is unique, removes only the last instance of that id. Does nothing if * the specified id is not found * @param {Grid} grid the grid on which we are acting * @param {string} id the id we'd like to remove from the menu * */ grid.api.registerMethod( 'core', 'removeFromGridMenu', service.removeFromGridMenu ); }, /** * @ngdoc function * @name addToGridMenu * @propertyOf ui.grid.gridMenuService * @description add items to the grid menu. Used by features * to add their menu items if they are enabled, can also be used by * end users to add menu items. This method has the advantage of allowing * remove again, which can simplify management of which items are included * in the menu when. (Noting that in most cases the shown and active functions * provide a better way to handle visibility of menu items) * @param {Grid} grid the grid on which we are acting * @param {array} items menu items in the format as described in the tutorial, with * the added note that if you want to use remove you must also specify an `id` field, * which is provided when you want to remove an item. The id should be unique. * */ addToGridMenu: function( grid, menuItems ) { if ( !angular.isArray( menuItems ) ) { gridUtil.logError( 'addToGridMenu: menuItems must be an array, and is not, not adding any items'); } else { if ( grid.gridMenuScope ){ grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems ? grid.gridMenuScope.registeredMenuItems : []; grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems.concat( menuItems ); } else { gridUtil.logError( 'Asked to addToGridMenu, but gridMenuScope not present. Timing issue? Please log issue with ui-grid'); } } }, /** * @ngdoc function * @name removeFromGridMenu * @methodOf ui.grid.gridMenuService * @description Remove an item from the grid menu based on a provided id. Assumes * that the id is unique, removes only the last instance of that id. Does nothing if * the specified id is not found. If there is no gridMenuScope or registeredMenuItems * then do nothing silently - the desired result is those menu items not be present and they * aren't. * @param {Grid} grid the grid on which we are acting * @param {string} id the id we'd like to remove from the menu * */ removeFromGridMenu: function( grid, id ){ var foundIndex = -1; if ( grid && grid.gridMenuScope ){ grid.gridMenuScope.registeredMenuItems.forEach( function( value, index ) { if ( value.id === id ){ if (foundIndex > -1) { gridUtil.logError( 'removeFromGridMenu: found multiple items with the same id, removing only the last' ); } else { foundIndex = index; } } }); } if ( foundIndex > -1 ){ grid.gridMenuScope.registeredMenuItems.splice( foundIndex, 1 ); } }, /** * @ngdoc array * @name gridMenuCustomItems * @propertyOf ui.grid.class:GridOptions * @description (optional) An array of menu items that should be added to * the gridMenu. Follow the format documented in the tutorial for column * menu customisation. The context provided to the action function will * include context.grid. An alternative if working with dynamic menus is to use the * provided api - core.addToGridMenu and core.removeFromGridMenu, which handles * some of the management of items for you. * */ /** * @ngdoc boolean * @name gridMenuShowHideColumns * @propertyOf ui.grid.class:GridOptions * @description true by default, whether the grid menu should allow hide/show * of columns * */ /** * @ngdoc method * @methodOf ui.grid.gridMenuService * @name getMenuItems * @description Decides the menu items to show in the menu. This is a * combination of: * * - the default menu items that are always included, * - any menu items that have been provided through the addMenuItem api. These * are typically added by features within the grid * - any menu items included in grid.options.gridMenuCustomItems. These can be * changed dynamically, as they're always recalculated whenever we show the * menu * @param {$scope} $scope the scope of this gridMenu, from which we can find all * the information that we need * @returns {array} an array of menu items that can be shown */ getMenuItems: function( $scope ) { var menuItems = [ // this is where we add any menu items we want to always include ]; if ( $scope.grid.options.gridMenuCustomItems ){ if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){ gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not'); } else { menuItems = menuItems.concat( $scope.grid.options.gridMenuCustomItems ); } } var clearFilters = [{ title: i18nService.getSafeText('gridMenu.clearAllFilters'), action: function ($event) { $scope.grid.clearAllFilters(); }, shown: function() { return $scope.grid.options.enableFiltering; }, order: 100 }]; menuItems = menuItems.concat( clearFilters ); menuItems = menuItems.concat( $scope.registeredMenuItems ); if ( $scope.grid.options.gridMenuShowHideColumns !== false ){ menuItems = menuItems.concat( service.showHideColumns( $scope ) ); } menuItems.sort(function(a, b){ return a.order - b.order; }); return menuItems; }, /** * @ngdoc array * @name gridMenuTitleFilter * @propertyOf ui.grid.class:GridOptions * @description (optional) A function that takes a title string * (usually the col.displayName), and converts it into a display value. The function * must return either a string or a promise. * * Used for internationalization of the grid menu column names - for angular-translate * you can pass $translate as the function, for i18nService you can pass getSafeText as the * function * @example *
* gridOptions = { * gridMenuTitleFilter: $translate * } **/ /** * @ngdoc method * @methodOf ui.grid.gridMenuService * @name showHideColumns * @description Adds two menu items for each of the columns in columnDefs. One * menu item for hide, one menu item for show. Each is visible when appropriate * (show when column is not visible, hide when column is visible). Each toggles * the visible property on the columnDef using toggleColumnVisibility * @param {$scope} $scope of a gridMenu, which contains a reference to the grid */ showHideColumns: function( $scope ){ var showHideColumns = []; if ( !$scope.grid.options.columnDefs || $scope.grid.options.columnDefs.length === 0 || $scope.grid.columns.length === 0 ) { return showHideColumns; } // add header for columns showHideColumns.push({ title: i18nService.getSafeText('gridMenu.columns'), order: 300 }); $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; }; $scope.grid.options.columnDefs.forEach( function( colDef, index ){ if ( colDef.enableHiding !== false ){ // add hide menu item - shows an OK icon as we only show when column is already visible var menuItem = { icon: 'ui-grid-icon-ok', action: function($event) { $event.stopPropagation(); service.toggleColumnVisibility( this.context.gridCol ); }, shown: function() { return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined; }, context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, leaveOpen: true, order: 301 + index * 2 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); // add show menu item - shows no icon as we only show when column is invisible menuItem = { icon: 'ui-grid-icon-cancel', action: function($event) { $event.stopPropagation(); service.toggleColumnVisibility( this.context.gridCol ); }, shown: function() { return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined); }, context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, leaveOpen: true, order: 301 + index * 2 + 1 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); } }); return showHideColumns; }, /** * @ngdoc method * @methodOf ui.grid.gridMenuService * @name setMenuItemTitle * @description Handles the response from gridMenuTitleFilter, adding it directly to the menu * item if it returns a string, otherwise waiting for the promise to resolve or reject then * putting the result into the title * @param {object} menuItem the menuItem we want to put the title on * @param {object} colDef the colDef from which we can get displayName, name or field * @param {Grid} grid the grid, from which we can get the options.gridMenuTitleFilter * */ setMenuItemTitle: function( menuItem, colDef, grid ){ var title = grid.options.gridMenuTitleFilter( colDef.displayName || gridUtil.readableColumnName(colDef.name) || colDef.field ); if ( typeof(title) === 'string' ){ menuItem.title = title; } else if ( title.then ){ // must be a promise menuItem.title = ""; title.then( function( successValue ) { menuItem.title = successValue; }, function( errorValue ) { menuItem.title = errorValue; }); } else { gridUtil.logError('Expected gridMenuTitleFilter to return a string or a promise, it has returned neither, bad config'); menuItem.title = 'badconfig'; } }, /** * @ngdoc method * @methodOf ui.grid.gridMenuService * @name toggleColumnVisibility * @description Toggles the visibility of an individual column. Expects to be * provided a context that has on it a gridColumn, which is the column that * we'll operate upon. We change the visibility, and refresh the grid as appropriate * @param {GridCol} gridCol the column that we want to toggle * */ toggleColumnVisibility: function( gridCol ) { gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined ); gridCol.grid.refresh(); gridCol.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); gridCol.grid.api.core.raise.columnVisibilityChanged( gridCol ); } }; return service; }]) .directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 'i18nService', function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) { return { priority: 0, scope: true, require: ['^uiGrid'], templateUrl: 'ui-grid/ui-grid-menu-button', replace: true, link: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0]; // For the aria label $scope.i18n = { aria: i18nService.getSafeText('gridMenu.aria') }; uiGridGridMenuService.initialize($scope, uiGridCtrl.grid); $scope.shown = false; $scope.toggleMenu = function () { if ( $scope.shown ){ $scope.$broadcast('hide-menu'); $scope.shown = false; } else { $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope ); $scope.$broadcast('show-menu'); $scope.shown = true; } }; $scope.$on('menu-hidden', function() { $scope.shown = false; gridUtil.focus.bySelector($elm, '.ui-grid-icon-container'); }); } }; }]); })(); (function(){ /** * @ngdoc directive * @name ui.grid.directive:uiGridMenu * @element style * @restrict A * * @description * Allows us to interpolate expressions in ` I am in a box.
* $scope.gridOptions = { * customScroller: function myScrolling(uiGridViewport, scrollHandler) { * uiGridViewport.on('scroll', function myScrollingOverride(event) { * // Do something here * * scrollHandler(event); * }); * } * }; ** @param {object} uiGridViewport Element being scrolled. (this gets passed in by the grid). * @param {function} scrollHandler Function that needs to be called when scrolling happens. (this gets passed in by the grid). */ if (grid && grid.options && grid.options.customScroller) { grid.options.customScroller($elm, scrollHandler); } else { $elm.on('scroll', scrollHandler); } var ignoreScroll = false; function scrollHandler(evt) { //Leaving in this commented code in case it can someday be used //It does improve performance, but because the horizontal scroll is normalized, // using this code will lead to the column header getting slightly out of line with columns // //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) { // //don't ask for scrollTop if we just set it // ignoreScroll = false; // return; //} //ignoreScroll = true; var newScrollTop = $elm[0].scrollTop; var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid); var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop); var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft); var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll); scrollEvent.newScrollLeft = newScrollLeft; scrollEvent.newScrollTop = newScrollTop; if ( horizScrollPercentage > -1 ){ scrollEvent.x = { percentage: horizScrollPercentage }; } if ( vertScrollPercentage > -1 ){ scrollEvent.y = { percentage: vertScrollPercentage }; } grid.scrollContainers($scope.$parent.containerId, scrollEvent); } if ($scope.$parent.bindScrollVertical) { grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll); } if ($scope.$parent.bindScrollHorizontal) { grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll); grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader); grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter); } function syncVerticalScroll(scrollEvent){ containerCtrl.prevScrollArgs = scrollEvent; var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport); $elm[0].scrollTop = newScrollTop; } function syncHorizontalScroll(scrollEvent){ containerCtrl.prevScrollArgs = scrollEvent; var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } function syncHorizontalHeader(scrollEvent){ var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); if (containerCtrl.headerViewport) { containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } } function syncHorizontalFooter(scrollEvent){ var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); if (containerCtrl.footerViewport) { containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } } $scope.$on('$destroy', function unbindEvents() { $elm.off(); }); }, controller: ['$scope', function ($scope) { this.rowStyle = function (index) { var rowContainer = $scope.rowContainer; var colContainer = $scope.colContainer; var styles = {}; if (rowContainer.currentTopRow !== 0){ //top offset based on hidden rows count var translateY = "translateY("+ (rowContainer.currentTopRow * rowContainer.grid.options.rowHeight) +"px)"; styles['transform'] = translateY; styles['-webkit-transform'] = translateY; styles['-ms-transform'] = translateY; } if (colContainer.currentFirstColumn !== 0) { if (colContainer.grid.isRTL()) { styles['margin-right'] = colContainer.columnOffset + 'px'; } else { styles['margin-left'] = colContainer.columnOffset + 'px'; } } return styles; }; }] }; } ]); })(); (function() { angular.module('ui.grid') .directive('uiGridVisible', function uiGridVisibleAction() { return function ($scope, $elm, $attr) { $scope.$watch($attr.uiGridVisible, function (visible) { // $elm.css('visibility', visible ? 'visible' : 'hidden'); $elm[visible ? 'removeClass' : 'addClass']('ui-grid-invisible'); }); }; }); })(); (function () { 'use strict'; angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants', '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants, $templateCache, gridClassFactory, $timeout, $parse, $compile) { // gridUtil.logDebug('ui-grid controller'); var self = this; self.grid = gridClassFactory.createGrid($scope.uiGrid); //assign $scope.$parent if appScope not already assigned self.grid.appScope = self.grid.appScope || $scope.$parent; $elm.addClass('grid' + self.grid.id); self.grid.rtl = gridUtil.getStyles($elm[0])['direction'] === 'rtl'; // angular.extend(self.grid.options, ); //all properties of grid are available on scope $scope.grid = self.grid; if ($attrs.uiGridColumns) { $attrs.$observe('uiGridColumns', function(value) { self.grid.options.columnDefs = value; self.grid.buildColumns() .then(function(){ self.grid.preCompileCellTemplates(); self.grid.refreshCanvas(true); }); }); } // if fastWatch is set we watch only the length and the reference, not every individual object var deregFunctions = []; if (self.grid.options.fastWatch) { self.uiGrid = $scope.uiGrid; if (angular.isString($scope.uiGrid.data)) { deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) ); deregFunctions.push( $scope.$parent.$watch(function() { if ( self.grid.appScope[$scope.uiGrid.data] ){ return self.grid.appScope[$scope.uiGrid.data].length; } else { return undefined; } }, dataWatchFunction) ); } else { deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) ); deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, function(){ dataWatchFunction($scope.uiGrid.data); }) ); } deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) ); deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, function(){ columnDefsWatchFunction($scope.uiGrid.columnDefs); }) ); } else { if (angular.isString($scope.uiGrid.data)) { deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) ); } else { deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) ); } deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) ); } function columnDefsWatchFunction(n, o) { if (n && n !== o) { self.grid.options.columnDefs = $scope.uiGrid.columnDefs; self.grid.buildColumns({ orderByColumnDefs: true }) .then(function(){ self.grid.preCompileCellTemplates(); self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.COLUMN); }); } } var mostRecentData; function dataWatchFunction(newData) { // gridUtil.logDebug('dataWatch fired'); var promises = []; if ( self.grid.options.fastWatch ){ if (angular.isString($scope.uiGrid.data)) { newData = self.grid.appScope[$scope.uiGrid.data]; } else { newData = $scope.uiGrid.data; } } mostRecentData = newData; if (newData) { // columns length is greater than the number of row header columns, which don't count because they're created automatically var hasColumns = self.grid.columns.length > (self.grid.rowHeaderColumns ? self.grid.rowHeaderColumns.length : 0); if ( // If we have no columns !hasColumns && // ... and we don't have a ui-grid-columns attribute, which would define columns for us !$attrs.uiGridColumns && // ... and we have no pre-defined columns self.grid.options.columnDefs.length === 0 && // ... but we DO have data newData.length > 0 ) { // ... then build the column definitions from the data that we have self.grid.buildColumnDefsFromData(newData); } // If we haven't built columns before and either have some columns defined or some data defined if (!hasColumns && (self.grid.options.columnDefs.length > 0 || newData.length > 0)) { // Build the column set, then pre-compile the column cell templates promises.push(self.grid.buildColumns() .then(function() { self.grid.preCompileCellTemplates(); })); } $q.all(promises).then(function() { // use most recent data, rather than the potentially outdated data passed into watcher handler self.grid.modifyRows(mostRecentData) .then(function () { // if (self.viewport) { self.grid.redrawInPlace(true); // } $scope.$evalAsync(function() { self.grid.refreshCanvas(true); self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW); }); }); }); } } var styleWatchDereg = $scope.$watch(function () { return self.grid.styleComputations; }, function() { self.grid.refreshCanvas(true); }); $scope.$on('$destroy', function() { deregFunctions.forEach( function( deregFn ){ deregFn(); }); styleWatchDereg(); }); self.fireEvent = function(eventName, args) { // Add the grid to the event arguments if it's not there if (typeof(args) === 'undefined' || args === undefined) { args = {}; } if (typeof(args.grid) === 'undefined' || args.grid === undefined) { args.grid = self.grid; } $scope.$broadcast(eventName, args); }; self.innerCompile = function innerCompile(elm) { $compile(elm)($scope); }; }]); /** * @ngdoc directive * @name ui.grid.directive:uiGrid * @element div * @restrict EA * @param {Object} uiGrid Options for the grid to use * * @description Create a very basic grid. * * @example
* mySortFn = function(a, b) { * var nulls = $scope.gridApi.core.sortHandleNulls(a, b); * if ( nulls !== null ){ * return nulls; * } else { * // your code for sorting here * }; ** @param {object} a sort value a * @param {object} b sort value b * @returns {number} null if there were no nulls/undefineds, otherwise returns * a sort value that should be passed back from the sort function * */ self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls ); /** * @ngdoc function * @name sortChanged * @methodOf ui.grid.core.api:PublicApi * @description The sort criteria on one or more columns has * changed. Provides as parameters the grid and the output of * getColumnSorting, which is an array of gridColumns * that have sorting on them, sorted in priority order. * * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed. * @param {Function} callBack Will be called when the event is emited. The function passes back the grid and an array of * columns with sorts on them, in priority order. * * @example *
* gridApi.core.on.sortChanged( $scope, function(grid, sortColumns){ * // do something * }); **/ self.api.registerEvent( 'core', 'sortChanged' ); /** * @ngdoc function * @name columnVisibilityChanged * @methodOf ui.grid.core.api:PublicApi * @description The visibility of a column has changed, * the column itself is passed out as a parameter of the event. * * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed. * @param {Function} callBack Will be called when the event is emited. The function passes back the GridCol that has changed. * * @example *
* gridApi.core.on.columnVisibilityChanged( $scope, function (column) { * // do something * } ); **/ self.api.registerEvent( 'core', 'columnVisibilityChanged' ); /** * @ngdoc method * @name notifyDataChange * @methodOf ui.grid.core.api:PublicApi * @description Notify the grid that a data or config change has occurred, * where that change isn't something the grid was otherwise noticing. This * might be particularly relevant where you've changed values within the data * and you'd like cell classes to be re-evaluated, or changed config within * the columnDef and you'd like headerCellClasses to be re-evaluated. * @param {string} type one of the * {@link ui.grid.service:uiGridConstants#properties_dataChange uiGridConstants.dataChange} * values (ALL, ROW, EDIT, COLUMN), which tells us which refreshes to fire. * */ self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange ); /** * @ngdoc method * @name clearAllFilters * @methodOf ui.grid.core.api:PublicApi * @description Clears all filters and optionally refreshes the visible rows. * @param {object} refreshRows Defaults to true. * @param {object} clearConditions Defaults to false. * @param {object} clearFlags Defaults to false. * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing. */ self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters); self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]); self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]); self.registerDataChangeCallback( self.updateFooterHeightCallback, [uiGridConstants.dataChange.OPTIONS]); self.registerStyleComputation({ priority: 10, func: self.getFooterStyles }); }; Grid.prototype.calcFooterHeight = function () { if (!this.hasFooter()) { return 0; } var height = 0; if (this.options.showGridFooter) { height += this.options.gridFooterHeight; } height += this.calcColumnFooterHeight(); return height; }; Grid.prototype.calcColumnFooterHeight = function () { var height = 0; if (this.options.showColumnFooter) { height += this.options.columnFooterHeight; } return height; }; Grid.prototype.getFooterStyles = function () { var style = '.grid' + this.id + ' .ui-grid-footer-aggregates-row { height: ' + this.options.columnFooterHeight + 'px; }'; style += ' .grid' + this.id + ' .ui-grid-footer-info { height: ' + this.options.gridFooterHeight + 'px; }'; return style; }; Grid.prototype.hasFooter = function () { return this.options.showGridFooter || this.options.showColumnFooter; }; /** * @ngdoc function * @name isRTL * @methodOf ui.grid.class:Grid * @description Returns true if grid is RightToLeft */ Grid.prototype.isRTL = function () { return this.rtl; }; /** * @ngdoc function * @name registerColumnBuilder * @methodOf ui.grid.class:Grid * @description When the build creates columns from column definitions, the columnbuilders will be called to add * additional properties to the column. * @param {function(colDef, col, gridOptions)} columnBuilder function to be called */ Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) { this.columnBuilders.push(columnBuilder); }; /** * @ngdoc function * @name buildColumnDefsFromData * @methodOf ui.grid.class:Grid * @description Populates columnDefs from the provided data * @param {function(colDef, col, gridOptions)} rowBuilder function to be called */ Grid.prototype.buildColumnDefsFromData = function (dataRows){ this.options.columnDefs = gridUtil.getColumnsFromData(dataRows, this.options.excludeProperties); }; /** * @ngdoc function * @name registerRowBuilder * @methodOf ui.grid.class:Grid * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add * additional properties to the row. * @param {function(row, gridOptions)} rowBuilder function to be called */ Grid.prototype.registerRowBuilder = function registerRowBuilder(rowBuilder) { this.rowBuilders.push(rowBuilder); }; /** * @ngdoc function * @name registerDataChangeCallback * @methodOf ui.grid.class:Grid * @description When a data change occurs, the data change callbacks of the specified type * will be called. The rules are: * * - when the data watch fires, that is considered a ROW change (the data watch only notices * added or removed rows) * - when the api is called to inform us of a change, the declared type of that change is used * - when a cell edit completes, the EDIT callbacks are triggered * - when the columnDef watch fires, the COLUMN callbacks are triggered * - when the options watch fires, the OPTIONS callbacks are triggered * * For a given event: * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks * - ROW calls ROW and ALL callbacks * - EDIT calls EDIT and ALL callbacks * - COLUMN calls COLUMN and ALL callbacks * - OPTIONS calls OPTIONS and ALL callbacks * * @param {function(grid)} callback function to be called * @param {array} types the types of data change you want to be informed of. Values from * the {@link ui.grid.service:uiGridConstants#properties_dataChange uiGridConstants.dataChange} * values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to ALL * @returns {function} deregister function - a function that can be called to deregister this callback */ Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) { var uid = gridUtil.nextUid(); if ( !types ){ types = [uiGridConstants.dataChange.ALL]; } if ( !Array.isArray(types)){ gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types ); } this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this }; var self = this; var deregisterFunction = function() { delete self.dataChangeCallbacks[uid]; }; return deregisterFunction; }; /** * @ngdoc function * @name callDataChangeCallbacks * @methodOf ui.grid.class:Grid * @description Calls the callbacks based on the type of data change that * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the * event type is matching, or if the type is ALL. * @param {string} type the type of event that occurred - one of the * {@link ui.grid.service:uiGridConstants#properties_dataChange uiGridConstants.dataChange} * values (ALL, ROW, EDIT, COLUMN, OPTIONS) */ Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) { angular.forEach( this.dataChangeCallbacks, function( callback, uid ){ if ( callback.types.indexOf( uiGridConstants.dataChange.ALL ) !== -1 || callback.types.indexOf( type ) !== -1 || type === uiGridConstants.dataChange.ALL ) { if (callback._this) { callback.callback.apply(callback._this,this); } else { callback.callback( this ); } } }, this); }; /** * @ngdoc function * @name notifyDataChange * @methodOf ui.grid.class:Grid * @description Notifies us that a data change has occurred, used in the public * api for users to tell us when they've changed data or some other event that * our watches cannot pick up * @param {string} type the type of event that occurred - one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN) */ Grid.prototype.notifyDataChange = function notifyDataChange(type) { var constants = uiGridConstants.dataChange; if ( type === constants.ALL || type === constants.COLUMN || type === constants.EDIT || type === constants.ROW || type === constants.OPTIONS ){ this.callDataChangeCallbacks( type ); } else { gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type); } }; /** * @ngdoc function * @name columnRefreshCallback * @methodOf ui.grid.class:Grid * @description refreshes the grid when a column refresh * is notified, which triggers handling of the visible flag. * This is called on uiGridConstants.dataChange.COLUMN, and is * registered as a dataChangeCallback in grid.js * @param {string} name column name */ Grid.prototype.columnRefreshCallback = function columnRefreshCallback( grid ){ grid.buildColumns(); grid.queueGridRefresh(); }; /** * @ngdoc function * @name processRowsCallback * @methodOf ui.grid.class:Grid * @description calls the row processors, specifically * intended to reset the sorting when an edit is called, * registered as a dataChangeCallback on uiGridConstants.dataChange.EDIT * @param {string} name column name */ Grid.prototype.processRowsCallback = function processRowsCallback( grid ){ grid.queueGridRefresh(); }; /** * @ngdoc function * @name updateFooterHeightCallback * @methodOf ui.grid.class:Grid * @description recalculates the footer height, * registered as a dataChangeCallback on uiGridConstants.dataChange.OPTIONS * @param {string} name column name */ Grid.prototype.updateFooterHeightCallback = function updateFooterHeightCallback( grid ){ grid.footerHeight = grid.calcFooterHeight(); grid.columnFooterHeight = grid.calcColumnFooterHeight(); }; /** * @ngdoc function * @name getColumn * @methodOf ui.grid.class:Grid * @description returns a grid column for the column name * @param {string} name column name */ Grid.prototype.getColumn = function getColumn(name) { var columns = this.columns.filter(function (column) { return column.colDef.name === name; }); return columns.length > 0 ? columns[0] : null; }; /** * @ngdoc function * @name getColDef * @methodOf ui.grid.class:Grid * @description returns a grid colDef for the column name * @param {string} name column.field */ Grid.prototype.getColDef = function getColDef(name) { var colDefs = this.options.columnDefs.filter(function (colDef) { return colDef.name === name; }); return colDefs.length > 0 ? colDefs[0] : null; }; /** * @ngdoc function * @name assignTypes * @methodOf ui.grid.class:Grid * @description uses the first row of data to assign colDef.type for any types not defined. */ /** * @ngdoc property * @name type * @propertyOf ui.grid.class:GridOptions.columnDef * @description the type of the column, used in sorting. If not provided then the * grid will guess the type. Add this only if the grid guessing is not to your * satisfaction. One of: * - 'string' * - 'boolean' * - 'number' * - 'date' * - 'object' * - 'numberStr' * Note that if you choose date, your dates should be in a javascript date type * */ Grid.prototype.assignTypes = function(){ var self = this; self.options.columnDefs.forEach(function (colDef, index) { //Assign colDef type if not specified if (!colDef.type) { var col = new GridColumn(colDef, index, self); var firstRow = self.rows.length > 0 ? self.rows[0] : null; if (firstRow) { colDef.type = gridUtil.guessType(self.getCellValue(firstRow, col)); } else { colDef.type = 'string'; } } }); }; /** * @ngdoc function * @name isRowHeaderColumn * @methodOf ui.grid.class:Grid * @description returns true if the column is a row Header * @param {object} column column */ Grid.prototype.isRowHeaderColumn = function isRowHeaderColumn(column) { return this.rowHeaderColumns.indexOf(column) !== -1; }; /** * @ngdoc function * @name addRowHeaderColumn * @methodOf ui.grid.class:Grid * @description adds a row header column to the grid * @param {object} column def */ Grid.prototype.addRowHeaderColumn = function addRowHeaderColumn(colDef, order) { var self = this; //default order if (order === undefined) { order = 0; } var rowHeaderCol = new GridColumn(colDef, gridUtil.nextUid(), self); rowHeaderCol.isRowHeader = true; if (self.isRTL()) { self.createRightContainer(); rowHeaderCol.renderContainer = 'right'; } else { self.createLeftContainer(); rowHeaderCol.renderContainer = 'left'; } // relies on the default column builder being first in array, as it is instantiated // as part of grid creation self.columnBuilders[0](colDef,rowHeaderCol,self.options) .then(function(){ rowHeaderCol.enableFiltering = false; rowHeaderCol.enableSorting = false; rowHeaderCol.enableHiding = false; rowHeaderCol.headerPriority = order; self.rowHeaderColumns.push(rowHeaderCol); self.rowHeaderColumns = self.rowHeaderColumns.sort(function (a, b) { return a.headerPriority - b.headerPriority; }); self.buildColumns() .then( function() { self.preCompileCellTemplates(); self.queueGridRefresh(); }); }); }; /** * @ngdoc function * @name getOnlyDataColumns * @methodOf ui.grid.class:Grid * @description returns all columns except for rowHeader columns */ Grid.prototype.getOnlyDataColumns = function getOnlyDataColumns() { var self = this; var cols = []; self.columns.forEach(function (col) { if (self.rowHeaderColumns.indexOf(col) === -1) { cols.push(col); } }); return cols; }; /** * @ngdoc function * @name buildColumns * @methodOf ui.grid.class:Grid * @description creates GridColumn objects from the columnDefinition. Calls each registered * columnBuilder to further process the column * @param {object} options An object contains options to use when building columns * * * **orderByColumnDefs**: defaults to **false**. When true, `buildColumns` will reorder existing columns according to the order within the column definitions. * * @returns {Promise} a promise to load any needed column resources */ Grid.prototype.buildColumns = function buildColumns(opts) { var options = { orderByColumnDefs: false }; angular.extend(options, opts); // gridUtil.logDebug('buildColumns'); var self = this; var builderPromises = []; var headerOffset = self.rowHeaderColumns.length; var i; // Remove any columns for which a columnDef cannot be found // Deliberately don't use forEach, as it doesn't like splice being called in the middle // Also don't cache columns.length, as it will change during this operation for (i = 0; i < self.columns.length; i++){ if (!self.getColDef(self.columns[i].name)) { self.columns.splice(i, 1); i--; } } //add row header columns to the grid columns array _after_ columns without columnDefs have been removed //rowHeaderColumns is ordered by priority so insert in reverse for (var j = self.rowHeaderColumns.length - 1; j >= 0; j--) { self.columns.unshift(self.rowHeaderColumns[j]); } // look at each column def, and update column properties to match. If the column def // doesn't have a column, then splice in a new gridCol self.options.columnDefs.forEach(function (colDef, index) { self.preprocessColDef(colDef); var col = self.getColumn(colDef.name); if (!col) { col = new GridColumn(colDef, gridUtil.nextUid(), self); self.columns.splice(index + headerOffset, 0, col); } else { // tell updateColumnDef that the column was pre-existing col.updateColumnDef(colDef, false); } self.columnBuilders.forEach(function (builder) { builderPromises.push(builder.call(self, colDef, col, self.options)); }); }); /*** Reorder columns if necessary ***/ if (!!options.orderByColumnDefs) { // Create a shallow copy of the columns as a cache var columnCache = self.columns.slice(0); // We need to allow for the "row headers" when mapping from the column defs array to the columns array // If we have a row header in columns[0] and don't account for it we'll overwrite it with the column in columnDefs[0] // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result var len = Math.min(self.options.columnDefs.length, self.columns.length); for (i = 0; i < len; i++) { // If the column at this index has a different name than the column at the same index in the column defs... if (self.columns[i + headerOffset].name !== self.options.columnDefs[i].name) { // Replace the one in the cache with the appropriate column columnCache[i + headerOffset] = self.getColumn(self.options.columnDefs[i].name); } else { // Otherwise just copy over the one from the initial columns columnCache[i + headerOffset] = self.columns[i + headerOffset]; } } // Empty out the columns array, non-destructively self.columns.length = 0; // And splice in the updated, ordered columns from the cache Array.prototype.splice.apply(self.columns, [0, 0].concat(columnCache)); } return $q.all(builderPromises).then(function(){ if (self.rows.length > 0){ self.assignTypes(); } }); }; Grid.prototype.preCompileCellTemplate = function(col) { var self = this; var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col)); html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)'); var compiledElementFn = $compile(html); col.compiledElementFn = compiledElementFn; if (col.compiledElementFnDefer) { col.compiledElementFnDefer.resolve(col.compiledElementFn); } }; /** * @ngdoc function * @name preCompileCellTemplates * @methodOf ui.grid.class:Grid * @description pronapiles all cell templates */ Grid.prototype.preCompileCellTemplates = function() { var self = this; self.columns.forEach(function (col) { if ( col.cellTemplate ){ self.preCompileCellTemplate( col ); } else if ( col.cellTemplatePromise ){ col.cellTemplatePromise.then( function() { self.preCompileCellTemplate( col ); }); } }); }; /** * @ngdoc function * @name getGridQualifiedColField * @methodOf ui.grid.class:Grid * @description Returns the $parse-able accessor for a column within its $scope * @param {GridColumn} col col object */ Grid.prototype.getQualifiedColField = function (col) { var base = 'row.entity'; if ( col.field === uiGridConstants.ENTITY_BINDING ) { return base; } return gridUtil.preEval(base + '.' + col.field); }; /** * @ngdoc function * @name createLeftContainer * @methodOf ui.grid.class:Grid * @description creates the left render container if it doesn't already exist */ Grid.prototype.createLeftContainer = function() { if (!this.hasLeftContainer()) { this.renderContainers.left = new GridRenderContainer('left', this, { disableColumnOffset: true }); } }; /** * @ngdoc function * @name createRightContainer * @methodOf ui.grid.class:Grid * @description creates the right render container if it doesn't already exist */ Grid.prototype.createRightContainer = function() { if (!this.hasRightContainer()) { this.renderContainers.right = new GridRenderContainer('right', this, { disableColumnOffset: true }); } }; /** * @ngdoc function * @name hasLeftContainer * @methodOf ui.grid.class:Grid * @description returns true if leftContainer exists */ Grid.prototype.hasLeftContainer = function() { return this.renderContainers.left !== undefined; }; /** * @ngdoc function * @name hasRightContainer * @methodOf ui.grid.class:Grid * @description returns true if rightContainer exists */ Grid.prototype.hasRightContainer = function() { return this.renderContainers.right !== undefined; }; /** * undocumented function * @name preprocessColDef * @methodOf ui.grid.class:Grid * @description defaults the name property from field to maintain backwards compatibility with 2.x * validates that name or field is present */ Grid.prototype.preprocessColDef = function preprocessColDef(colDef) { var self = this; if (!colDef.field && !colDef.name) { throw new Error('colDef.name or colDef.field property is required'); } //maintain backwards compatibility with 2.x //field was required in 2.x. now name is required if (colDef.name === undefined && colDef.field !== undefined) { // See if the column name already exists: var newName = colDef.field, counter = 2; while (self.getColumn(newName)) { newName = colDef.field + counter.toString(); counter++; } colDef.name = newName; } }; // 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 Grid.prototype.newInN = function newInN(o, n, oAccessor, nAccessor) { var self = this; var t = []; for (var i = 0; i < n.length; i++) { var nV = nAccessor ? n[i][nAccessor] : n[i]; var found = false; for (var j = 0; j < o.length; j++) { var oV = oAccessor ? o[j][oAccessor] : o[j]; if (self.options.rowEquality(nV, oV)) { found = true; break; } } if (!found) { t.push(nV); } } return t; }; /** * @ngdoc function * @name getRow * @methodOf ui.grid.class:Grid * @description returns the GridRow that contains the rowEntity * @param {object} rowEntity the gridOptions.data array element instance * @param {array} lookInRows [optional] the rows to look in - if not provided then * looks in grid.rows */ Grid.prototype.getRow = function getRow(rowEntity, lookInRows) { var self = this; lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows; var rows = lookInRows.filter(function (row) { return self.options.rowEquality(row.entity, rowEntity); }); return rows.length > 0 ? rows[0] : null; }; /** * @ngdoc function * @name modifyRows * @methodOf ui.grid.class:Grid * @description creates or removes GridRow objects from the newRawData array. Calls each registered * rowBuilder to further process the row * @param {array} newRawData Modified set of data * * This method aims to achieve three things: * 1. the resulting rows array is in the same order as the newRawData, we'll call * rowsProcessors immediately after to sort the data anyway * 2. if we have row hashing available, we try to use the rowHash to find the row * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected * * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates * the newRows and newHash * * ``` * newRawData.forEach newEntity * if (hashing enabled) * check oldHash for newEntity * else * look for old row directly in oldRows * if !oldRowFound // must be a new row * create newRow * append to the newRows and add to newHash * run the processors * ``` * * Rows are identified using the hashKey if configured. If not configured, then rows * are identified using the gridOptions.rowEquality function * * This method is useful when trying to select rows immediately after loading data without * using a $timeout/$interval, e.g.: * * $scope.gridOptions.data = someData; * $scope.gridApi.grid.modifyRows($scope.gridOptions.data); * $scope.gridApi.selection.selectRow($scope.gridOptions.data[0]); * * OR to persist row selection after data update (e.g. rows selected, new data loaded, want * originally selected rows to be re-selected)) */ Grid.prototype.modifyRows = function modifyRows(newRawData) { var self = this; var oldRows = self.rows.slice(0); var oldRowHash = self.rowHashMap || self.createRowHashMap(); self.rowHashMap = self.createRowHashMap(); self.rows.length = 0; newRawData.forEach( function( newEntity, i ) { var newRow, oldRow; if ( self.options.enableRowHashing ){ // if hashing is enabled, then this row will be in the hash if we already know about it oldRow = oldRowHash.get( newEntity ); } else { // otherwise, manually search the oldRows to see if we can find this row oldRow = self.getRow(newEntity, oldRows); } // update newRow to have an entity if ( oldRow ) { newRow = oldRow; newRow.entity = newEntity; } // if we didn't find the row, it must be new, so create it if ( !newRow ){ newRow = self.processRowBuilders(new GridRow(newEntity, i, self)); } self.rows.push( newRow ); self.rowHashMap.put( newEntity, newRow ); }); self.assignTypes(); var p1 = $q.when(self.processRowsProcessors(self.rows)) .then(function (renderableRows) { return self.setVisibleRows(renderableRows); }); var p2 = $q.when(self.processColumnsProcessors(self.columns)) .then(function (renderableColumns) { return self.setVisibleColumns(renderableColumns); }); return $q.all([p1, p2]); }; /** * Private Undocumented Method * @name addRows * @methodOf ui.grid.class:Grid * @description adds the newRawData array of rows to the grid and calls all registered * rowBuilders. this keyword will reference the grid */ Grid.prototype.addRows = function addRows(newRawData) { var self = this; var existingRowCount = self.rows.length; for (var i = 0; i < newRawData.length; i++) { var newRow = self.processRowBuilders(new GridRow(newRawData[i], i + existingRowCount, self)); if (self.options.enableRowHashing) { var found = self.rowHashMap.get(newRow.entity); if (found) { found.row = newRow; } } self.rows.push(newRow); } }; /** * @ngdoc function * @name processRowBuilders * @methodOf ui.grid.class:Grid * @description processes all RowBuilders for the gridRow * @param {GridRow} gridRow reference to gridRow * @returns {GridRow} the gridRow with all additional behavior added */ Grid.prototype.processRowBuilders = function processRowBuilders(gridRow) { var self = this; self.rowBuilders.forEach(function (builder) { builder.call(self, gridRow, self.options); }); return gridRow; }; /** * @ngdoc function * @name registerStyleComputation * @methodOf ui.grid.class:Grid * @description registered a styleComputation function * * If the function returns a value it will be appended into the grid's `