angular-ui-grid
Version:
A data grid for Angular
1,336 lines (1,156 loc) • 470 kB
JavaScript
/*!
* 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