UNPKG

angular-ui-grid

Version:

A data grid for Angular

1,336 lines (1,156 loc) 470 kB
/*! * ui-grid - v4.9.1 - 2020-10-26 * Copyright (c) 2020 ; 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` * * * 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.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 * - `uiGridConstants.scrollbars.WHEN_NEEDED` - shows scrollbars in this direction when needed */ scrollbars: { NEVER: 0, ALWAYS: 1, WHEN_NEEDED: 2 } }); })(); angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUtil', 'uiGridConstants', function ($compile, $parse, gridUtil, uiGridConstants) { return { 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 ) { $scope.col.getCompiledElementFn() .then(function (compiledElementFn) { compiledElementFn($scope, function(clonedElement, scope) { $elm.append(clonedElement); }); }).catch(angular.noop); } 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) { var initColClass = $scope.col.getColClass(false), classAdded; $elm.addClass(initColClass); function updateClass( 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 function cellChangeFunction( 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 var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction ); function deregisterFunction() { dataChangeDereg(); rowWatchDereg(); } $scope.$on('$destroy', deregisterFunction); $elm.on('$destroy', deregisterFunction); } }; } }; }]); (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 */ 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 ) { return Boolean( $scope.grid.options.enableSorting && typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.enableSorting); }, /** * @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 Boolean(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 ) { return Boolean($scope.col && $scope.col.suppressRemoveSort); }, /** * @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 ) { return !(typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.colDef && $scope.col.colDef.enableHiding === false ); }, /** * @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: function() {return 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: function() {return 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: function() {return 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: function() {return 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 {GridColumn} 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 {GridColumn} 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'), renderContainerOffset = renderContainerElm.getBoundingClientRect().left - $scope.grid.element[0].getBoundingClientRect().left, containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].scrollLeft; // repositionMenu is now always called after it's visible in the DOM, // allowing us to simply get the width every time the menu is opened var myWidth = gridUtil.elementWidth(menu, true), 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 ) { // 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 + paddingRight; if (left < positionData.offset + myWidth) { left = Math.max(positionData.left - containerScrollLeft + positionData.parentLeft - paddingRight + myWidth, positionData.offset + myWidth); } $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 * */ return { 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 {GridColumn} 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; $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() { var menuItems = angular.element($elm[0].querySelector('.ui-grid-menu-items'))[0]; $elm[0].removeAttribute('style'); if ( $scope.hideThenShow ) { delete $scope.hideThenShow; $scope.$broadcast('show-menu'); $scope.menuShown = true; } else { $scope.hideMenu( true ); if ($scope.col && $scope.col.visible) { // 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) .catch(angular.noop); } } if (menuItems) { menuItems.onkeydown = null; angular.forEach(menuItems.children, function removeHandlers(item) { item.onkeydown = null; }); } }); $scope.$on('menu-shown', function() { $timeout(function() { uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement ); var hasVisibleMenuItems = $scope.menuItems.some(function (menuItem) { return menuItem.shown(); }); // automatically set the focus to the first button element in the now open menu. if (hasVisibleMenuItems) { gridUtil.focus.bySelector($document, '.ui-grid-menu-items .ui-grid-menu-item:not(.ng-hide)', true) .catch(angular.noop); } delete $scope.colElementPosition; delete $scope.columnElement; addKeydownHandlersToMenu(); }); }); /* Column methods */ $scope.sortColumn = function (event, dir) { event.stopPropagation(); $scope.grid.sortColumn($scope.col, dir, true) .then(function () { $scope.grid.refresh(); $scope.hideMenu(); }).catch(angular.noop); }; $scope.unsortColumn = function () { $scope.col.unsort(); $scope.grid.refresh(); $scope.hideMenu(); }; function addKeydownHandlersToMenu() { var menu = angular.element($elm[0].querySelector('.ui-grid-menu-items'))[0], menuItems, visibleMenuItems = []; if (menu) { menu.onkeydown = function closeMenu(event) { if (event.keyCode === uiGridConstants.keymap.ESC) { event.preventDefault(); $scope.hideMenu(); } }; menuItems = menu.querySelectorAll('.ui-grid-menu-item:not(.ng-hide)'); angular.forEach(menuItems, function filterVisibleItems(item) { if (item.offsetParent !== null) { this.push(item); } }, visibleMenuItems); if (visibleMenuItems.length) { if (visibleMenuItems.length === 1) { visibleMenuItems[0].onkeydown = function singleItemHandler(event) { circularFocusHandler(event, true); }; } else { visibleMenuItems[0].onkeydown = function firstItemHandler(event) { circularFocusHandler(event, false, event.shiftKey, visibleMenuItems.length - 1); }; visibleMenuItems[visibleMenuItems.length - 1].onkeydown = function lastItemHandler(event) { circularFocusHandler(event, false, !event.shiftKey, 0); }; } } } function circularFocusHandler(event, isSingleItem, shiftKeyStatus, index) { if (event.keyCode === uiGridConstants.keymap.TAB) { if (isSingleItem) { event.preventDefault(); } else if (shiftKeyStatus) { event.preventDefault(); visibleMenuItems[index].focus(); } } } } // 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(); } }).catch(angular.noop); } 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; }); }] }; }]); })(); (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) { $scope.col.updateFilters = function( filterable ) { $elm.children().remove(); if ( filterable ) { var template = $scope.col.filterHeaderTemplate; if (template === undefined && $scope.col.providedFilterHeaderTemplate !== '') { if ($scope.col.filterHeaderTemplatePromise) { $scope.col.filterHeaderTemplatePromise.then(function () { template = $scope.col.filterHeaderTemplate; $elm.append($compile(template)($scope)); }); } } else { $elm.append($compile(template)($scope)); } } }; $scope.$on( '$destroy', function() { delete $scope.col.filterable; delete $scope.col.updateFilters; }); }, post: function ($scope, $elm) { $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) { return { priority: 0, scope: { col: '=', row: '=', renderIndex: '=' }, replace: true, require: '^uiGrid', compile: function compile() { return { pre: function ($scope, $elm) { var template = $scope.col.footerCellTemplate; if (template === undefined && $scope.col.providedFooterCellTemplate !== '') { if ($scope.col.footerCellTemplatePromise) { $scope.col.footerCellTemplatePromise.then(function () { template = $scope.col.footerCellTemplate; $elm.append($compile(template)($scope)); }); } } else { $elm.append($compile(template)($scope)); } }, 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() { 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(); // 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 ); } }; } }; }]); })(); (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; } } }).catch(angular.noop); }, 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', function($templateCache, $compile, uiGridConstants, gridUtil) { return { restrict: 'EA', replace: true, require: '^uiGrid', scope: true, compile: function() { 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), newElm = $compile(template)($scope); $elm.append(newElm); }).catch(angular.noop); } }; } }; }]); })(); (function() { 'use strict'; angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService', '$rootScope', function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService, $rootScope) { // Do stuff after mouse has been down this many ms on the header cell var mousedownTimeout = 500, changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa return { priority: 0, scope: { col: '=', row: '=', renderIndex: '=' }, require: ['^uiGrid', '^uiGridRenderContainer'], replace: true, compile: function() { return { pre: function ($scope, $elm) { var template = $scope.col.headerCellTemplate; if (template === undefined && $scope.col.providedHeaderCellTemplate !== '') { if ($scope.col.headerCellTemplatePromise) { $scope.col.headerCellTemplatePromise.then(function () { template = $scope.col.headerCellTemplate; $elm.append($compile(template)($scope)); }); } } else { $elm.append($compile(template)($scope)); } }, 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 $scope.col && $scope.col.sort && 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 label = col.sort && col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort && col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none); if ($scope.isSortPriorityVisible()) { label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + (col.sort.priority + 1); } 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 $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') ); // apply any headerCellClass var classAdded, 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); } }).catch(angular.noop); 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.handleKeyDown = function(event) { if (event.keyCode === 32 || event.keyCode === 13) { event.preventDefault(); $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.mov