<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300,300italic,400italic,700,700italic" rel="stylesheet" type="text/css">
<script src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular-route.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular-resource.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular-sanitize.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
<script src="https://95c7050854423f809e66-6999ba0e7a4f47d417515fb3f08fa9b8.ssl.cf1.rackcdn.com/encore-ui-tpls-0.5.1.min.js"></script>
<link rel="stylesheet" href="rx-data-table.min.css" />
<link rel="stylesheet" href="https://95c7050854423f809e66-6999ba0e7a4f47d417515fb3f08fa9b8.ssl.cf1.rackcdn.com/encore-ui-0.5.1.min.css">
<script src="rx-data-table.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="demo">
<h1>Encore-UI rxDataTable demo</h1>
<div ng-controller="DemoCtrl">
<h3>Customized rxApp</h3>
<rx-app site-title="My App" menu="menuItems" id="custom-rxApp">
<rx-page title="'Customized Page Title'" subtitle="subtitle">
<rx-data-table column-configuration="dtConfig" list-of-data="dtData">
</rx-data-table>
</rx-page>
</rx-app>
</div>
</body>
</html>
angular.module('demo', ['ngRoute',
'ngResource',
'encore.ui',
'rxDataTable',
'ngSanitize'])
.controller('DemoCtrl', function($scope, $location, $rootScope) {
// Encore-UI Nav
$scope.subtitle = 'With a subtitle';
$scope.changeSubtitle = function() {
$scope.subtitle = 'With a new subtitle at ' + Date.now();
};
// Fake navigation
var customApp = document.getElementById('custom-rxApp');
customApp.addEventListener('click', function(ev) {
var target = ev.target;
if (target.className.indexOf('item-link') > -1) {
// prevent the default jump to top
ev.preventDefault();
// update angular location
$location.path(target.getAttribute('href'));
$rootScope.$apply();
}
});
$scope.menuItems = [{
title: 'Example Menu',
children: [{
href: '#1',
linkText: '1st Order Item'
}, {
href: '#2',
linkText: '1st Order Item w/ Children',
// childHeader: 'Current Account {{ user }}',
directive: 'sample-nav-directive',
children: [{
href: '#2-1',
linkText: '2nd Order Item',
children: [{
href: '#2-1-1',
linkText: '3rd Order Item'
}]
}, {
href: '#2-2',
linkText: '2nd Order Item w/ Children',
active: true,
children: [{
href: '#2-2-1',
linkText: '3rd Order Item'
}, {
href: '#2-2-2',
linkText: '3rd Order Item w/ children',
active: true,
children: [{
href: '#2-2-2-1',
linkText: '4th Order Item',
active: true
}, {
href: '#2-2-2-2',
linkText: '4th Order Item'
}]
}, {
href: '#2-2-3',
linkText: '3rd Order Item'
}]
}, {
href: '#2-3',
linkText: '2nd Order Item'
}]
}, {
href: '#3',
linkText: '1st Order Item'
}]
}];
$scope.dtConfig = [{
"cols": 1,
"title": "Status",
"dataField": "status"
}, {
"cols": 1,
"title": "Name",
"dataField": "name"
}, {
"cols": 1,
"title": "Ip",
"dataField": "ip"
}, {
"cols": 1,
"title": "Port",
"dataField": "port"
}, {
"cols": 1,
"title": "Balance method",
"dataField": "balance-method"
}];
$scope.dtData = [{
"id": "1",
"status": "green",
"name": "domain.com",
"ip": "192.168.100.5",
"port": "80",
"balance-method": "round robin",
"balance-description": "Round robin will evenly distribute requests."
}, {
"id": "2",
"status": "red",
"name": "domain.com",
"ip": "192.168.100.5",
"port": "443",
"balance-method": "round robin",
"balance-description": "Round robin will evenly distribute requests."
}, {
"id": "3",
"status": "yellow",
"name": "domain.com",
"ip": "192.168.100.100",
"port": "443",
"balance-method": "least conn",
"balance-description": "Not sure what to put here."
}];
})
/* Styles go here */
/* jshint maxlen: 1000 */
var app = angular.module('rxDataTable', []);
/**
*
* @ngdoc directive
* @namespace rxDataTable
* @name rxDataTable
* @restrict E
* @description
* Directive that creates a data table with responsive design properties. This
* requires the following directives:
*
* - {@link rxDataTable.paginate:rxPaginate rxPaginate} Pagination Directive
* - {@link rxDataTable.paginate:rxItemsPerPage rxItemsPerPage} Items per Page Directive
*
* @param {Object=} pager This is the page tracking object for the directive. If
* no page tracking object is passed in, then the data table will be shown
* without pagination.
* @param {Array.<Object>} list-of-data This is the list of data that the data table will represent
* @param {Array.string=|string=|boolean=} predicate This is the sort predicate. This should be an
* array of strings that will be used as sort predicates. (i.e. **`"['-severity']"`**).
* You may also pass a value of **`false`** in order to disable sorting on
* all columns that don't have a sortField value explicitely defined.
* @param {string=} row-key This is the attribute of the data objects that will
* be used to attatch a data-value-key paramater to each
* row of the table
* @param {string='Items'} item-name This is what the data table will fill in
* to indicate what the items in it really are.
* @param {number=} notify-duration This is a default notification duration in
* milliseconds. This value is 3000 by default.
* @param {string=} row-style This is an object in a string format that is parsed
* in the code and applied to each row in the table.
*
* It takes three attributes:
*
* - **`class`** `{string}`: This is the class that gets to the row.
* - **`field`** `{string}`: This is the data field that the comparisons will be made against.
* - **`bool`** `{boolean}`: If true, then it just checks the field to see if it's truthy and applies the class. If it's false (default), then the value of the field is applied as a class along with the value in the class attribute. To account for non-string data values in the field, this will check to see if the first character in the field value is numerical, if it is, it prepends the value with an underscore (\_). So, for example, if you have your class as data-service-level and the field value turns out to be a 2, then the class attribute of the row will be **`class="data-service-level _2"`**. This allows you to grab that row with **`.data-service-level._2`** and apply CSS values to it.
*
* @param {Object} column-display This object will hold the current display
* state of the various columns in the data table. It has/needs two
* properties:
*
* - **`index`** `{integer}` This is the current column preset index that is
* selected.
* - **`config`** `{array.<integer>}` This is the list of the currently displayed
* column indices from the `columnConfiguration` object
* @param {array.<Object>} column-presets This is a list of objects that
* configure the available column presets for the data table. The format of
* each object should be as follows:
*
* - **`title`** `{string}` This is the title of the preset that will show in the
* dropdown
* - **`config`** `{array.<integer>}` This is the list of indices from the
* columnConfiguration object that will be displayed. Order matters.
* @param {string=} column-multi-sort This is a string taking value of `true` or
* `false`. If `true`, the multi-sorting capabilities of the table will be
* enabled. Value is `false` by default.
* @param {string=} column-reordering This is a string taking value of `true`
* or `false`. If `true`, the `columnReordering` and presets are enabled.
* Default is `false`.
* @param {array.<object>} column-configuration This are the available column definitions, see the
* extended information for this in the {@link #/guides/rx-data-table#column-object Data Table Guide}
*/
app.directive('rxDataTable', function ($http, $timeout, $document, $filter, PageTracking) {
return {
restrict: 'E',
templateUrl: 'src/templates/rx-data-table.html',
replace: true,
scope: {
pager: '=?',
columnConfiguration: '=',
columnDisplay: '=?',
columnPresets: '=?',
rowKey: '@',
rowStyle: '@',
itemName: '@',
listOfData: '&',
predicate: '=?',
checkboxEvent: '&',
columnMultiSort: '@',
notifyDuration: '@',
columnReordering: '@'
},
link: function (scope) {
/* jshint evil: true */
scope.configurationVisible = false;
scope.enableColumnMultiSort = (!_.isEmpty(scope.columnMultiSort)) ? scope.columnMultiSort : false;
scope.enableColumnReordering = (!_.isEmpty(scope.columnReordering)) ? scope.columnReordering : false;
scope.defaultNotificationDuration = (_.isUndefined(scope.notifyDuration)) ? 3000 : parseInt(scope.notifyDuration, 10);
if (_.isUndefined(scope.pager)) {
scope.pager = PageTracking.createInstance();
scope.pager.showAll = true;
}
if (_.isUndefined(scope.columnPresets)) {
// There aren't any presets defined, so we are going to create
// a basic default view
scope.columnPresets = [
{
'title': 'Default View',
'config': []
}
];
_.forEach(scope.columnConfiguration, function (column, index) {
this.columnPresets[0].config.push(index);
}, scope);
}
if (_.isUndefined(scope.columnDisplay)) {
scope.columnDisplay = {index: 0};
}
scope.buildLink = function (row, column) {
if (_.has(column, 'linkField') && _.has(row, column.linkField)) {
return row[column.linkField];
} else if (_.has(column, 'linkFunction') && _.isFunction(column.linkFunction)) {
return column.linkFunction(row);
}
};
scope.getPredicate = function () {
if (scope.predicate === false) {
// This means we're going to be disabling sorting on all
// columns unless they have an explicit sort field
scope.disableSorting = true;
return [];
} else if (!_.isArray(scope.predicate)) {
scope.predicate = [scope.compilePredicateString(scope.getConfig()[0])];
}
return scope.predicate;
};
scope.canAddNewMultiSort = function () {
var pred = scope.getPredicate();
if (_.isEmpty(_.last(pred))) {
return false;
} else {
return (pred.length < scope.columnConfiguration.length);
}
};
scope.getSortField = function (column) {
return (column.sortField||(column.sortField !== false)) && column.sortField || column.dataField;
};
scope.allowEditing = function (column, row) {
return ((_.has(column, 'editable')) && (column.editable.clause(row)));
};
scope.getEditType = function (column, row) {
if (scope.allowEditing(column, row)) {
if (_.has(column.editable, 'data')) {
return 'typeahead';
} else if (_.has(column.editable, 'options')) {
return 'select';
} else {
return 'text';
}
} else {
return false;
}
};
scope.getNGClass = function (column, row) {
var classes = {};
if (_.has(column, 'editable') && _.has(column.editable, 'nullable')) {
classes.nullable = column.editable.nullable;
}
var sortClass = scope.sortClass(column);
if (!_.isEmpty(sortClass)) {
classes[sortClass] = true;
}
if (_.has(column, 'ng-class') && _.isFunction(column['ng-class'])) {
var classFunction = column['ng-class'];
var classValue = classFunction(row);
if (!_.isEmpty(classValue)) {
classes[classValue] = true;
}
} else if (_.has(column, 'ng-class') && _.isObject(column['ng-class'])) {
classes = angular.extend(classes, column['ng-class']);
}
return classes;
};
scope.getEditableOptions = function (column, row) {
if (_.has(column, 'editable')) {
var editable = column.editable;
if (_.has(editable, 'data')) {
var opts = editable.data(row);
if (_.isArray(opts)) {
return opts;
} else {
return [];
}
} else if (_.has(editable, 'options')) {
return editable.options;
}
}
return [];
};
scope.nullField = function (column, row, elementScope) {
if (_.has(column.editable, 'nullable') && column.editable.nullable) {
scope.updateField(column, row, null, elementScope);
}
};
scope.showStatusMessage = function(type, message, duration) {
if (_.isUndefined(duration)) {
duration = scope.defaultNotificationDuration;
}
scope.updateFieldStatus = {
'status': type,
'message': message
};
if (_.isNumber(duration)) {
$timeout(scope.clearStatusMessage.bind(scope), duration);
}
};
scope.clearStatusMessage = function() {
scope.updateFieldStatus = undefined;
};
scope.updateField = function(column, row, data) {
if (!_.has(column.editable, 'endpoint')) {
return false;
}
scope.showStatusMessage('saving', 'Saving value for "' + column.title + '"', false);
var updateMethod = $http[column.editable.endpoint.method];
var updateBody = {};
updateBody[column.dataField] = data;
var updateURL = column.editable.endpoint.url;
var foundElements = /\{(\w+)\}/.exec(updateURL);
while (foundElements) {
updateURL = updateURL.replace(foundElements[0], row[foundElements[1]]);
foundElements = /\{(\w+)\}/.exec(updateURL);
}
// Now we are going to check to see if there is a
// pre-update callback that needs to happen.
if (_.has(column.editable, 'preUpdate') && _.isFunction(column.editable.preUpdate)) {
// This will stop updating with a false value returned
// from the preUpdate function.
if (!column.editable.preUpdate(column, row, data)) {
scope.showStatusMessage('error', 'There was an error running the pre update method and the data was not saved.');
return;
}
}
// We'll run the method
updateMethod(updateURL, updateBody).then(function () {
scope.showStatusMessage('success', 'Saved data for "' + column.title + '" field');
row[column.dataField] = _.clone(data);
// Now we are going to check to see if there is a
// post-update-success callback that needs to happen.
if (_.has(column.editable, 'postUpdateSuccess') && _.isFunction(column.editable.postUpdateSuccess)) {
column.editable.postUpdateSuccess(column, row, data);
}
return true;
}, function (responseData) {
var errorMessage = 'Error saving data for "' + column.title + '" field';
if (_.has(responseData.data, 'error')) {
errorMessage += '\n' + responseData.data.error;
}
scope.$emit('data-table-error', errorMessage);
// Now we are going to check to see if there is a
// post-update-error callback that needs to happen.
if (_.has(column.editable, 'postUpdateError') && _.isFunction(column.editable.postUpdateError)) {
column.editable.postUpdateError(column, row, responseData);
}
return false;
}).then(function () {
$timeout(scope.clearStatusMessage.bind(scope), scope.defaultNotificationDuration);
return true;
});
// We're returning false so that we are manually updating
// the method on success
return false;
};
scope.$on('data-table-error', function ($event, errorString, errorDisplayTimeout) {
if (!_.isNumber(errorDisplayTimeout)) {
errorDisplayTimeout = scope.defaultNotificationDuration;
}
scope.showStatusMessage('error', errorString, errorDisplayTimeout);
$timeout(function () {
scope.updateFieldStatus = undefined;
}, errorDisplayTimeout);
});
scope.iconUnwrap = function (column, row, type) {
return _.filter(column.icon, function (icon) {
if (_.has(icon, 'fieldValue') && (icon.fieldValue === this.row[icon.field])) {
return true;
} else if (row[icon.field] === true) {
return true;
}
}, {row: row}).filter(function (icon) {
if ((_.has(icon, 'name') && (this.type === 'i'))||(_.has(icon, 'class') && (this.type === 'div'))) {
return true;
}
}, {type: type});
};
scope.rowClass = function (row) {
if (!_.isEmpty(scope.rowStyle)) {
if (!_.has(scope, 'rowStyleObject')) {
scope.rowStyleObject = eval('(' + scope.rowStyle + ')');
}
if (scope.rowStyleObject.bool) {
if (row[scope.rowStyleObject.field]) {
return scope.rowStyleObject.class;
}
} else {
var fClass = row[scope.rowStyleObject.field];
fClass = (isNaN(parseInt(fClass.toString().charAt(0), 10))) ? fClass : '_'+fClass.toString();
return scope.rowStyleObject.class + ' '+ fClass;
}
}
};
scope.hasValue = function(row, column) {
if (_.isArray(column.dataField)) {
return _.any(column.dataField, function (fieldName) {
return (_.has(this, fieldName) && !_.isEmpty(this, fieldName));
}, row);
} else {
return (_.has(row, column.dataField) && !_.isEmpty(row, column.dataField));
}
};
scope.decompilePredicateString = function (pred) {
if (_.isArray(pred)) {
return pred;
}
var rev = false;
if (pred.substr(0,1) === '-') {
pred = pred.substr(1);
rev = true;
}
return {'column': pred, 'reverse': rev};
};
scope.getPresetConfiguration = function () {
// This is here to make sure that there is a value specified in
// the columnDisplay.index binding. This is used throughout the
// application and just ensures that things don't go insane.
if (!_.isNumber(scope.columnDisplay.index)) {
scope.columnDisplay.index = 0;
}
if (!scope.isPresetCustom()) {
scope.columnDisplay.config = _.clone(scope.getColumnPresets()[scope.columnDisplay.index].config);
}
return scope.columnDisplay.config;
};
scope.getConfig = function () {
var visibleColumns = [];
_.forEach(scope.getPresetConfiguration(), function (columnIndex) {
this.visibleColumns.push(this.columns[columnIndex]);
}, {visibleColumns: visibleColumns, columns: scope.columnConfiguration});
return visibleColumns;
};
scope.getColumnPresets = function () {
// This is to make sure that when I call columnPresets that it
// actually has something there.
if (_.isEmpty(scope.columnPresets)) {
scope.columnPresets = [];
}
return scope.columnPresets;
};
scope.markPresetAsCustom = function () {
var presets = scope.getColumnPresets();
if (!_.find(presets, {'title': 'User Preset'})) {
presets.push({
'title': 'User Preset',
'config': _.clone(scope.columnDisplay.config)
});
} else {
presets[presets.length - 1].config = _.clone(scope.columnDisplay.config);
}
if (_.isNumber(scope.columnDisplay.index)) {
scope.columnDisplay.index = presets.length - 1;
}
};
scope.getColumnPresetSelects = function () {
return _.map(scope.getColumnPresets(), function (preset, index) {
return {'text': preset.title, 'value': index};
});
};
scope.isPresetCustom = function () {
return (scope.getColumnPresets()[scope.columnDisplay.index].title == 'User Preset');
};
scope.moveColumn = function(currentIndex, destinationIndex) {
if (!scope.isPresetCustom()) {
scope.markPresetAsCustom();
}
var columnToMove = scope.columnDisplay.config.splice(currentIndex, 1)[0];
scope.columnDisplay.config.splice(destinationIndex, 0, columnToMove);
};
scope.moveColumnUp = function (columnIndex) {
if (columnIndex > 0) {
scope.moveColumn(columnIndex, columnIndex - 1);
}
};
scope.moveColumnDown = function (columnIndex) {
if (columnIndex < scope.columnDisplay.config.length - 1) {
scope.moveColumn(columnIndex, columnIndex + 1);
}
};
scope.removeColumn = function (columnIndex) {
if (!scope.isPresetCustom()) {
scope.markPresetAsCustom();
}
scope.columnDisplay.config.splice(columnIndex, 1);
};
scope.showColumn = function (columnIndex) {
if (!scope.isPresetCustom()) {
scope.markPresetAsCustom();
}
if (!_.contains(scope.columnDisplay.config, columnIndex)) {
scope.columnDisplay.config.push(columnIndex);
}
};
scope.getAvailableColumns = function () {
var columnSelects = [];
_.forEach(scope.columnConfiguration, function (column, index) {
this.push({'value': index, 'text': column.title});
}, columnSelects);
return _.filter(columnSelects, function (column) {
return (!_.contains(this, column.value));
}, scope.columnDisplay.config);
};
scope.findColumnFromPredicate = function (pred) {
var column = _.find(scope.getConfig(), function (column) {
return (this.pred == this.getSortField(column));
}, {pred: pred, getSortField: scope.getSortField});
return column || {};
};
scope.parseReverseSort = function (column, rev) {
if (_.isObject(column)) {
return (column.sortReverse) ? !rev : rev;
} else {
return scope.parseReverseSort(scope.findColumnFromPredicate(column), rev);
}
};
scope.compilePredicateString = function (column, rev, fromSortField) {
var pred = (fromSortField) ? column : scope.getSortField(column);
return ((scope.parseReverseSort(column, rev)) ? '-' : '') + pred;
};
scope.singleColumnSort = function(column) {
scope.pager.pageNumber = 0;
if (scope.sortedBy(column)) {
scope.predicate = [scope.compilePredicateString(column, true)];
} else {
scope.predicate = [scope.compilePredicateString(column)];
}
angular.element(document).data('sortingData', scope.getPredicate());
};
scope.addColumnSort = function (column) {
var sortIndex = scope.getSortedIndex(column);
if (sortIndex === -1) {
sortIndex = scope.getSortedIndex(column, true);
if (sortIndex >= 0) {
if (scope.getPredicate().length > 1) {
scope.predicate.splice(sortIndex, 1);
} else {
scope.predicate[sortIndex] = scope.compilePredicateString(column);
}
} else {
scope.predicate.push(scope.compilePredicateString(column));
}
} else {
scope.predicate[sortIndex] = scope.compilePredicateString(column, true);
}
};
scope.sortable = function (column) {
return ((scope.disableSorting && _.has(column, 'sortField')) || (!scope.disableSorting));
};
scope.sort = function ($event, column) {
if (!scope.sortable(column)) {
return;
}
if ($event.shiftKey) {
scope.addColumnSort(column);
} else {
scope.singleColumnSort(column);
}
};
scope.sortClass = function (column) {
var index = scope.getSortedIndex(column);
if (index === -1) {
index = scope.getSortedIndex(column, true);
}
if (index >= 0) {
return 'sorted-' + index + '-' + ((scope.sortedBy(column)) ? 'asc' : 'desc');
}
};
scope.getSortedIndex = function (column, inverted) {
var pred = scope.compilePredicateString(column, inverted);
return scope.getPredicate().indexOf(pred);
};
scope.sortedBy = function (column, inverted) {
return (scope.getSortedIndex(column, inverted) >= 0);
};
scope.reversePredicate = function (index) {
var pred = scope.decompilePredicateString(scope.getPredicate()[index]);
pred.reverse = scope.parseReverseSort(pred.column, pred.reverse);
scope.predicate[index] = scope.compilePredicateString(pred.column, !pred.reverse, true);
};
scope.updatePredicate = function (index, selectBoxValue) {
var column = scope.findColumnFromPredicate(selectBoxValue);
if (!_.isEmpty(column)) {
scope.predicate[index] = scope.compilePredicateString(column);
}
};
scope.removePredicate = function (index) {
if (scope.getPredicate().length > 1) {
scope.predicate.splice(index, 1);
}
};
scope.getSortableColumnSelects = function () {
var returnSelects = [];
_.forEach(scope.getConfig(), function (column) {
this.retObj.push({'text': column.title, 'value': this.getSortField(column)});
}, {'retObj': returnSelects, 'getSortField': scope.getSortField});
return _.filter(_.filter(returnSelects, 'text'), 'value');
};
scope.toggleVisibility = function (state) {
if (state !== undefined) {
scope.configurationVisible = state;
} else {
scope.configurationVisible = !scope.configurationVisible;
}
};
scope.menuShown = {};
scope.toggleMenu = function (row, column) {
if (scope.isMenuShown(row, column)) {
scope.menuShown = {};
} else {
scope.menuShown = {col: column, row: row};
}
};
scope.isMenuShown = function (row, column) {
if (_.isUndefined(scope.menuShown)) {
scope.menuShown = {};
}
return (_.isEqual(scope.menuShown, {row: row, col: column}));
};
scope.executeAction = function (row, menuItem) {
if (_.has(menuItem, 'action') && _.isFunction(menuItem.action)) {
menuItem.action(row);
scope.menuShown = {};
}
};
// Have to a setTimeout so that it's there.
setTimeout(function () {
var stopProp = function (e) {
e.stopPropagation();
};
angular.element(document.querySelector('.data-table-config-container')).on('click', stopProp);
angular.element(document.querySelector('.menu-column')).on('click', stopProp);
}, 1);
scope.$watch(function () { return scope.listOfData; }, function () {
var stopProp = function (e) {
e.stopPropagation();
};
angular.element(document.querySelector('.menu-column')).on('click', stopProp);
}, true);
$document.on('click', function () {
this.toggleVisibility(false);
}.bind(scope));
}
};
});
app.filter('UnusedSorts', function() {
return function(configObject, predicates, currentColumn) {
return _.filter(configObject, function (column) {
// This is here to find the sortField value
var sortField = (column.sortField||(column.sortField !== false)) && column.sortField || column.dataField;
if (currentColumn == sortField) {
return true;
} else {
return !(_.contains(this.predicate, sortField) || _.contains(this.predicate, '-'+sortField));
}
}, {predicate: predicates});
};
});
app.filter('ColumnValue', function ($filter) {
return function (row, column, allowEditing) {
var columnValue = {value: ''};
allowEditing = (_.isUndefined(allowEditing)) ? true : allowEditing;
var field = (_.has(column, 'displayField') && _.has(row, column.displayField)) ? column.displayField : column.dataField;
if (!_.isArray(field)) {
field = [field];
}
_.forEach(field, function (fieldName, fieldIndex, field) {
if (_.has(this.row, fieldName)) {
if (_.has(this.column, 'filter')) {
if (_.has(this.column, 'filterParameters')) {
this.columnValue.value += $filter(this.column.filter).apply(this, [this.row[fieldName]].concat(this.column.filterParameters));
} else {
this.columnValue.value += $filter(this.column.filter)(this.row[fieldName]);
}
} else {
this.columnValue.value += this.row[fieldName];
}
if (fieldIndex < field.length - 1) {
this.columnValue.value += '\n';
}
}
}, {columnValue: columnValue, column: column, row: row});
columnValue = columnValue.value;
if (allowEditing) {
columnValue = ((_.isEmpty(columnValue)) && (_.has(column, 'emptyValue'))) ? column.emptyValue : columnValue;
}
return columnValue;
};
});
angular.module('rxDataTable')
/**
*
* @ngdoc directive
* @name rxDataTable.paginate:rxPaginate
* @restrict E
* @description
* Directive that takes in the page tracking object and outputs a page
* switching controller
*
* @param {Object} page-tracking This is the page tracking service instance to
* be used for this directive
* @param {number} number-of-pages This is the maximum number of pages that the
* page object will display at a time.
*/
.directive('rxDataTablePaginate', function () {
return {
templateUrl: 'src/templates/rx-data-table-paginate.html',
replace: true,
restrict: 'E',
scope: {
pageTracking: '=',
numberOfPages: '@'
},
link: function (scope) {
scope.$watch(function () {
return scope.pageTracking.total;
}, function () {
if (scope.pageTracking.pageNumber >= scope.pageTracking.totalPages) {
scope.pageTracking.pageNumber = 0;
}
});
}
};
})
/**
*
* @ngdoc service
* @name rxDataTable.paginate:PageTracking
* @description
* This is the data service that can be used in conjunction with the pagination
* objects to store/control page display of data tables and other items.
*
* @property {number} MAX_PER_PAGE This is a value that is used in the
* iteration function to generate the item size list.
* @property {number} MIN_PER_PAGE This is a value that is used in the
* iteration function to generate the item size list.
* @property {number} ITEMS_PER_PAGE_STEP This is a value that is used in the
* iteration function to generate the item size list.
* @property {number} itemsPerPage This is the current setting for the number
* of items to display per page
* @property {number} pagesToShow This is the number of pages to show
* in the pagination controls
* @property {Array} itemSizeList This is where the
* {@link rxDataTable.paginate:rxItemsPerPage rxItemsPerPage}
* Directive will store it's list of items per page
* @property {number} pageNumber This is where the current page number is
* stored.
* @property {boolean} pageInit This is used to determine if the page has been
* initialzed before.
* @property {number} total This is the total number of items that are in the
* data set
* @property {boolean} showAll This is used to determine whether or not to use
* the pagination or not.
*
* @method createInstance This is used to generate the instance of the
* PageTracking object.
*/
.factory('PageTracking', function() {
function PageTrackingObject(showAll) {
this.MAX_PER_PAGE = 50;
this.MIN_PER_PAGE = 10;
this.ITEMS_PER_PAGE_STEP = 10;
this.itemsPerPage = 10;
this.pagesToShow = 5;
this.itemSizeList = [];
this.pageNumber = 0;
this.pageInit = false;
this.total = 0;
this.showAll = (showAll) ? true : false;
}
return {
createInstance: function(showAll) {
return new PageTrackingObject(showAll);
}
};
}).
/**
*
* @ngdoc directive
* @name rxDataTable.paginate:rxItemsPerPage
* @restrict E
* @description
* Directive that takes in a page-tracking object and a label for what to call
* items and outputs a select box that allows you to change how many items in
* the list to show at a time
*
* @param {Object} pager This is the page tracking service instance to
* be used for this directive
* @param {string='Items'} label This is the name of the items that you are
* restricting. It defaults to 'Items' and thus outputs 'Items per page'
*/
directive('rxDataTableItemsPerPage', function(PageTracking) {
return {
restrict: 'E',
replace: true,
templateUrl: 'src/templates/rx-data-table-itemsPerPage.html',
scope: {
label: '@',
pager: '=?'
},
link: function(scope) {
if (_.isUndefined(scope.pager)) {
scope.pager = PageTracking.createInstance();
}
if (!scope.pager.pageInit) {
scope.pager.pageInit = true;
}
try {
scope.updatePaging = function () {
scope.pager.itemsPerPage = parseInt(scope.pager.itemsPerPage, 10);
scope.pager.pageNumber = 0;
}.bind(scope);
} catch (err) {
// This is here because the tests are being weird.
}
scope.pager.itemSizeList = _.range(scope.pager.MIN_PER_PAGE,
scope.pager.MAX_PER_PAGE + scope.pager.ITEMS_PER_PAGE_STEP,
scope.pager.ITEMS_PER_PAGE_STEP);
}
};
})
/**
*
* @ngdoc filter
* @name rxDataTable.paginate:Paginate
* @description
* This is the pagination filter that is used to calculate the division in the
* items list for the paging.
*
* @param {Object} items The list of items that are to be sliced into pages
* @param {Object} pager The instance of the PageTracking service. If not
* specified, a new one will be created.
*
* @returns {Object} The list of items for the current page in the PageTracking object
*/
.filter('Paginate', function (PageTracking) {
return function (items, pager) {
if (!pager) {
pager = PageTracking.createInstance();
}
if (pager.showAll) {
pager.total = items.length;
return items;
}
if (items) {
pager.total = items.length;
pager.totalPages = Math.ceil(pager.total / pager.itemsPerPage);
var first = pager.pageNumber * pager.itemsPerPage;
var added = first + pager.itemsPerPage;
var last = (added > items.length) ? items.length : added;
pager.first = parseInt(first + 1, 10);
pager.last = parseInt(last, 10);
return items.slice(first, last);
}
};
})
/**
*
* @ngdoc filter
* @name rxDataTable.paginate:Page
* @description
* This is the pagination filter that is used to limit the number of pages
* shown
*
* @param {Object} pager The instance of the PageTracking service. If not
* specified, a new one will be created.
*
* @returns {Array} The list of page numbers that will be displayed.
*/
.filter('Page', function (PageTracking) {
return function (pager) {
if (!pager) {
pager = PageTracking.createInstance();
}
var displayPages = [],
// the next four variables determine the number of pages to show ahead of and behind the current page
pagesToShow = pager.pagesToShow || 5,
pageDelta = (pagesToShow - 1) / 2,
pagesAhead = Math.ceil(pageDelta),
pagesBehind = Math.floor(pageDelta);
if ( pager && pager.length !== 0) {
// determine starting page based on (current page - (1/2 of pagesToShow))
var pageStart = Math.max(Math.min(pager.pageNumber - pagesBehind, pager.totalPages - pagesToShow), 0),
// determine ending page based on (current page + (1/2 of pagesToShow))
pageEnd = Math.min(Math.max(pager.pageNumber + pagesAhead, pagesToShow - 1), pager.totalPages - 1);
for (pageStart; pageStart <= pageEnd; pageStart++) {
// create array of page indexes
displayPages.push(pageStart);
}
}
return displayPages;
};
});
angular.module('rxDataTable').run(['$templateCache', function ($templateCache) {
$templateCache.put('src/templates/rx-data-table-itemsPerPage.html', '<form id="itemsPerPageForm" class="itemsPerPage"> <label for="itemsPerPageSelector">{{ label }}</label> <select name="itemsPerPageSelector" id="itemsPerPageSelector" ng-model="pager.itemsPerPage" ng-change="updatePaging()"> <option ng-repeat="i in pager.itemSizeList">{{ i }}</option> </select> </form> ');
$templateCache.put('src/templates/rx-data-table-paginate.html', '<ul class="pagination"> <li ng-class="{disabled: pageTracking.pageNumber==0}" class="pagination-first"> <a ng-click="pageTracking.pageNumber=0" ng-hide="pageTracking.pageNumber==0">First</a> <span ng-show="pageTracking.pageNumber==0">First</span> </li> <li ng-class="{disabled: pageTracking.pageNumber==0}" class="pagination-prev"> <a ng-click="pageTracking.pageNumber=(pageTracking.pageNumber - 1)" ng-hide="pageTracking.pageNumber==0">« Prev</a> <span ng-show="pageTracking.pageNumber==0">« Prev</span> </li> <li ng-repeat="n in pageTracking | Page " ng-class="{active: n==pageTracking.pageNumber, \'page-number-last\': n==pageTracking.totalPages - 1}" class="pagination-page"> <a ng-click="pageTracking.pageNumber=n">{{n + 1}}</a> </li> <li ng-class="{disabled: pageTracking.pageNumber==pageTracking.totalPages - 1 || pageTracking.total==0}" class="pagination-next"> <a ng-click="pageTracking.pageNumber=(pageTracking.pageNumber + 1)" ng-hide="pageTracking.pageNumber==pageTracking.totalPages - 1 || pageTracking.total==0"> Next »</a> <span ng-show="pageTracking.pageNumber==pageTracking.totalPages - 1">Next »</span> </li> <li ng-class="{disabled: pageTracking.pageNumber==pageTracking.totalPages - 1}" class="pagination-last"> <a ng-click="pageTracking.pageNumber=pageTracking.totalPages - 1" ng-hide="pageTracking.pageNumber==pageTracking.totalPages - 1">Last</a> <span ng-show="pageTracking.pageNumber==pageTracking.totalPages - 1">Last</span> </li> </ul> ');
$templateCache.put('src/templates/rx-data-table.html', '<div class="data-table"> <div class=\'alert\' ng-show="updateFieldStatus" ng-class="{\'loading\': updateFieldStatus.status==\'saving\', \'success\': updateFieldStatus.status==\'success\', \'error\': updateFieldStatus.status==\'error\'}"> {{ updateFieldStatus.message }} </div> <div class="data-info-row"> <strong ng-if="!pager.showAll">{{(pager.last == 0) ? \'0\' : pager.first}} - {{pager.last}} of <span class=\'total-data-items\'>{{pager.total}} {{ itemName && itemName || \'Items\' }}</span></strong> <div class="data-table-config-container" ng-if="enableColumnReordering||enableColumnMultiSort" ng-class="{\'dropdown-shown\': configurationVisible}"> <button class="btn-link" ng-click="toggleVisibility()"> <i class="fa fa-table data-table-config-icon" title="Configure Data Table"></i> </button> <div class="data-table-config reveal-animation" ng-show="configurationVisible"> <div class="header" ng-if="enableColumnMultiSort">Sorting</div> <div class="data-table-multi-sort" ng-if="enableColumnMultiSort"> <div class="data-config-row"> <div class="multi-sort-select header">Column</div> <div class="multi-sort-reverse-icon header">Dir</div> <div class="multi-sort-remove-icon header"><span ng-show="predicate.length> 1">Rem</span></div> </div> <div class="data-config-row" ng-repeat="pred in predicate" ng-init="predColumn=decompilePredicateString(pred)"> <div class="multi-sort-select"> <select name="sort-{{$index}}" ng-model="predColumn.column" ng-change="updatePredicate($index, predColumn.column)"> <option ng-repeat="column in getConfig() | UnusedSorts:predicate:predColumn.column" value="{{ getSortField(column) }}" ng-selected="getSortField(column)==predColumn.column">{{ column.title }}</option> </select> </div> <button class="btn-link multi-sort-reverse-icon" ng-click="reversePredicate($index)"> <i class="fa" ng-class="{\'fa-sort-amount-asc\': !parseReverseSort(predColumn.column, predColumn.reverse), \'fa-sort-amount-desc\': parseReverseSort(predColumn.column, predColumn.reverse)}"></i> </button> <div class="multi-sort-remove-icon"> <button class="btn-link" ng-click="removePredicate($index)"> <i class="fa fa-times" ng-if="predicate.length> 1"></i> </button> </div> </div> <button class="btn-link multi-sort-add" ng-if="canAddNewMultiSort()" ng-click="predicate.push(\'\')"> Add New Sort </button> </div> <div class="header" ng-if="enableColumnReordering">Column Configuration</div> <div class="data-table-column-display" ng-if="enableColumnReordering"> <div class="data-config-row"> <div class="header">Column Presets</div> </div> <div class="data-config-row column-preset-row"> <select ng-options="preset.value as preset.text for preset in getColumnPresetSelects()" ng-model="columnDisplay.index"></select> </div> <div class="data-config-row"> <div class="header">Column Order</div> </div> <div class="data-config-row" ng-repeat="column in getConfig()"> <div class="data-config-column-title"> {{ column.title }} </div> <div class="column-order-arrows"> <button class="btn-link btn-move-down" ng-if="!$last" ng-click="moveColumnDown($index)"> <i class="fa fa-arrow-down"></i> </button> <button class="btn-link btn-move-up" ng-if="!$first" ng-click="moveColumnUp($index)"> <i class="fa fa-arrow-up"></i> </button> </div> <div class="column-hide-display"> <button class="btn-link" ng-click="removeColumn($index)"> <i class="fa fa-times"></i> </button> </div> </div> <div class="data-config-row" ng-if="getAvailableColumns().length> 0"> <div class="header">Available Columns</div> </div> <div class="data-config-row column-show-columns" ng-if="getAvailableColumns().length> 0"> <select ng-model="addColumn.index" ng-options="column.value as column.text for column in getAvailableColumns()"> </select> <button ng-click="showColumn(addColumn.index)" class="button button-tiny">add</button> </div> </div> </div> </div> </div> <div class="data-header"> <div class="data-header-cell data-column-{{ $index + 1 }} flex-columns-{{ column.cols }}" ng-repeat="column in getConfig()" data-title="{{ column.title && column.title || column.dataField }}" ng-class="sortClass(column, $index)"> <span class="checkbox-span" ng-if="column.checkbox && column.checkAll"> <input ng-click="checkAll(this)" type="checkbox" id="check_all_checkbox"> </span> <button ng-if="!column.checkbox" ng-click="sort($event, column)" class="btn-link data-link"> <span class=\'data-header-cell-content\'>{{ column.title }}</span> <i ng-if="column.help" class="fa fa-question-circle" popover="{{ column.help.body }}" popover-title="{{ column.help.title }}" popover-trigger="mouseenter"></i> <i class="fa fa-chevron-down" ng-show="sortedBy(column, true)"></i> <i class="fa fa-chevron-up" ng-show="sortedBy(column)"></i> </button> </div> </div> <div class="data-row data-row-{{ $index + 1}}" ng-repeat="row in listOfData() | orderBy:predicate | Paginate:pager track by row[rowKey]||$index" data-row-key="{{row[rowKey]}}" ng-class="rowClass(row)"> <div class="data-cell flex-columns-{{ column.cols }} data-column-{{ $index + 1}} {{ column.class }}" ng-repeat="column in getConfig()" data-title="{{ column.title }}" ng-class="getNGClass(column, row)"> <div ng-if="column.checkbox" class="checkbox"> <input type="checkbox" value="{{ row[column.dataField] }}" ng-click="clickAction(\'{{ row[column.dataField] }}\')" id="checkbox_{{ row[column.dataField] }}"> </div> <div ng-if="column.menu" class="menu-column"> <button class="menu-toggle btn-link" ng-click="toggleMenu(row, column)"> <i ng-if="!column.menu.icon" class="fa fa-cog fa-lg"></i> <i ng-if="column.menu.icon" class="fa" ng-class="column.menu.icon"></i> </button> <ul class="menu-items" ng-show="isMenuShown(row, column)"> <li class="menu-item" ng-repeat="menuItem in column.menu.items"> <button class="btn-link" ng-class="menuItem.class" ng-click="executeAction(row, menuItem)"> <i ng-if="menuItem.icon" class="fa" ng-class="menuItem.icon"></i> <span class="menu-item-text">{{ menuItem.text }}</span> </button> </li> </ul> </div> <div ng-if="!column.checkbox && !column.menu"> <i ng-repeat="icon in iconUnwrap(column, row, \'i\')" class="data-table-cell-icon {{ icon.name }}" tooltip="{{ icon.tooltip.text }}" tooltip-placement="{{ (icon.tooltip.placement) ? icon.tooltip.placement : \'top\' }}"></i> <div ng-repeat="icon in iconUnwrap(column, row, \'div\')" class="data-table-cell-icon {{ icon.class }}" alt="{{ icon.alt }}" tooltip="{{ icon.tooltip.text }}" tooltip-placement="{{ (icon.tooltip.placement) ? icon.tooltip.placement : \'top\' }}"></div> <a ng-if="buildLink(row, column) && hasValue(row, column)" href="{{ buildLink(row, column) }}" class="data-cell-content" target="{{ (column.linkTarget) ? column.linkTarget : \'_blank\' }}">{{ row | ColumnValue:column }}</a> <span ng-if="!buildLink(row, column) && hasValue(row, column)" class="data-cell-content"> <span ng-if="!allowEditing(column,row)">{{ row | ColumnValue:column:false }}</span> <span ng-if="allowEditing(column,row)" class="data-editable"> <span ng-switch="getEditType(column, row)"> <span ng-switch-when="select" class="data-editable-field" editable-select="row[column.dataField]" e-ng-options="o.value as o.text for o in getEditableOptions(column, row)" buttons="no" onbeforesave="updateField(column, row, $data, this)">{{ row | ColumnValue:column }}</span> <span ng-switch-when="typeahead" class="data-editable-field" editable-text="row[column.dataField]" e-typeahead="o.value as o.text for o in getEditableOptions(column, row) | filter:$viewValue" e-typeahead-loading="loadingData" e-typeahead-on-select="updateField(column, row, $data, this)" buttons="no" onshow="getEditableOptions(column, row)">{{ row | ColumnValue:column }}</span> <span ng-switch-default class="data-editable-field" editable-text="row[column.dataField]" onbeforesave="updateField(column, row, $data, this)">{{ row | ColumnValue:column }}</span> <i class="fa fa-refresh data-table-type-loader" ng-show="loadingData"></i> <button class="data-table-field-nullable btn-link" ng-if="column.editable.nullable" ng-click="nullField(column, row, this)" title="Remove {{ column.title }} Value"> <i class="fa fa-times fa-lg"></i> </button> </span> </span> </span> </div> </div> </div> <div ng-if="!pager.showAll" class="pagination-container" ng-show="pager.total> 0"> <rx-data-table-paginate page-tracking="pager"/> </div> <div ng-if="!pager.showAll" class="items-per-page-container" ng-show="pager.total> 0"> <rx-data-table-items-per-page pager="pager" label="{{ itemName && itemName || \'Items\'}} Per Page"/></rx-data-table-items-per-page> </div> </div> ');
}]);
/*! rxDataTable v0.4.7 by Stephen Golub(stephen.golub@rackspace.com) - https://github.com/nickburns2006/rxDataTable - New BSD License */
.flex-columns-10{-webkit-box-flex:10;-moz-box-flex:10;-webkit-flex:10;-ms-flex:10;flex:10}.flex-columns-9{-webkit-box-flex:9;-moz-box-flex:9;-webkit-flex:9;-ms-flex:9;flex:9}.flex-columns-8{-webkit-box-flex:8;-moz-box-flex:8;-webkit-flex:8;-ms-flex:8;flex:8}.flex-columns-7{-webkit-box-flex:7;-moz-box-flex:7;-webkit-flex:7;-ms-flex:7;flex:7}.flex-columns-6{-webkit-box-flex:6;-moz-box-flex:6;-webkit-flex:6;-ms-flex:6;flex:6}.flex-columns-5{-webkit-box-flex:5;-moz-box-flex:5;-webkit-flex:5;-ms-flex:5;flex:5}.flex-columns-4{-webkit-box-flex:4;-moz-box-flex:4;-webkit-flex:4;-ms-flex:4;flex:4}.flex-columns-3{-webkit-box-flex:3;-moz-box-flex:3;-webkit-flex:3;-ms-flex:3;flex:3}.flex-columns-2{-webkit-box-flex:2;-moz-box-flex:2;-webkit-flex:2;-ms-flex:2;flex:2}.flex-columns-1{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.data-table{margin:0 25px}.data-table .pagination-container{text-align:center;width:100%;margin-bottom:15px}.data-table .items-per-page-container{text-align:center;width:100%}.data-info-row{display:block;text-align:right}.data-header{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-o-user-select:none;user-select:none}.data-header-cell{text-align:left;background:#878787;color:#fff;border:1px solid #6e6e6e;border-right:0;cursor:pointer;overflow:visible;white-space:nowrap}.data-header-cell::last-child{border-right:1px solid #6e6e6e;margin-right:-2px}.data-header-cell .btn-link{cursor:pointer;background:0 0;border:0}.data-header-cell .data-link,.data-header-cell .checkbox-span{display:block;height:auto;width:100%;text-align:left}.data-header-cell .data-link{color:#FFF;padding:10px}.data-header-cell .checkbox-span{padding:7px;padding-bottom:8px;text-align:center}.data-row-container.attention{border-left:3px solid #39C!important;margin-top:5px;overflow:hidden}.data-row-container.attention .data-row{border-top:0}.data-row{background-color:#fff;border-bottom:1px solid #eee;height:45px;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.data-row .label{top:0;font-size:.85em}.data-row:nth-child(odd){background-color:#f6f6f6}.data-row .data-cell{height:100%;border-left:1px solid #eee;border-right:0;margin-right:-1px}.data-row .data-cell .checkbox{text-align:center}.data-row .data-cell:last-child{border-right:1px solid #eee;margin-right:-2px}.data-row .data-cell>span,.data-row .data-cell>div{display:block;margin:10px}.data-row .data-cell .data-table-cell-icon{display:block;vertical-align:middle;float:left;margin:2px}.data-row .data-cell .data-cell-content{white-space:pre;display:block;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.data-row:hover{background-color:#f5f5f5}.data-row.one-row{height:45px}.data-row.two-row{height:65px}.data-editable .data-editable-field+.editable-text .editable-input{width:100%}.data-editable .data-editable-field+.editable-select .editable-input{width:calc(100% - 20px)}.data-editable .data-editable-field.editable-click{color:#547917;border-bottom-color:#547917;font-weight:600;line-height:19px}.data-editable .data-editable-field.editable-click:hover{cursor:pointer;text-decoration:underline}.data-cell.nullable .data-table-field-nullable{display:none;color:#547917;cursor:pointer}.data-cell.nullable .editable-wrap{width:calc(100% - 25px);margin-right:8px}.data-cell.nullable .editable-wrap+.data-table-field-nullable,.data-cell.nullable .editable-wrap+.data-table-type-loader+.data-table-field-nullable{display:inline-block}.data-table-config-container{position:relative;width:15px;display:inline-block}.data-table-config-container .data-table-config-icon{cursor:pointer;color:#547917;font-size:14px}.data-table-config{position:absolute;overflow:visible;right:0;background-color:#FFF;border:1px solid #000;border-radius:5px;padding:4px;min-width:150px}.data-table-config>.header{text-align:center;font-size:11px;font-weight:700;margin-bottom:2px;border-bottom:2px solid #000}.data-table-config .data-config-row{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;margin:2px 0}.data-table-config .data-config-row:nth-child(even){background-color:#f6f6f6}.data-table-config .data-config-row>.header{font-size:10px;font-weight:700;margin-bottom:2px;border-bottom:1px solid #000}.data-table-multi-sort .multi-sort-select{-webkit-box-flex:4;-moz-box-flex:4;-webkit-flex:4;-ms-flex:4;flex:4;text-align:left}.data-table-multi-sort .multi-sort-reverse-icon,.data-table-multi-sort .multi-sort-remove-icon{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;text-align:center;cursor:pointer}.data-table-multi-sort .multi-sort-add{width:100%;text-align:center;font-size:10px;font-weight:700;margin-top:5px;cursor:pointer}.data-table-column-display .header{margin:0 auto}.data-table-column-display .column-preset-row{text-align:center}.data-table-column-display .data-config-column-title{text-align:left;-webkit-box-flex:3;-moz-box-flex:3;-webkit-flex:3;-ms-flex:3;flex:3}.data-table-column-display .column-order-arrows{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;text-align:left;padding-left:5px}.data-table-column-display .column-order-arrows .btn-move-up{margin-left:15px}.data-table-column-display .column-order-arrows .btn-move-down+.btn-move-up{margin-left:0}.data-table-column-display .column-hide-display{-webkit-box-flex:1;-moz-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;text-align:center}.menu-column{position:relative}.menu-column .menu-toggle{color:#AAA;min-height:14px}.menu-column .menu-toggle:hover,.menu-column .menu-toggle:active{color:#555}.menu-column .menu-items{z-index:99;position:absolute;top:-10px;right:98px;min-width:12em;font-size:.9em;border:4px solid #fff;border-radius:4px;background:#fff;text-shadow:rgba(0,0,0,.09) 1px 1px 1px;box-shadow:rgba(0,0,0,.09) 1px 1px 4px 0;padding:6px}.menu-column .menu-items::before{content:"\f0da";font-family:FontAwesome;font-style:normal;font-weight:400;text-decoration:inherit;color:#fff;font-size:24px;display:block;right:-4px;position:absolute;top:0;width:0;z-index:1}.menu-column .menu-items .menu-item{margin:3px 0}.paginate-area{text-align:center}.pagination{display:inline-block;padding-left:0}.pagination li{display:inline}.pagination li a{cursor:pointer}.pagination li a,.pagination li span{position:relative;float:left;padding:6px 12px;margin-left:-1px;text-decoration:none;background:#fff;border-top:2px solid transparent;color:#ababab}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;cursor:default;border-top-color:#000;color:#000;background:#fff}.pagination .disabled>span,.pagination .disabled>span:hover,.pagination .disabled>span:focus,.pagination .disabled>a,.pagination .disabled>a:hover,.pagination .disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}
/*!
angular-xeditable - 0.1.8
Edit-in-place for angular.js
Build date: 2014-01-10
*/
/**
* Angular-xeditable module
*
*/
angular.module('xeditable', [])
/**
* Default options.
*
* @namespace editable-options
*/
//todo: maybe better have editableDefaults, not options...
.value('editableOptions', {
/**
* Theme. Possible values `bs3`, `bs2`, `default`.
*
* @var {string} theme
* @memberOf editable-options
*/
theme: 'default',
/**
* Whether to show buttons for single editalbe element.
* Possible values `right` (default), `no`.
*
* @var {string} buttons
* @memberOf editable-options
*/
buttons: 'right',
/**
* Default value for `blur` attribute of single editable element.
* Can be `cancel|submit|ignore`.
*
* @var {string} blurElem
* @memberOf editable-options
*/
blurElem: 'cancel',
/**
* Default value for `blur` attribute of editable form.
* Can be `cancel|submit|ignore`.
*
* @var {string} blurForm
* @memberOf editable-options
*/
blurForm: 'ignore',
/**
* How input elements get activated. Possible values: `focus|select|none`.
*
* @var {string} activate
* @memberOf editable-options
*/
activate: 'focus'
});
/*
Angular-ui bootstrap datepicker
http://angular-ui.github.io/bootstrap/#/datepicker
*/
angular.module('xeditable').directive('editableBsdate', ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableBsdate',
inputTpl: '<input type="text">'
});
}]);
/*
Angular-ui bootstrap editable timepicker
http://angular-ui.github.io/bootstrap/#/timepicker
*/
angular.module('xeditable').directive('editableBstime', ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableBstime',
inputTpl: '<timepicker></timepicker>',
render: function() {
this.parent.render.call(this);
// timepicker can't update model when ng-model set directly to it
// see: https://github.com/angular-ui/bootstrap/issues/1141
// so we wrap it into DIV
var div = angular.element('<div class="well well-small" style="display:inline-block;"></div>');
// move ng-model to wrapping div
div.attr('ng-model', this.inputEl.attr('ng-model'));
this.inputEl.removeAttr('ng-model');
// move ng-change to wrapping div
if(this.attrs.eNgChange) {
div.attr('ng-change', this.inputEl.attr('ng-change'));
this.inputEl.removeAttr('ng-change');
}
// wrap
this.inputEl.wrap(div);
}
});
}]);
//checkbox
angular.module('xeditable').directive('editableCheckbox', ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableCheckbox',
inputTpl: '<input type="checkbox">',
render: function() {
this.parent.render.call(this);
if(this.attrs.eTitle) {
this.inputEl.wrap('<label></label>');
this.inputEl.after(angular.element('<span></span>').text(this.attrs.eTitle));
}
},
autosubmit: function() {
var self = this;
self.inputEl.bind('change', function() {
setTimeout(function() {
self.scope.$apply(function() {
self.scope.$form.$submit();
});
}, 500);
});
}
});
}]);
// checklist
angular.module('xeditable').directive('editableChecklist', [
'editableDirectiveFactory',
'editableNgOptionsParser',
function(editableDirectiveFactory, editableNgOptionsParser) {
return editableDirectiveFactory({
directiveName: 'editableChecklist',
inputTpl: '<span></span>',
useCopy: true,
render: function() {
this.parent.render.call(this);
var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
var html = '<label ng-repeat="'+parsed.ngRepeat+'">'+
'<input type="checkbox" checklist-model="$parent.$data" checklist-value="'+parsed.locals.valueFn+'">'+
'<span ng-bind="'+parsed.locals.displayFn+'"></span></label>';
this.inputEl.removeAttr('ng-model');
this.inputEl.removeAttr('ng-options');
this.inputEl.html(html);
}
});
}]);
/*
Input types: text|email|tel|number|url|search|color|date|datetime|time|month|week
*/
(function() {
var types = 'text|email|tel|number|url|search|color|date|datetime|time|month|week'.split('|');
//todo: datalist
// generate directives
angular.forEach(types, function(type) {
var directiveName = 'editable'+type.charAt(0).toUpperCase() + type.slice(1);
angular.module('xeditable').directive(directiveName, ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: directiveName,
inputTpl: '<input type="'+type+'">'
});
}]);
});
//`range` is bit specific
angular.module('xeditable').directive('editableRange', ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableRange',
inputTpl: '<input type="range" id="range" name="range">',
render: function() {
this.parent.render.call(this);
this.inputEl.after('<output>{{$data}}</output>');
}
});
}]);
}());
// radiolist
angular.module('xeditable').directive('editableRadiolist', [
'editableDirectiveFactory',
'editableNgOptionsParser',
function(editableDirectiveFactory, editableNgOptionsParser) {
return editableDirectiveFactory({
directiveName: 'editableRadiolist',
inputTpl: '<span></span>',
render: function() {
this.parent.render.call(this);
var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
var html = '<label ng-repeat="'+parsed.ngRepeat+'">'+
'<input type="radio" ng-model="$parent.$data" value="{{'+parsed.locals.valueFn+'}}">'+
'<span ng-bind="'+parsed.locals.displayFn+'"></span></label>';
this.inputEl.removeAttr('ng-model');
this.inputEl.removeAttr('ng-options');
this.inputEl.html(html);
},
autosubmit: function() {
var self = this;
self.inputEl.bind('change', function() {
setTimeout(function() {
self.scope.$apply(function() {
self.scope.$form.$submit();
});
}, 500);
});
}
});
}]);
//select
angular.module('xeditable').directive('editableSelect', ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableSelect',
inputTpl: '<select></select>',
autosubmit: function() {
var self = this;
self.inputEl.bind('change', function() {
self.scope.$apply(function() {
self.scope.$form.$submit();
});
});
}
});
}]);
//textarea
angular.module('xeditable').directive('editableTextarea', ['editableDirectiveFactory',
function(editableDirectiveFactory) {
return editableDirectiveFactory({
directiveName: 'editableTextarea',
inputTpl: '<textarea></textarea>',
addListeners: function() {
var self = this;
self.parent.addListeners.call(self);
// submit textarea by ctrl+enter even with buttons
if (self.single && self.buttons !== 'no') {
self.autosubmit();
}
},
autosubmit: function() {
var self = this;
self.inputEl.bind('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && (e.keyCode === 13)) {
self.scope.$apply(function() {
self.scope.$form.$submit();
});
}
});
}
});
}]);
/**
* EditableController class.
* Attached to element with `editable-xxx` directive.
*
* @namespace editable-element
*/
/*
TODO: this file should be refactored to work more clear without closures!
*/
angular.module('xeditable').factory('editableController',
['$q', 'editableUtils',
function($q, editableUtils) {
//EditableController function
EditableController.$inject = ['$scope', '$attrs', '$element', '$parse', 'editableThemes', 'editableOptions', '$rootScope', '$compile', '$q'];
function EditableController($scope, $attrs, $element, $parse, editableThemes, editableOptions, $rootScope, $compile, $q) {
var valueGetter;
//if control is disabled - it does not participate in waiting process
var inWaiting;
var self = this;
self.scope = $scope;
self.elem = $element;
self.attrs = $attrs;
self.inputEl = null;
self.editorEl = null;
self.single = true;
self.error = '';
self.theme = editableThemes[editableOptions.theme] || editableThemes['default'];
self.parent = {};
//to be overwritten by directive
self.inputTpl = '';
self.directiveName = '';
// with majority of controls copy is not needed, but..
// copy MUST NOT be used for `select-multiple` with objects as items
// copy MUST be used for `checklist`
self.useCopy = false;
//runtime (defaults)
self.single = null;
/**
* Attributes defined with `e-*` prefix automatically transfered from original element to
* control.
* For example, if you set `<span editable-text="user.name" e-style="width: 100px"`>
* then input will appear as `<input style="width: 100px">`.
* See [demo](#text-customize).
*
* @var {any|attribute} e-*
* @memberOf editable-element
*/
/**
* Whether to show ok/cancel buttons. Values: `right|no`.
* If set to `no` control automatically submitted when value changed.
* If control is part of form buttons will never be shown.
*
* @var {string|attribute} buttons
* @memberOf editable-element
*/
self.buttons = 'right';
/**
* Action when control losses focus. Values: `cancel|submit|ignore`.
* Has sense only for single editable element.
* Otherwise, if control is part of form - you should set `blur` of form, not of individual element.
*
* @var {string|attribute} blur
* @memberOf editable-element
*/
// no real `blur` property as it is transfered to editable form
//init
self.init = function(single) {
self.single = single;
self.name = $attrs.eName || $attrs[self.directiveName];
/*
if(!$attrs[directiveName] && !$attrs.eNgModel && ($attrs.eValue === undefined)) {
throw 'You should provide value for `'+directiveName+'` or `e-value` in editable element!';
}
*/
if($attrs[self.directiveName]) {
valueGetter = $parse($attrs[self.directiveName]);
} else {
throw 'You should provide value for `'+self.directiveName+'` in editable element!';
}
// settings for single and non-single
if (!self.single) {
// hide buttons for non-single
self.buttons = 'no';
} else {
self.buttons = self.attrs.buttons || editableOptions.buttons;
}
//if name defined --> watch changes and update $data in form
if($attrs.eName) {
self.scope.$watch('$data', function(newVal){
self.scope.$form.$data[$attrs.eName] = newVal;
});
}
/**
* Called when control is shown.
* See [demo](#select-remote).
*
* @var {method|attribute} onshow
* @memberOf editable-element
*/
if($attrs.onshow) {
self.onshow = function() {
return self.catchError($parse($attrs.onshow)($scope));
};
}
/**
* Called when control is hidden after both save or cancel.
*
* @var {method|attribute} onhide
* @memberOf editable-element
*/
if($attrs.onhide) {
self.onhide = function() {
return $parse($attrs.onhide)($scope);
};
}
/**
* Called when control is cancelled.
*
* @var {method|attribute} oncancel
* @memberOf editable-element
*/
if($attrs.oncancel) {
self.oncancel = function() {
return $parse($attrs.oncancel)($scope);
};
}
/**
* Called during submit before value is saved to model.
* See [demo](#onbeforesave).
*
* @var {method|attribute} onbeforesave
* @memberOf editable-element
*/
if ($attrs.onbeforesave) {
self.onbeforesave = function() {
return self.catchError($parse($attrs.onbeforesave)($scope));
};
}
/**
* Called during submit after value is saved to model.
* See [demo](#onaftersave).
*
* @var {method|attribute} onaftersave
* @memberOf editable-element
*/
if ($attrs.onaftersave) {
self.onaftersave = function() {
return self.catchError($parse($attrs.onaftersave)($scope));
};
}
// watch change of model to update editable element
// now only add/remove `editable-empty` class.
// Initially this method called with newVal = undefined, oldVal = undefined
// so no need initially call handleEmpty() explicitly
$scope.$parent.$watch($attrs[self.directiveName], function(newVal, oldVal) {
self.handleEmpty();
});
};
self.render = function() {
var theme = self.theme;
//build input
self.inputEl = angular.element(self.inputTpl);
//build controls
self.controlsEl = angular.element(theme.controlsTpl);
self.controlsEl.append(self.inputEl);
//build buttons
if(self.buttons !== 'no') {
self.buttonsEl = angular.element(theme.buttonsTpl);
self.submitEl = angular.element(theme.submitTpl);
self.cancelEl = angular.element(theme.cancelTpl);
self.buttonsEl.append(self.submitEl).append(self.cancelEl);
self.controlsEl.append(self.buttonsEl);
self.inputEl.addClass('editable-has-buttons');
}
//build error
self.errorEl = angular.element(theme.errorTpl);
self.controlsEl.append(self.errorEl);
//build editor
self.editorEl = angular.element(self.single ? theme.formTpl : theme.noformTpl);
self.editorEl.append(self.controlsEl);
// transfer `e-*|data-e-*|x-e-*` attributes
for(var k in $attrs.$attr) {
if(k.length <= 1) {
continue;
}
var transferAttr = false;
var nextLetter = k.substring(1, 2);
// if starts with `e` + uppercase letter
if(k.substring(0, 1) === 'e' && nextLetter === nextLetter.toUpperCase()) {
transferAttr = k.substring(1); // cut `e`
} else {
continue;
}
// exclude `form` and `ng-submit`,
if(transferAttr === 'Form' || transferAttr === 'NgSubmit') {
continue;
}
// convert back to lowercase style
transferAttr = transferAttr.substring(0, 1).toLowerCase() + editableUtils.camelToDash(transferAttr.substring(1));
// workaround for attributes without value (e.g. `multiple = "multiple"`)
var attrValue = ($attrs[k] === '') ? transferAttr : $attrs[k];
// set attributes to input
self.inputEl.attr(transferAttr, attrValue);
}
self.inputEl.addClass('editable-input');
self.inputEl.attr('ng-model', '$data');
// add directiveName class to editor, e.g. `editable-text`
self.editorEl.addClass(editableUtils.camelToDash(self.directiveName));
if(self.single) {
self.editorEl.attr('editable-form', '$form');
// transfer `blur` to form
self.editorEl.attr('blur', self.attrs.blur || (self.buttons === 'no' ? 'cancel' : editableOptions.blurElem));
}
//apply `postrender` method of theme
if(angular.isFunction(theme.postrender)) {
theme.postrender.call(self);
}
};
// with majority of controls copy is not needed, but..
// copy MUST NOT be used for `select-multiple` with objects as items
// copy MUST be used for `checklist`
self.setLocalValue = function() {
self.scope.$data = self.useCopy ?
angular.copy(valueGetter($scope.$parent)) :
valueGetter($scope.$parent);
};
//show
self.show = function() {
// set value of scope.$data
self.setLocalValue();
/*
Originally render() was inside init() method, but some directives polluting editorEl,
so it is broken on second openning.
Cloning is not a solution as jqLite can not clone with event handler's.
*/
self.render();
// insert into DOM
$element.after(self.editorEl);
// compile (needed to attach ng-* events from markup)
$compile(self.editorEl)($scope);
// attach listeners (`escape`, autosubmit, etc)
self.addListeners();
// hide element
$element.addClass('editable-hide');
// onshow
return self.onshow();
};
//hide
self.hide = function() {
self.editorEl.remove();
$element.removeClass('editable-hide');
// onhide
return self.onhide();
};
// cancel
self.cancel = function() {
// oncancel
self.oncancel();
// don't call hide() here as it called in form's code
};
/*
Called after show to attach listeners
*/
self.addListeners = function() {
// bind keyup for `escape`
self.inputEl.bind('keyup', function(e) {
if(!self.single) {
return;
}
// todo: move this to editable-form!
switch(e.keyCode) {
// hide on `escape` press
case 27:
self.scope.$apply(function() {
self.scope.$form.$cancel();
});
break;
}
});
// autosubmit when `no buttons`
if (self.single && self.buttons === 'no') {
self.autosubmit();
}
// click - mark element as clicked to exclude in document click handler
self.editorEl.bind('click', function(e) {
// ignore right/middle button click
if (e.which !== 1) {
return;
}
if (self.scope.$form.$visible) {
self.scope.$form._clicked = true;
}
});
};
// setWaiting
self.setWaiting = function(value) {
if (value) {
// participate in waiting only if not disabled
inWaiting = !self.inputEl.attr('disabled') &&
!self.inputEl.attr('ng-disabled') &&
!self.inputEl.attr('ng-enabled');
if (inWaiting) {
self.inputEl.attr('disabled', 'disabled');
if(self.buttonsEl) {
self.buttonsEl.find('button').attr('disabled', 'disabled');
}
}
} else {
if (inWaiting) {
self.inputEl.removeAttr('disabled');
if (self.buttonsEl) {
self.buttonsEl.find('button').removeAttr('disabled');
}
}
}
};
self.activate = function() {
setTimeout(function() {
var el = self.inputEl[0];
if (editableOptions.activate === 'focus' && el.focus) {
el.focus();
}
if (editableOptions.activate === 'select' && el.select) {
el.select();
}
}, 0);
};
self.setError = function(msg) {
if(!angular.isObject(msg)) {
$scope.$error = msg;
self.error = msg;
}
};
/*
Checks that result is string or promise returned string and shows it as error message
Applied to onshow, onbeforesave, onaftersave
*/
self.catchError = function(result, noPromise) {
if (angular.isObject(result) && noPromise !== true) {
$q.when(result).then(
//success and fail handlers are equal
angular.bind(this, function(r) {
this.catchError(r, true);
}),
angular.bind(this, function(r) {
this.catchError(r, true);
})
);
//check $http error
} else if (noPromise && angular.isObject(result) && result.status &&
(result.status !== 200) && result.data && angular.isString(result.data)) {
this.setError(result.data);
//set result to string: to let form know that there was error
result = result.data;
} else if (angular.isString(result)) {
this.setError(result);
}
return result;
};
self.save = function() {
valueGetter.assign($scope.$parent, angular.copy(self.scope.$data));
// no need to call handleEmpty here as we are watching change of model value
// self.handleEmpty();
};
/*
attach/detach `editable-empty` class to element
*/
self.handleEmpty = function() {
var val = valueGetter($scope.$parent);
var isEmpty = val === null || val === undefined || val === "" || (angular.isArray(val) && val.length === 0);
$element.toggleClass('editable-empty', isEmpty);
};
/*
Called when `buttons = "no"` to submit automatically
*/
self.autosubmit = angular.noop;
self.onshow = angular.noop;
self.onhide = angular.noop;
self.oncancel = angular.noop;
self.onbeforesave = angular.noop;
self.onaftersave = angular.noop;
}
return EditableController;
}]);
/*
editableFactory is used to generate editable directives (see `/directives` folder)
Inside it does several things:
- detect form for editable element. Form may be one of three types:
1. autogenerated form (for single editable elements)
2. wrapper form (element wrapped by <form> tag)
3. linked form (element has `e-form` attribute pointing to existing form)
- attach editableController to element
Depends on: editableController, editableFormFactory
*/
angular.module('xeditable').factory('editableDirectiveFactory',
['$parse', '$compile', 'editableThemes', '$rootScope', '$document', 'editableController', 'editableFormController',
function($parse, $compile, editableThemes, $rootScope, $document, editableController, editableFormController) {
//directive object
return function(overwrites) {
return {
restrict: 'A',
scope: true,
require: [overwrites.directiveName, '?^form'],
controller: editableController,
link: function(scope, elem, attrs, ctrl) {
// editable controller
var eCtrl = ctrl[0];
// form controller
var eFormCtrl;
// this variable indicates is element is bound to some existing form,
// or it's single element who's form will be generated automatically
// By default consider single element without any linked form.ß
var hasForm = false;
// element wrapped by form
if(ctrl[1]) {
eFormCtrl = ctrl[1];
hasForm = true;
} else if(attrs.eForm) { // element not wrapped by <form>, but we hane `e-form` attr
var getter = $parse(attrs.eForm)(scope);
if(getter) { // form exists in scope (above), e.g. editable column
eFormCtrl = getter;
hasForm = true;
} else { // form exists below or not exist at all: check document.forms
for(var i=0; i<$document[0].forms.length;i++){
if($document[0].forms[i].name === attrs.eForm) {
// form is below and not processed yet
eFormCtrl = null;
hasForm = true;
break;
}
}
}
}
/*
if(hasForm && !attrs.eName) {
throw 'You should provide `e-name` for editable element inside form!';
}
*/
//check for `editable-form` attr in form
/*
if(eFormCtrl && ) {
throw 'You should provide `e-name` for editable element inside form!';
}
*/
// store original props to `parent` before merge
angular.forEach(overwrites, function(v, k) {
if(eCtrl[k] !== undefined) {
eCtrl.parent[k] = eCtrl[k];
}
});
// merge overwrites to base editable controller
angular.extend(eCtrl, overwrites);
// init editable ctrl
eCtrl.init(!hasForm);
// publich editable controller as `$editable` to be referenced in html
scope.$editable = eCtrl;
// add `editable` class to element
elem.addClass('editable');
// hasForm
if(hasForm) {
if(eFormCtrl) {
scope.$form = eFormCtrl;
if(!scope.$form.$addEditable) {
throw 'Form with editable elements should have `editable-form` attribute.';
}
scope.$form.$addEditable(eCtrl);
} else {
// future form (below): add editable controller to buffer and add to form later
$rootScope.$$editableBuffer = $rootScope.$$editableBuffer || {};
$rootScope.$$editableBuffer[attrs.eForm] = $rootScope.$$editableBuffer[attrs.eForm] || [];
$rootScope.$$editableBuffer[attrs.eForm].push(eCtrl);
scope.$form = null; //will be re-assigned later
}
// !hasForm
} else {
// create editableform controller
scope.$form = editableFormController();
// add self to editable controller
scope.$form.$addEditable(eCtrl);
// if `e-form` provided, publish local $form in scope
if(attrs.eForm) {
scope.$parent[attrs.eForm] = scope.$form;
}
// bind click - if no external form defined
if(!attrs.eForm) {
elem.addClass('editable-click');
elem.bind('click', function(e) {
e.preventDefault();
e.editable = eCtrl;
scope.$apply(function(){
scope.$form.$show();
});
});
}
}
}
};
};
}]);
/*
Returns editableForm controller
*/
angular.module('xeditable').factory('editableFormController',
['$parse', '$document', '$rootScope', 'editablePromiseCollection', 'editableUtils',
function($parse, $document, $rootScope, editablePromiseCollection, editableUtils) {
// array of opened editable forms
var shown = [];
// bind click to body: cancel|submit|ignore forms
$document.bind('click', function(e) {
// ignore right/middle button click
if (e.which !== 1) {
return;
}
var toCancel = [];
var toSubmit = [];
for (var i=0; i<shown.length; i++) {
// exclude clicked
if (shown[i]._clicked) {
shown[i]._clicked = false;
continue;
}
// exclude waiting
if (shown[i].$waiting) {
continue;
}
if (shown[i]._blur === 'cancel') {
toCancel.push(shown[i]);
}
if (shown[i]._blur === 'submit') {
toSubmit.push(shown[i]);
}
}
if (toCancel.length || toSubmit.length) {
$rootScope.$apply(function() {
angular.forEach(toCancel, function(v){ v.$cancel(); });
angular.forEach(toSubmit, function(v){ v.$submit(); });
});
}
});
var base = {
$addEditable: function(editable) {
//console.log('add editable', editable.elem, editable.elem.bind);
this.$editables.push(editable);
//'on' is not supported in angular 1.0.8
editable.elem.bind('$destroy', angular.bind(this, this.$removeEditable, editable));
//bind editable's local $form to self (if not bound yet, below form)
if (!editable.scope.$form) {
editable.scope.$form = this;
}
//if form already shown - call show() of new editable
if (this.$visible) {
editable.catchError(editable.show());
}
},
$removeEditable: function(editable) {
//arrayRemove
for(var i=0; i < this.$editables.length; i++) {
if(this.$editables[i] === editable) {
this.$editables.splice(i, 1);
return;
}
}
},
/**
* Shows form with editable controls.
*
* @method $show()
* @memberOf editable-form
*/
$show: function() {
if (this.$visible) {
return;
}
this.$visible = true;
var pc = editablePromiseCollection();
//own show
pc.when(this.$onshow());
//clear errors
this.$setError(null, '');
//children show
angular.forEach(this.$editables, function(editable) {
pc.when(editable.show());
});
//wait promises and activate
pc.then({
onWait: angular.bind(this, this.$setWaiting),
onTrue: angular.bind(this, this.$activate),
onFalse: angular.bind(this, this.$activate),
onString: angular.bind(this, this.$activate)
});
// add to internal list of shown forms
// setTimeout needed to prevent closing right after opening (e.g. when trigger by button)
setTimeout(angular.bind(this, function() {
// clear `clicked` to get ready for clicks on visible form
this._clicked = false;
if(editableUtils.indexOf(shown, this) === -1) {
shown.push(this);
}
}), 0);
},
/**
* Sets focus on form field specified by `name`.
*
* @method $activate(name)
* @param {string} name name of field
* @memberOf editable-form
*/
$activate: function(name) {
var i;
if (this.$editables.length) {
//activate by name
if (angular.isString(name)) {
for(i=0; i<this.$editables.length; i++) {
if (this.$editables[i].name === name) {
this.$editables[i].activate();
return;
}
}
}
//try activate error field
for(i=0; i<this.$editables.length; i++) {
if (this.$editables[i].error) {
this.$editables[i].activate();
return;
}
}
//by default activate first field
this.$editables[0].activate();
}
},
/**
* Hides form with editable controls without saving.
*
* @method $hide()
* @memberOf editable-form
*/
$hide: function() {
if (!this.$visible) {
return;
}
this.$visible = false;
// self hide
this.$onhide();
// children's hide
angular.forEach(this.$editables, function(editable) {
editable.hide();
});
// remove from internal list of shown forms
editableUtils.arrayRemove(shown, this);
},
/**
* Triggers `oncancel` event and calls `$hide()`.
*
* @method $cancel()
* @memberOf editable-form
*/
$cancel: function() {
if (!this.$visible) {
return;
}
// self cancel
this.$oncancel();
// children's cancel
angular.forEach(this.$editables, function(editable) {
editable.cancel();
});
// self hide
this.$hide();
},
$setWaiting: function(value) {
this.$waiting = !!value;
// we can't just set $waiting variable and use it via ng-disabled in children
// because in editable-row form is not accessible
angular.forEach(this.$editables, function(editable) {
editable.setWaiting(!!value);
});
},
/**
* Shows error message for particular field.
*
* @method $setError(name, msg)
* @param {string} name name of field
* @param {string} msg error message
* @memberOf editable-form
*/
$setError: function(name, msg) {
angular.forEach(this.$editables, function(editable) {
if(!name || editable.name === name) {
editable.setError(msg);
}
});
},
$submit: function() {
if (this.$waiting) {
return;
}
//clear errors
this.$setError(null, '');
//children onbeforesave
var pc = editablePromiseCollection();
angular.forEach(this.$editables, function(editable) {
pc.when(editable.onbeforesave());
});
/*
onbeforesave result:
- true/undefined: save data and close form
- false: close form without saving
- string: keep form open and show error
*/
pc.then({
onWait: angular.bind(this, this.$setWaiting),
onTrue: angular.bind(this, checkSelf, true),
onFalse: angular.bind(this, checkSelf, false),
onString: angular.bind(this, this.$activate)
});
//save
function checkSelf(childrenTrue){
var pc = editablePromiseCollection();
pc.when(this.$onbeforesave());
pc.then({
onWait: angular.bind(this, this.$setWaiting),
onTrue: childrenTrue ? angular.bind(this, this.$save) : angular.bind(this, this.$hide),
onFalse: angular.bind(this, this.$hide),
onString: angular.bind(this, this.$activate)
});
}
},
$save: function() {
// write model for each editable
angular.forEach(this.$editables, function(editable) {
editable.save();
});
//call onaftersave of self and children
var pc = editablePromiseCollection();
pc.when(this.$onaftersave());
angular.forEach(this.$editables, function(editable) {
pc.when(editable.onaftersave());
});
/*
onaftersave result:
- true/undefined/false: just close form
- string: keep form open and show error
*/
pc.then({
onWait: angular.bind(this, this.$setWaiting),
onTrue: angular.bind(this, this.$hide),
onFalse: angular.bind(this, this.$hide),
onString: angular.bind(this, this.$activate)
});
},
$onshow: angular.noop,
$oncancel: angular.noop,
$onhide: angular.noop,
$onbeforesave: angular.noop,
$onaftersave: angular.noop
};
return function() {
return angular.extend({
$editables: [],
/**
* Form visibility flag.
*
* @var {bool} $visible
* @memberOf editable-form
*/
$visible: false,
/**
* Form waiting flag. It becomes `true` when form is loading or saving data.
*
* @var {bool} $waiting
* @memberOf editable-form
*/
$waiting: false,
$data: {},
_clicked: false,
_blur: null
}, base);
};
}]);
/**
* EditableForm directive. Should be defined in <form> containing editable controls.
* It add some usefull methods to form variable exposed to scope by `name="myform"` attribute.
*
* @namespace editable-form
*/
angular.module('xeditable').directive('editableForm',
['$rootScope', '$parse', 'editableFormController', 'editableOptions',
function($rootScope, $parse, editableFormController, editableOptions) {
return {
restrict: 'A',
require: ['form'],
//require: ['form', 'editableForm'],
//controller: EditableFormController,
compile: function() {
return {
pre: function(scope, elem, attrs, ctrl) {
var form = ctrl[0];
var eForm;
//if `editableForm` has value - publish smartly under this value
//this is required only for single editor form that is created and removed
if(attrs.editableForm) {
if(scope[attrs.editableForm] && scope[attrs.editableForm].$show) {
eForm = scope[attrs.editableForm];
angular.extend(form, eForm);
} else {
eForm = editableFormController();
scope[attrs.editableForm] = eForm;
angular.extend(eForm, form);
}
} else { //just merge to form and publish if form has name
eForm = editableFormController();
angular.extend(form, eForm);
}
//read editables from buffer (that appeared before FORM tag)
var buf = $rootScope.$$editableBuffer;
var name = form.$name;
if(name && buf && buf[name]) {
angular.forEach(buf[name], function(editable) {
eForm.$addEditable(editable);
});
delete buf[name];
}
},
post: function(scope, elem, attrs, ctrl) {
var eForm;
if(attrs.editableForm && scope[attrs.editableForm] && scope[attrs.editableForm].$show) {
eForm = scope[attrs.editableForm];
} else {
eForm = ctrl[0];
}
/**
* Called when form is shown.
*
* @var {method|attribute} onshow
* @memberOf editable-form
*/
if(attrs.onshow) {
eForm.$onshow = angular.bind(eForm, $parse(attrs.onshow), scope);
}
/**
* Called when form hides after both save or cancel.
*
* @var {method|attribute} onhide
* @memberOf editable-form
*/
if(attrs.onhide) {
eForm.$onhide = angular.bind(eForm, $parse(attrs.onhide), scope);
}
/**
* Called when form is cancelled.
*
* @var {method|attribute} oncancel
* @memberOf editable-form
*/
if(attrs.oncancel) {
eForm.$oncancel = angular.bind(eForm, $parse(attrs.oncancel), scope);
}
/**
* Whether form initially rendered in shown state.
*
* @var {bool|attribute} shown
* @memberOf editable-form
*/
if(attrs.shown && $parse(attrs.shown)(scope)) {
eForm.$show();
}
/**
* Action when form losses focus. Values: `cancel|submit|ignore`.
* Default is `ignore`.
*
* @var {string|attribute} blur
* @memberOf editable-form
*/
eForm._blur = attrs.blur || editableOptions.blurForm;
// onbeforesave, onaftersave
if(!attrs.ngSubmit && !attrs.submit) {
/**
* Called after all children `onbeforesave` callbacks but before saving form values
* to model.
* If at least one children callback returns `non-string` - it will not not be called.
* See [editable-form demo](#editable-form) for details.
*
* @var {method|attribute} onbeforesave
* @memberOf editable-form
*
*/
if(attrs.onbeforesave) {
eForm.$onbeforesave = function() {
return $parse(attrs.onbeforesave)(scope, {$data: eForm.$data});
};
}
/**
* Called when form values are saved to model.
* See [editable-form demo](#editable-form) for details.
*
* @var {method|attribute} onaftersave
* @memberOf editable-form
*
*/
if(attrs.onaftersave) {
eForm.$onaftersave = function() {
return $parse(attrs.onaftersave)(scope, {$data: eForm.$data});
};
}
elem.bind('submit', function(event) {
event.preventDefault();
scope.$apply(function() {
eForm.$submit();
});
});
}
// click - mark form as clicked to exclude in document click handler
elem.bind('click', function(e) {
// ignore right/middle button click
if (e.which !== 1) {
return;
}
if (eForm.$visible) {
eForm._clicked = true;
}
});
}
};
}
};
}]);
/**
* editablePromiseCollection
*
* Collect results of function calls. Shows waiting if there are promises.
* Finally, applies callbacks if:
* - onTrue(): all results are true and all promises resolved to true
* - onFalse(): at least one result is false or promise resolved to false
* - onString(): at least one result is string or promise rejected or promise resolved to string
*/
angular.module('xeditable').factory('editablePromiseCollection', ['$q', function($q) {
function promiseCollection() {
return {
promises: [],
hasFalse: false,
hasString: false,
when: function(result, noPromise) {
if (result === false) {
this.hasFalse = true;
} else if (!noPromise && angular.isObject(result)) {
this.promises.push($q.when(result));
} else if (angular.isString(result)){
this.hasString = true;
} else { //result === true || result === undefined || result === null
return;
}
},
//callbacks: onTrue, onFalse, onString
then: function(callbacks) {
callbacks = callbacks || {};
var onTrue = callbacks.onTrue || angular.noop;
var onFalse = callbacks.onFalse || angular.noop;
var onString = callbacks.onString || angular.noop;
var onWait = callbacks.onWait || angular.noop;
var self = this;
if (this.promises.length) {
onWait(true);
$q.all(this.promises).then(
//all resolved
function(results) {
onWait(false);
//check all results via same `when` method (without checking promises)
angular.forEach(results, function(result) {
self.when(result, true);
});
applyCallback();
},
//some rejected
function(error) {
onWait(false);
onString();
}
);
} else {
applyCallback();
}
function applyCallback() {
if (!self.hasString && !self.hasFalse) {
onTrue();
} else if (!self.hasString && self.hasFalse) {
onFalse();
} else {
onString();
}
}
}
};
}
return promiseCollection;
}]);
/**
* editableUtils
*/
angular.module('xeditable').factory('editableUtils', [function() {
return {
indexOf: function (array, obj) {
if (array.indexOf) return array.indexOf(obj);
for ( var i = 0; i < array.length; i++) {
if (obj === array[i]) return i;
}
return -1;
},
arrayRemove: function (array, value) {
var index = this.indexOf(array, value);
if (index >= 0) {
array.splice(index, 1);
}
return value;
},
// copy from https://github.com/angular/angular.js/blob/master/src/Angular.js
camelToDash: function(str) {
var SNAKE_CASE_REGEXP = /[A-Z]/g;
return str.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? '-' : '') + letter.toLowerCase();
});
},
dashToCamel: function(str) {
var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
var MOZ_HACK_REGEXP = /^moz([A-Z])/;
return str.
replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter;
}).
replace(MOZ_HACK_REGEXP, 'Moz$1');
}
};
}]);
/**
* editableNgOptionsParser
*
* see: https://github.com/angular/angular.js/blob/master/src/ng/directive/select.js#L131
*/
angular.module('xeditable').factory('editableNgOptionsParser', [
function() {
//0000111110000000000022220000000000000000000000333300000000000000444444444444444000000000555555555555555000000066666666666666600000000000000007777000000000000000000088888
var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/;
function parser(optionsExp) {
var match;
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
throw 'ng-options parse error';
}
var
displayFn = match[2] || match[1],
valueName = match[4] || match[6],
keyName = match[5],
groupByFn = match[3] || '',
valueFn = match[2] ? match[1] : valueName,
valuesFn = match[7],
track = match[8],
trackFn = track ? match[8] : null;
var ngRepeat;
if (keyName === undefined) { // array
ngRepeat = valueName + ' in ' + valuesFn;
if (track !== undefined) {
ngRepeat += ' track by '+trackFn;
}
} else { // object
ngRepeat = '('+keyName+', '+valueName+') in '+valuesFn;
}
// group not supported yet
return {
ngRepeat: ngRepeat,
locals: {
valueName: valueName,
keyName: keyName,
valueFn: valueFn,
displayFn: displayFn
}
};
}
return parser;
}]);
/*
Editable themes:
- default
- bootstrap 2
- bootstrap 3
Note: in postrender() `this` is instance of editableController
*/
angular.module('xeditable').factory('editableThemes', function() {
var themes = {
//default
'default': {
formTpl: '<form class="editable-wrap"></form>',
noformTpl: '<span class="editable-wrap"></span>',
controlsTpl: '<span class="editable-controls"></span>',
inputTpl: '',
errorTpl: '<div class="editable-error" ng-show="$error" ng-bind="$error"></div>',
buttonsTpl: '<span class="editable-buttons"></span>',
submitTpl: '<button type="submit">save</button>',
cancelTpl: '<button type="button" ng-click="$form.$cancel()">cancel</button>'
},
//bs2
'bs2': {
formTpl: '<form class="form-inline editable-wrap" role="form"></form>',
noformTpl: '<span class="editable-wrap"></span>',
controlsTpl: '<div class="editable-controls controls control-group" ng-class="{\'error\': $error}"></div>',
inputTpl: '',
errorTpl: '<div class="editable-error help-block" ng-show="$error" ng-bind="$error"></div>',
buttonsTpl: '<span class="editable-buttons"></span>',
submitTpl: '<button type="submit" class="btn btn-primary"><span class="icon-ok icon-white"></span></button>',
cancelTpl: '<button type="button" class="btn" ng-click="$form.$cancel()">'+
'<span class="icon-remove"></span>'+
'</button>'
},
//bs3
'bs3': {
formTpl: '<form class="form-inline editable-wrap" role="form"></form>',
noformTpl: '<span class="editable-wrap"></span>',
controlsTpl: '<div class="editable-controls form-group" ng-class="{\'has-error\': $error}"></div>',
inputTpl: '',
errorTpl: '<div class="editable-error help-block" ng-show="$error" ng-bind="$error"></div>',
buttonsTpl: '<span class="editable-buttons"></span>',
submitTpl: '<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-ok"></span></button>',
cancelTpl: '<button type="button" class="btn btn-default" ng-click="$form.$cancel()">'+
'<span class="glyphicon glyphicon-remove"></span>'+
'</button>',
//bs3 specific prop to change buttons class: btn-sm, btn-lg
buttonsClass: '',
//bs3 specific prop to change standard inputs class: input-sm, input-lg
inputClass: '',
postrender: function() {
//apply `form-control` class to std inputs
switch(this.directiveName) {
case 'editableText':
case 'editableSelect':
case 'editableTextarea':
case 'editableEmail':
case 'editableTel':
case 'editableNumber':
case 'editableUrl':
case 'editableSearch':
case 'editableDate':
case 'editableDatetime':
case 'editableTime':
case 'editableMonth':
case 'editableWeek':
this.inputEl.addClass('form-control');
if(this.theme.inputClass) {
// don`t apply `input-sm` and `input-lg` to select multiple
// should be fixed in bs itself!
if(this.inputEl.attr('multiple') &&
(this.theme.inputClass === 'input-sm' || this.theme.inputClass === 'input-lg')) {
break;
}
this.inputEl.addClass(this.theme.inputClass);
}
break;
}
//apply buttonsClass (bs3 specific!)
if(this.buttonsEl && this.theme.buttonsClass) {
this.buttonsEl.find('button').addClass(this.theme.buttonsClass);
}
}
}
};
return themes;
});