Source: directives/extended-table.js

'use strict';

/**
 * @name keta.directives.ExtendedTable
 * @author Marco Lehmann <marco.lehmann@kiwigrid.com>
 * @copyright Kiwigrid GmbH 2014-2015
 * @module keta.directives.ExtendedTable
 * @description
 * <p>
 *   A table directive with extended functionality as column sorting, column customizing, paging
 *   and filtering.
 * </p>
 * <p>
 *   Basic principle is the usage of optional parameters and callbacks, which offer a really
 *   flexible API for customizing the table to you very own needs.
 * </p>
 * @example
 * &lt;div data-keta-extended-table
 *     data-rows="rows"
 *     data-current-locale="currentLocale"
 *     data-label-add-column="labelAddColumn"
 *     data-disabled-components="disabledComponents"
 *     data-switchable-columns="switchableColumns"
 *     data-group-by-property="groupByProperty"
 *     data-order-by-property="orderByProperty"
 *     data-visible-columns="visibleColumns"
 *     data-header-label-callback="headerLabelCallback"
 *     data-operations-mode="operationsMode"
 *     data-row-sort-enabled="rowSortEnabled"
 *     data-row-sort-criteria="rowSortCriteria"
 *     data-row-sort-order-ascending="rowSortOrderAscending"
 *     data-unsortable-columns="unsortableColumns"
 *     data-action-list="actionList"
 *     data-cell-renderer="cellRenderer"
 *     data-column-class-callback="columnClassCallback"
 *     data-table-class-callback="tableClassCallback"
 *     data-row-class-callback="rowClassCallback"
 *     data-search-input-width-classes="searchInputWidthClasses"
 *     data-selector-width-classes="selectorWidthClasses"
 *     data-pager="pager"
 *     data-search="search"
 *     data-search-wait-ms="waitTime"
 *     data-search-results="searchResults"&gt;&lt;/div&gt;
 * @example
 * angular.module('exampleApp', ['keta.directives.ExtendedTable', 'keta.services.Device'])
 *     .controller('ExampleController', function(
 *         $scope,
 *         ketaExtendedTableConstants, ketaExtendedTableMessageKeys, ketaDeviceConstants) {
 *
 *         // data as array of objects, keys from first element are taken as headers
 *         $scope.rows = [{
 *             guid: 'guid-1',
 *             idName: 'Device 1',
 *             stateDevice: 'OK',
 *             deviceClass: 'class-1'
 *         }, {
 *             guid: 'guid-2',
 *             idName: 'Device 2',
 *             stateDevice: 'ERROR',
 *             deviceClass: 'class-2'
 *         }, {
 *             guid: 'guid-3',
 *             idName: 'Device 3',
 *             stateDevice: 'FATAL',
 *             deviceClass: 'class-3'
 *         }];
 *
 *         // override default-labels if necessary
 *         // get default labels
 *         $scope.labels = ketaExtendedTableMessageKeys;
 *
 *         // use case 1: overwrite specific key
 *         $scope.labels.de_DE['__keta.directives.ExtendedTable_search'] = 'Finde';
 *
 *         // use case 2: add locale
 *         $scope.labels.fr_FR = {
 *             '__keta.directives.ExtendedTable_search': 'Recherche',
 *             '__keta.directives.ExtendedTable_add_column': 'Ajouter colonne',
 *             '__keta.directives.ExtendedTable_remove_column': 'Retirer la colonne',
 *             '__keta.directives.ExtendedTable_sort': 'Sorte',
 *             '__keta.directives.ExtendedTable_no_entries': 'Pas d’entrées'
 *         };
 *
 *         // array of disabled components (default: everything except the table itself is disabled)
 *         $scope.disabledComponents = [
 *             // the table itself
 *             ketaExtendedTableConstants.COMPONENT.TABLE,
 *             // an input field to search throughout the full dataset
 *             ketaExtendedTableConstants.COMPONENT.FILTER,
 *             // a selector to add columns to table
 *             ketaExtendedTableConstants.COMPONENT.SELECTOR,
 *             // a pager to navigate through paged data
 *             ketaExtendedTableConstants.COMPONENT.PAGER
 *         ];
 *
 *         // array of switchable columns (empty by default)
 *         // together with selector component the given columns can be removed from
 *         // table and added to table afterwards
 *         // to support grouping in select field an array of objects is required (match is compared by id property)
 *         $scope.switchableColumns = [{
 *             id: 'deviceClass'
 *         }];
 *
 *         // property to group selector by
 *         $scope.groupByProperty = 'column.deviceName';
 *
 *         // property to order selector by
 *         $scope.orderByProperty = 'tagName';
 *
 *         // array of visible columns (full by default)
 *         // use this property to filter out columns like primary keys
 *         $scope.visibleColumns = ['idName', 'stateDevice', 'deviceClass'];
 *
 *         // callback method to specify header labels (instead of using auto-generated ones)
 *         $scope.headerLabelCallback = function(column) {
 *             var mappings = {
 *                 idName: 'Name',
 *                 stateDevice: 'State',
 *                 deviceClass: 'Device Class'
 *             };
 *             return (angular.isDefined(mappings[column])) ? mappings[column] : column;
 *         };
 *
 *         // operations mode ("view" for frontend or "data" for backend)
 *         // by defining operations mode as "view" the directive itself manages sorting,
 *         // paging and filtering; if you just pass a pre-sorted, pre-paged and pre-filtered
 *         // dataset by querying a backend, you have to use "data"
 *         $scope.operationsMode = ketaExtendedTableConstants.OPERATIONS_MODE.VIEW;
 *
 *         // boolean flag to enable or disable row sorting in frontend by showing appropriate icons
 *         $scope.rowSortEnabled = true;
 *
 *         // criteria to sort for as string
 *         $scope.rowSortCriteria = 'idName';
 *
 *         // boolean flag to determine if sort order is ascending (true by default)
 *         $scope.rowSortOrderAscending = true;
 *
 *         // array of columns (empty by default) that should not be sorted
 *         $scope.unsortableColumns = ['idName'];
 *
 *         // Array of actions to render for each row.
 *         // getLink method will be used to construct a link with the help of the row object,
 *         // getLabel is used as callback to retrieve value for title-tag,
 *         // icon is used as icon-class for visualizing the action.
 *         // runAction is a callback-function that will be executed when the user clicks on
 *         // the corresponding button. To use this functionality it is necessary to provide the type-parameter
 *         // with the value 'action'.
 *         // type can have the values 'link' (a normal link with href-attribute will be rendered) or
 *         // 'action' (a link with ng-click attribute to execute a callback will be rendered).
 *         // For simplicity the type-property can be left out. In this case the directive renders
 *         // a normal link-tag (same as type 'link').
 *         // display is an optional callback to return condition for displaying action item based on given row
 *         $scope.actionList = [{
 *             getLink: function(row) {
 *                 return 'edit/' + row.guid;
 *             },
 *             getLabel: function() {
 *                 return 'Edit';
 *             },
 *             icon: 'glyphicon glyphicon-pencil',
 *             type: ketaExtendedTableConstants.ACTION_LIST_TYPE.LINK
 *         }, {
 *             runAction: function(row) {
 *                 console.log('action called with ', row);
 *             },
 *             getLabel: function() {
 *                 return 'Remove';
 *             },
 *             icon: 'glyphicon glyphicon-remove'
 *             type: ketaExtendedTableConstants.ACTION_LIST_TYPE.ACTION,
 *             display: function(row) {
 *                 return row.type !== 'EnergyManager';
 *             }
 *         }];
 *
 *         // callback method to render each cell individually
 *         // with the help of this method you can overwrite default cell rendering behavior,
 *         // e.g. suppressing output for stateDevice property
 *         $scope.cellRenderer = function(row, column) {
 *             var value = angular.isDefined(row[column]) ? row[column] : null;
 *             if (column === 'stateDevice') {
 *                 value = '';
 *             }
 *             return value;
 *         };
 *
 *         // callback method to return class attribute for each column
 *         // in this example together with cellRenderer the deviceState column is
 *         // expressed as just a table data element with css classes
 *         $scope.columnClassCallback = function(row, column, isHeader) {
 *             var columnClass = '';
 *             if (column === 'stateDevice') {
 *                 columnClass = 'state';
 *                 if (row.state === ketaDeviceConstants.STATE.OK && !isHeader) {
 *                     columnClass+= ' state-success';
 *                 }
 *                 if (row.state === ketaDeviceConstants.STATE.ERROR && !isHeader) {
 *                     columnClass+= ' state-warning';
 *                 }
 *                 if (row.state === ketaDeviceConstants.STATE.FATAL && !isHeader) {
 *                     columnClass+= ' state-danger';
 *                 }
 *             }
 *             return columnClass;
 *         };
 *
 *         // callback method to return class attribute for each row
 *         $scope.rowClassCallback = function(row, isHeader) {
 *             var rowClass = 'row-is-selected';
 *             if (isHeader) {
 *                 rowClass += ' header-row';
 *             } else if (angular.isDefined(row.connected) && row.connected === true) {
 *                 rowClass += ' connected';
 *             }
 *             return rowClass;
 *         };
 *
 *         // callback method to return class array for table
 *         $scope.tableClassCallback = function() {
 *             return ['table-striped'];
 *         };
 *
 *         // bootstrap width classes to define the size of the search input
 *         $scope.searchInputWidthClasses = 'col-xs-12 col-sm-6';
 *
 *         // bootstrap width classes to define the size of the selector dropdown
 *         $scope.selectorWidthClasses = 'col-xs-12 col-sm-6 col-md-6 col-lg-6';
 *
 *         // object for pager configuration (total, limit, offset)
 *         // with this configuration object you are able to manage paging
 *         // total is the total number of rows in the dataset
 *         // limit is the number of rows shown per page
 *         // offset is the index in the dataset to start from
 *         var pager = {};
 *         pager[ketaExtendedTableConstants.PAGER.TOTAL] = $scope.allRows.length;
 *         pager[ketaExtendedTableConstants.PAGER.LIMIT] = 5;
 *         pager[ketaExtendedTableConstants.PAGER.OFFSET] = 0;
 *         $scope.pager = pager;
 *
 *         // search term to filter the table
 *         // as two-way-binded property this variable contains the search string
 *         // typed by the user in the frontend and can therefor be used for querying
 *         // the backend, if watched here additionally
 *         $scope.search = null;
 *
 *         // Minimal wait time in milliseconds after last character typed before search kicks-in.
 *         // updates on blur are instant
 *         // default is 0
 *         $scope.waitTime = 500;
 *
 *         // array of search results e.g. for usage in headlines
 *         // defaults to $scope.rows, typically not set directly by controller
 *         //$scope.searchResults = $scope.rows;
 *
 *     });
 *
 */

angular.module('keta.directives.ExtendedTable',
	[
		'ngSanitize',
		'keta.filters.OrderObjectBy',
		'keta.filters.Slice',
		'keta.filters.Unit',
		'keta.utils.Common'
	])

	.constant('ketaExtendedTableConstants', {
		COMPONENT: {
			TABLE: 'table',
			FILTER: 'filter',
			SELECTOR: 'selector',
			PAGER: 'pager'
		},
		OPERATIONS_MODE: {
			DATA: 'data',
			VIEW: 'view'
		},
		PAGER: {
			TOTAL: 'total',
			LIMIT: 'limit',
			OFFSET: 'offset'
		},
		ACTION_LIST_TYPE: {
			LINK: 'link',
			ACTION: 'action'
		}
	})

	// message keys with default values
	.constant('ketaExtendedTableMessageKeys', {
		'en_GB': {
			'__keta.directives.ExtendedTable_search': 'Search',
			'__keta.directives.ExtendedTable_add_column': 'Add column',
			'__keta.directives.ExtendedTable_remove_column': 'Remove column',
			'__keta.directives.ExtendedTable_sort': 'Sort',
			'__keta.directives.ExtendedTable_no_entries': 'No entries',
			'__keta.directives.ExtendedTable_of': 'of'
		},
		'de_DE': {
			'__keta.directives.ExtendedTable_search': 'Suche',
			'__keta.directives.ExtendedTable_add_column': 'Spalte hinzufügen',
			'__keta.directives.ExtendedTable_remove_column': 'Spalte entfernen',
			'__keta.directives.ExtendedTable_sort': 'Sortieren',
			'__keta.directives.ExtendedTable_no_entries': 'Keine Einträge',
			'__keta.directives.ExtendedTable_of': 'von'
		},
		'fr_FR': {
			'__keta.directives.ExtendedTable_search': 'Recherche',
			'__keta.directives.ExtendedTable_add_column': 'Ajouter colonne',
			'__keta.directives.ExtendedTable_remove_column': 'Retirer la colonne',
			'__keta.directives.ExtendedTable_sort': 'Trier',
			'__keta.directives.ExtendedTable_no_entries': 'Pas d’entrées',
			'__keta.directives.ExtendedTable_of': 'de'
		},
		'nl_NL': {
			'__keta.directives.ExtendedTable_search': 'Zoeken',
			'__keta.directives.ExtendedTable_add_column': 'Kolom toevoegen',
			'__keta.directives.ExtendedTable_remove_column': 'Kolom verwijderen',
			'__keta.directives.ExtendedTable_sort': 'Soort',
			'__keta.directives.ExtendedTable_no_entries': 'Geen data',
			'__keta.directives.ExtendedTable_of': 'van'
		},
		'it_IT': {
			'__keta.directives.ExtendedTable_search': 'Ricerca',
			'__keta.directives.ExtendedTable_add_column': 'Aggiungi colonna',
			'__keta.directives.ExtendedTable_remove_column': 'Rimuovere colonna',
			'__keta.directives.ExtendedTable_sort': 'Ordinare',
			'__keta.directives.ExtendedTable_no_entries': 'Nessuna voce',
			'__keta.directives.ExtendedTable_of': 'di'
		}
	})

	.directive('ketaExtendedTable', function ExtendedTableDirective(
		$compile, $filter,
		ketaExtendedTableConstants, ketaExtendedTableMessageKeys, ketaCommonUtils) {
		return {
			restrict: 'EA',
			replace: true,
			scope: {

				// data as array of objects, keys from first element are taken as headers
				rows: '=',

				// current locale
				currentLocale: '=?',

				// label prefixed to selector-component
				labels: '=?',

				// array of disabled components (empty by default)
				disabledComponents: '=?',

				// array of switchable columns (empty by default)
				switchableColumns: '=?',

				// property to group selector by
				groupByProperty: '=?',

				// property to order selector by
				orderByProperty: '=?',

				// array of visible columns (full by default)
				visibleColumns: '=?',

				// callback method to specify header labels (instead of using auto-generated ones)
				headerLabelCallback: '=?',

				// operations mode ("view" for frontend or "data" for backend)
				operationsMode: '=?',

				// boolean flag to enable or disable row sorting in frontend
				rowSortEnabled: '=?',

				// criteria to sort for as string
				rowSortCriteria: '=?',

				// boolean flag to enable ascending sort order for rows
				rowSortOrderAscending: '=?',

				// array of columns that can be sorted (empty by default)
				unsortableColumns: '=?',

				// array of actions to render for each row
				actionList: '=?',

				// callback method to render each cell individually
				cellRenderer: '=?',

				// callback method to return class attribute for each column
				columnClassCallback: '=?',

				// callback method to return class attribute for each row
				rowClassCallback: '=?',

				// callback method to return class array for table
				tableClassCallback: '=?',

				// bootstrap width classes to define the size of the search input
				searchInputWidthClasses: '=?',

				// bootstrap width classes to define the size of the selector dropdown
				selectorWidthClasses: '=?',

				// object for pager configuration (total, limit, offset)
				pager: '=?',

				// search term to filter the table
				search: '=?',

				// Minimal wait time after last character typed before search kicks-in.
				searchWaitMs: '=?',

				// array of search results
				searchResults: '=?',

				// array of selected rows results
				selectionResults: '=?',

				// boolean flag to enable or disable row selection
				selectionEnabled: '=?'

			},
			templateUrl: '/components/directives/extended-table.html',
			link: function(scope) {

				// rows
				scope.rows =
					angular.isDefined(scope.rows) && angular.isArray(scope.rows) ? scope.rows : [];

				scope.currentLocale = scope.currentLocale || 'en_GB';

				// object of labels
				scope.MESSAGE_KEY_PREFIX = '__keta.directives.ExtendedTable';
				scope.labels = angular.extend(ketaExtendedTableMessageKeys, scope.labels);

				scope.getLabel = function getLabel(key) {
					return ketaCommonUtils.getLabelByLocale(key, scope.labels, scope.currentLocale);
				};

				// headers to save
				scope.headers =
					angular.isDefined(scope.rows) && angular.isDefined(scope.rows[0]) ?
						scope.rows[0] : {};

				// disabledComponents
				scope.disabledComponents = scope.disabledComponents || [
					scope.COMPONENTS_FILTER,
					scope.COMPONENTS_SELECTOR,
					scope.COMPONENTS_PAGER
				];

				// switchableColumns
				scope.switchableColumns = scope.switchableColumns || [];
				scope.resetSelectedColumn();

				// groupByProperty
				scope.groupByProperty = scope.groupByProperty || null;

				// orderByProperty
				scope.orderByProperty = scope.orderByProperty || '';

				// visibleColumns
				scope.visibleColumns =
					scope.visibleColumns ||
					(angular.isDefined(scope.rows) && angular.isDefined(scope.rows[0]) ?
						Object.keys(scope.rows[0]) : []);

				// sortableColumns
				scope.unsortableColumns =
					scope.unsortableColumns || [];

				// headerLabelCallback
				scope.headerLabelCallback = scope.headerLabelCallback || function(column) {
					return column;
				};

				// operationsMode
				scope.operationsMode = scope.operationsMode || scope.OPERATIONS_MODE_VIEW;

				// rowSortEnabled
				scope.rowSortEnabled =
					angular.isDefined(scope.rowSortEnabled) ?
						scope.rowSortEnabled : false;

				// rowSortCriteria
				scope.rowSortCriteria =
					scope.rowSortCriteria ||
					(angular.isDefined(scope.rows) && angular.isDefined(scope.rows[0]) ?
						Object.keys(scope.rows[0])[0] : null);

				// rowSortOrderAscending
				scope.rowSortOrderAscending =
					angular.isDefined(scope.rowSortOrderAscending) ?
						scope.rowSortOrderAscending : true;

				// actionList
				scope.actionList = scope.actionList || [];

				// cellRenderer
				scope.cellRenderer = scope.cellRenderer || function(row, column) {
					return angular.isDefined(row[column]) ? row[column] : null;
				};

				// columnClassCallback
				scope.columnClassCallback = scope.columnClassCallback || function() {
					// parameters: row, column, isHeader
					return '';
				};

				// rowClassCallback
				scope.rowClassCallback = scope.rowClassCallback || function() {
					// parameters: row, isHeader
					return '';
				};

				// tableClassCallback
				scope.tableClassCallback = scope.tableClassCallback || function() {
					return ['table-striped'];
				};

				// bootstrap width classes for search input
				scope.searchInputWidthClasses = scope.searchInputWidthClasses || 'col-xs-12 col-sm-4';

				// bootstrap width classes for selector dropdown
				scope.selectorWidthClasses = scope.selectorWidthClasses || 'col-xs-12 col-sm-8 col-md-8 col-lg-8';

				// pager
				var defaultPager = {};
				defaultPager[scope.PAGER_TOTAL] = scope.rows.length;
				defaultPager[scope.PAGER_LIMIT] = scope.rows.length;
				defaultPager[scope.PAGER_OFFSET] = 0;
				scope.pager = angular.extend(defaultPager, scope.pager);
				scope.resetPager();

				// search
				scope.search = scope.search || null;

				// default wait-time for search
				scope.searchWaitMs = angular.isNumber(scope.searchWaitMs) ? scope.searchWaitMs : 0;

				// array of search results
				scope.searchResults = scope.searchResults || scope.rows;

				// selection enabled
				scope.selectionEnabled = scope.selectionEnabled || false;

				// array of selection results
				scope.selectionResults = scope.selectionResults || [];

			},
			controller: function($scope) {

				// CONSTANTS ---

				var KEYCODE_ENTER = 13;

				$scope.COMPONENTS_FILTER = ketaExtendedTableConstants.COMPONENT.FILTER;
				$scope.COMPONENTS_SELECTOR = ketaExtendedTableConstants.COMPONENT.SELECTOR;
				$scope.COMPONENTS_TABLE = ketaExtendedTableConstants.COMPONENT.TABLE;
				$scope.COMPONENTS_PAGER = ketaExtendedTableConstants.COMPONENT.PAGER;

				$scope.OPERATIONS_MODE_DATA = ketaExtendedTableConstants.OPERATIONS_MODE.DATA;
				$scope.OPERATIONS_MODE_VIEW = ketaExtendedTableConstants.OPERATIONS_MODE.VIEW;

				$scope.PAGER_TOTAL = ketaExtendedTableConstants.PAGER.TOTAL;
				$scope.PAGER_LIMIT = ketaExtendedTableConstants.PAGER.LIMIT;
				$scope.PAGER_OFFSET = ketaExtendedTableConstants.PAGER.OFFSET;

				$scope.ACTION_LIST_TYPE_LINK = ketaExtendedTableConstants.ACTION_LIST_TYPE.LINK;
				$scope.ACTION_LIST_TYPE_ACTION = ketaExtendedTableConstants.ACTION_LIST_TYPE.ACTION;

				// VARIABLES ---

				$scope.pages = [];
				$scope.currentPage = 0;
				$scope.selectedColumn = null;

				// HELPER ---

				/**
				 * Checkout all keys and fill empty keys with null
				 * @param {Array} objects array with objects to fill
				 * @returns {Object} filled object
				 */
				var fillAllKeys = function(objects) {
					var keys = [];

					// get all keys
					angular.forEach(objects, function(obj) {
						angular.forEach(obj, function(value, key) {

							if (angular.isDefined(obj) && keys.indexOf(key) === -1) {
								keys.push(key);
							}

						});
					});

					// fill empty keys
					angular.forEach(objects, function(obj) {
						angular.forEach(keys, function(key) {
							if (angular.isDefined(obj)) {
								obj[key] = angular.isDefined(obj[key]) ? obj[key] : null;
							}
						});
					});

					return objects;
				};

				// update properties without using defaults
				var update = function() {

					if (angular.isDefined($scope.rows) && angular.isDefined($scope.rows[0])) {

						// fill all keys
						$scope.rows = fillAllKeys($scope.rows);

						// headers to save
						$scope.headers = $scope.rows[0];

						// visibleColumns
						if ($scope.operationsMode === $scope.OPERATIONS_MODE_VIEW &&
							angular.equals($scope.visibleColumns, [])) {
							$scope.visibleColumns = Object.keys($scope.rows[0]);
						}

						// rowSortCriteria
						if ($scope.rowSortCriteria === null) {
							$scope.rowSortCriteria = Object.keys($scope.rows[0])[0];
						}

					} else {
						$scope.headers = {};
						if ($scope.operationsMode === $scope.OPERATIONS_MODE_VIEW) {
							$scope.visibleColumns = [];
						}
						$scope.rowSortCriteria = null;
					}

				};

				// check if element exists in array
				var inArray = function(array, element) {
					var found = false;
					angular.forEach(array, function(item) {
						if (item === element) {
							found = true;
						}
					});
					return found;
				};

				var resetSelection = function() {
					$scope.selectionResults = [];
				};

				// fill all keys initial
				$scope.rows = fillAllKeys($scope.rows);

				// reset pager object regarding filtered rows
				$scope.resetPager = function() {
					var rowsLength = $scope.rows.length;

					if ($scope.search !== null) {
						$scope.searchResults = $filter('filter')($scope.rows, $scope.searchIn);
						rowsLength = $scope.searchResults.length;
					}

					// update pager
					if ($scope.operationsMode === $scope.OPERATIONS_MODE_VIEW) {
						$scope.pager[$scope.PAGER_TOTAL] = rowsLength;

						if ($scope.pager[$scope.PAGER_LIMIT] === 0) {
							$scope.pager[$scope.PAGER_LIMIT] = rowsLength;
						}

						if ($scope.pager[$scope.PAGER_OFFSET] > rowsLength - 1) {
							$scope.pager[$scope.PAGER_OFFSET] = 0;
						}
					}

					// determine array of pages
					if (angular.isNumber($scope.pager[$scope.PAGER_TOTAL]) &&
						angular.isNumber($scope.pager[$scope.PAGER_LIMIT])) {
						$scope.pages = [];
						var numOfPages = Math.ceil($scope.pager[$scope.PAGER_TOTAL] / $scope.pager[$scope.PAGER_LIMIT]);
						for (var i = 0; i < numOfPages; i++) {
							$scope.pages.push(i + 1);
						}
					}

					// determine current page
					if (angular.isNumber($scope.pager[$scope.PAGER_LIMIT]) &&
						angular.isNumber($scope.pager[$scope.PAGER_OFFSET])) {
						$scope.currentPage =
							Math.floor($scope.pager[$scope.PAGER_OFFSET] / $scope.pager[$scope.PAGER_LIMIT]) + 1;
					}

				};

				// reset selected column
				$scope.resetSelectedColumn = function() {
					var possibleColumns = $filter('filter')($scope.switchableColumns, function(column) {
						return !inArray($scope.visibleColumns, column.id);
					});
					var stillPossible = false;
					angular.forEach(possibleColumns, function(column) {
						if (column.id === $scope.selectedColumn) {
							stillPossible = true;
						}
					});
					if (!stillPossible) {
						possibleColumns = $filter('orderBy')(possibleColumns, $scope.orderByProperty);
						$scope.selectedColumn = angular.isDefined(possibleColumns[0]) ? possibleColumns[0].id : null;
					}
				};

				// check if action list item should be shown
				$scope.showActionListItem = function(item, row) {
					var show = true;
					if (angular.isFunction(item.display)) {
						show = item.display(row);
					}
					return show;
				};

				// WATCHER ---

				$scope.$watch('rows', function(newValue, oldValue) {
					if (newValue !== null && newValue !== oldValue) {
						update();
						$scope.resetPager();
						resetSelection();
					}
				}, true);

				$scope.$watch('pager', function(newValue, oldValue) {
					if (newValue !== null && newValue !== oldValue) {
						$scope.resetPager();
						resetSelection();
					}
				}, true);

				$scope.$watch('search', function(newValue, oldValue) {
					if (newValue !== null && newValue !== oldValue) {
						$scope.resetPager();
						resetSelection();
					}
				});

				$scope.$watch('switchableColumns', function(newValue, oldValue) {
					if (newValue !== null && newValue !== oldValue) {
						$scope.resetSelectedColumn();
					}
				}, true);

				// ACTIONS ---

				$scope.getTableClasses = function() {
					var configuredClasses = $scope.tableClassCallback();
					configuredClasses.push('table');
					configuredClasses.push('form-group');
					return configuredClasses.join(' ');
				};

				$scope.isDisabled = function(key) {
					return inArray($scope.disabledComponents, key);
				};

				$scope.isSwitchable = function(key) {
					var switchable = false;
					angular.forEach($scope.switchableColumns, function(column) {
						if (column.id === key) {
							switchable = true;
						}
					});
					return switchable;
				};

				$scope.isSortCriteria = function(key) {
					return $scope.rowSortCriteria !== null ? key === $scope.rowSortCriteria : false;
				};

				$scope.isSortable = function(column) {
					return $scope.unsortableColumns.indexOf(column) === -1;
				};

				$scope.sortBy = function(column) {
					if ($scope.rowSortEnabled &&
						$scope.headerLabelCallback(column) !== null &&
						$scope.headerLabelCallback(column) !== '') {
						if ($scope.rowSortCriteria === column) {
							$scope.rowSortOrderAscending = !$scope.rowSortOrderAscending;
						} else {
							$scope.rowSortCriteria = column;
						}
					}
				};

				$scope.searchIn = function(row) {
					if (!angular.isDefined($scope.search) || $scope.search === null || $scope.search === '') {
						return true;
					}

					return $scope.visibleColumns.some(function(column) {
						if (angular.isDefined(row[column]) && row[column] !== null) {

							if (angular.isObject(row[column]) && !angular.isArray(row[column])) {

								var deepMatch = false;

								angular.forEach(row[column], function(prop) {
									if (String(prop).toLowerCase().indexOf($scope.search.toLowerCase()) !== -1) {
										deepMatch = true;
									}
								});

								return deepMatch;

							} else if (String(row[column]).toLowerCase().indexOf($scope.search.toLowerCase()) !== -1) {
								return true;
							}
						}
					});
				};

				$scope.filterColumns = function(column) {
					return !inArray($scope.visibleColumns, column.id);
				};

				$scope.addColumn = function(column) {
					$scope.visibleColumns.push(column);
					$scope.resetSelectedColumn();
				};

				$scope.removeColumn = function(column) {
					var columns = [];
					angular.forEach($scope.visibleColumns, function(col) {
						if (col !== column) {
							columns.push(col);
						}
					});
					$scope.visibleColumns = columns;
					$scope.resetSelectedColumn();
				};

				/**
				 * adds / removes clicked row to/from selectionResults array
				 * @param {object} row to add/remove to/from selection
				 * @returns {boolean} if selection is enabled
				 */
				$scope.selectRow = function(row) {

					if (!$scope.selectionEnabled) {
						return false;
					}

					var isSelected = false;

					for (var i = 0; i < $scope.selectionResults.length; i++) {
						if (angular.equals(row, $scope.selectionResults[i])) {
							isSelected = true;
							$scope.selectionResults.splice(i, 1);
							break;
						}
					}

					if (!isSelected) {
						$scope.selectionResults.push(row);
					}
				};

				/**
				 * checks if row is selected
				 * @param {object} row to check
				 * @returns {boolean} is selected or not
				 */
				$scope.isSelected = function(row) {

					if (!$scope.selectionEnabled) {
						return false;
					}

					var isSelected = false;

					for (var i = 0; i < $scope.selectionResults.length; i++) {
						if (angular.equals(row, $scope.selectionResults[i])) {
							isSelected = true;
							break;
						}
					}

					return isSelected;
				};

				/**
				 * @description Jumps to the given page and updates the view accordingly.
				 * @param {number} page The number of the page to go to.
				 * @returns {void} nothing
				 */
				$scope.goToPage = function(page) {
					if (page > 0 && page <= $scope.pages.length) {
						$scope.pager[$scope.PAGER_OFFSET] = $scope.pager[$scope.PAGER_LIMIT] * (page - 1);
						$scope.resetPager();
					}
				};

				/**
				 * @description Resets the pager input (page number) to a valid numeric value if the user
				 * has accidently entered something else and jumps to the page afterwards.
				 * @param {*} currentPage The current input by the user.
				 * @returns {void} nothing
				 */
				var resetPagerInputIfNecessary = function resetPagerInputIfNecessary(currentPage) {
					var parsingRadix = 10;
					var pageAsNumber = parseInt(currentPage, parsingRadix);
					var newPage = pageAsNumber;
					if (!angular.isNumber(pageAsNumber) || isNaN(pageAsNumber) || pageAsNumber <= 0) {
						newPage = 1;
					} else if (pageAsNumber > $scope.pages.length) {
						newPage = $scope.pages.length;
					}
					$scope.goToPage(newPage);
				};

				/**
				 * @description
				 * <p>
				 *   Checks the input (on specific events) that the user makes for the pager's current page
				 *   and resets the input to a valid number if something else (e.g. characters) were entered.
				 * </p>
				 * @param {*} currentPage The current input by the user.
				 * @param {object} $event The jQlite event that is connected with the user interaction.
				 * @returns {void} nothing
				 */
				$scope.checkPagerInput = function checkPagerInput(currentPage, $event) {
					switch ($event.type) {
						case 'keypress':
							if ($event.keyCode === KEYCODE_ENTER) {
								resetPagerInputIfNecessary(currentPage);
							}
							break;
						case 'blur':
							resetPagerInputIfNecessary(currentPage);
							break;
						default:
							break;
					}
				};

			}
		};
	});