2 * Angular Material Data Table
3 * https://github.com/daniel-nagy/md-data-table
7 (function (window, angular, undefined) {
10 angular.module('md.table.templates', ['md-table-pagination.html', 'md-table-progress.html', 'arrow-up.svg', 'navigate-before.svg', 'navigate-first.svg', 'navigate-last.svg', 'navigate-next.svg']);
12 angular.module('md-table-pagination.html', []).run(['$templateCache', function($templateCache) {
13 $templateCache.put('md-table-pagination.html',
14 '<div class="page-select" ng-if="$pagination.showPageSelect()">\n' +
15 ' <div class="label">{{$pagination.label.page}}</div>\n' +
17 ' <md-select virtual-page-select total="{{$pagination.pages()}}" class="md-table-select" ng-model="$pagination.page" md-container-class="md-pagination-select" ng-change="$pagination.onPaginationChange()" ng-disabled="$pagination.disabled" aria-label="Page">\n' +
19 ' <md-option ng-repeat="page in $pageSelect.pages" ng-value="page">{{page}}</md-option>\n' +
24 '<div class="limit-select" ng-if="$pagination.limitOptions">\n' +
25 ' <div class="label">{{$pagination.label.rowsPerPage}}</div>\n' +
27 ' <md-select class="md-table-select" ng-model="$pagination.limit" md-container-class="md-pagination-select" ng-disabled="$pagination.disabled" aria-label="Rows" placeholder="{{ $pagination.limitOptions[0] }}">\n' +
28 ' <md-option ng-repeat="option in $pagination.limitOptions" ng-value="option.value ? $pagination.eval(option.value) : option">{{::option.label ? option.label : option}}</md-option>\n' +
32 '<div class="buttons">\n' +
33 ' <div class="label">{{$pagination.min()}} - {{$pagination.max()}} {{$pagination.label.of}} {{$pagination.total}}</div>\n' +
35 ' <md-button class="md-icon-button" type="button" ng-if="$pagination.showBoundaryLinks()" ng-click="$pagination.first()" ng-disabled="$pagination.disabled || !$pagination.hasPrevious()" aria-label="First">\n' +
36 ' <md-icon md-svg-icon="navigate-first.svg"></md-icon>\n' +
39 ' <md-button class="md-icon-button" type="button" ng-click="$pagination.previous()" ng-disabled="$pagination.disabled || !$pagination.hasPrevious()" aria-label="Previous">\n' +
40 ' <md-icon md-svg-icon="navigate-before.svg"></md-icon>\n' +
43 ' <md-button class="md-icon-button" type="button" ng-click="$pagination.next()" ng-disabled="$pagination.disabled || !$pagination.hasNext()" aria-label="Next">\n' +
44 ' <md-icon md-svg-icon="navigate-next.svg"></md-icon>\n' +
47 ' <md-button class="md-icon-button" type="button" ng-if="$pagination.showBoundaryLinks()" ng-click="$pagination.last()" ng-disabled="$pagination.disabled || !$pagination.hasNext()" aria-label="Last">\n' +
48 ' <md-icon md-svg-icon="navigate-last.svg"></md-icon>\n' +
53 angular.module('md-table-progress.html', []).run(['$templateCache', function($templateCache) {
54 $templateCache.put('md-table-progress.html',
56 ' <th colspan="{{columnCount()}}">\n' +
57 ' <md-progress-linear ng-show="deferred()" md-mode="indeterminate"></md-progress-linear>\n' +
62 angular.module('arrow-up.svg', []).run(['$templateCache', function($templateCache) {
63 $templateCache.put('arrow-up.svg',
64 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>');
67 angular.module('navigate-before.svg', []).run(['$templateCache', function($templateCache) {
68 $templateCache.put('navigate-before.svg',
69 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>');
72 angular.module('navigate-first.svg', []).run(['$templateCache', function($templateCache) {
73 $templateCache.put('navigate-first.svg',
74 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 6 v12 h2 v-12 h-2z M17.41 7.41L16 6l-6 6 6 6 1.41-1.41L12.83 12z"/></svg>');
77 angular.module('navigate-last.svg', []).run(['$templateCache', function($templateCache) {
78 $templateCache.put('navigate-last.svg',
79 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15 6 v12 h2 v-12 h-2z M8 6L6.59 7.41 11.17 12l-4.58 4.59L8 18l6-6z"/></svg>');
82 angular.module('navigate-next.svg', []).run(['$templateCache', function($templateCache) {
83 $templateCache.put('navigate-next.svg',
84 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>');
88 angular.module('md.data.table', ['md.table.templates']);
90 angular.module('md.data.table').directive('mdBody', mdBody);
94 function compile(tElement) {
95 tElement.addClass('md-body');
104 angular.module('md.data.table').directive('mdCell', mdCell);
108 function compile(tElement) {
109 var select = tElement.find('md-select');
112 select.addClass('md-table-select').attr('md-container-class', 'md-table-select');
115 tElement.addClass('md-cell');
120 // empty controller to be bind properties to in postLink function
121 function Controller() {
125 function postLink(scope, element, attrs, ctrls) {
126 var select = element.find('md-select');
127 var cellCtrl = ctrls.shift();
128 var tableCtrl = ctrls.shift();
131 element.addClass('md-clickable');
135 select.on('click', function (event) {
136 event.stopPropagation();
139 element.addClass('md-clickable').on('click', function (event) {
140 event.stopPropagation();
145 cellCtrl.getTable = tableCtrl.getElement;
147 function getColumn() {
148 return tableCtrl.$$columns[getIndex()];
151 function getIndex() {
152 return Array.prototype.indexOf.call(element.parent().children(), element[0]);
155 scope.$watch(getColumn, function (column) {
161 element.addClass('md-numeric');
163 element.removeClass('md-numeric');
169 controller: Controller,
171 require: ['mdCell', '^^mdTable'],
176 angular.module('md.data.table').directive('mdColumn', mdColumn);
178 function mdColumn($compile, $mdUtil) {
180 function compile(tElement) {
181 tElement.addClass('md-column');
185 function postLink(scope, element, attrs, ctrls) {
186 var headCtrl = ctrls.shift();
187 var tableCtrl = ctrls.shift();
189 function attachSortIcon() {
190 var sortIcon = angular.element('<md-icon md-svg-icon="arrow-up.svg">');
192 $compile(sortIcon.addClass('md-sort-icon').attr('ng-class', 'getDirection()'))(scope);
194 if(element.hasClass('md-numeric')) {
195 element.prepend(sortIcon);
197 element.append(sortIcon);
201 function detachSortIcon() {
202 Array.prototype.some.call(element.find('md-icon'), function (icon) {
203 return icon.classList.contains('md-sort-icon') && element[0].removeChild(icon);
207 function disableSorting() {
209 element.removeClass('md-sort').off('click', setOrder);
212 function enableSorting() {
214 element.addClass('md-sort').on('click', setOrder);
217 function getIndex() {
218 return Array.prototype.indexOf.call(element.parent().children(), element[0]);
221 function isActive() {
222 return scope.orderBy && (headCtrl.order === scope.orderBy || headCtrl.order === '-' + scope.orderBy);
225 function isNumeric() {
226 return attrs.mdNumeric === '' || scope.numeric;
229 function setOrder() {
230 scope.$applyAsync(function () {
232 headCtrl.order = scope.getDirection() === 'md-asc' ? '-' + scope.orderBy : scope.orderBy;
234 headCtrl.order = scope.getDirection() === 'md-asc' ? scope.orderBy : '-' + scope.orderBy;
237 if(angular.isFunction(headCtrl.onReorder)) {
238 $mdUtil.nextTick(function () {
239 headCtrl.onReorder(headCtrl.order);
245 function updateColumn(index, column) {
246 tableCtrl.$$columns[index] = column;
249 element.addClass('md-numeric');
251 element.removeClass('md-numeric');
255 scope.getDirection = function () {
257 return headCtrl.order.charAt(0) === '-' ? 'md-desc' : 'md-asc';
260 return attrs.mdDesc === '' || scope.$eval(attrs.mdDesc) ? 'md-desc' : 'md-asc';
263 scope.$watch(isActive, function (active) {
265 element.addClass('md-active');
267 element.removeClass('md-active');
271 scope.$watch(getIndex, function (index) {
272 updateColumn(index, {'numeric': isNumeric()});
275 scope.$watch(isNumeric, function (numeric) {
276 updateColumn(getIndex(), {'numeric': numeric});
279 scope.$watch('orderBy', function (orderBy) {
281 if(!element.hasClass('md-sort')) {
284 } else if(element.hasClass('md-sort')) {
292 require: ['^^mdHead', '^^mdTable'],
295 numeric: '=?mdNumeric',
296 orderBy: '@?mdOrderBy'
301 mdColumn.$inject = ['$compile', '$mdUtil'];
303 angular.module('md.data.table')
304 .decorator('$controller', controllerDecorator)
305 .factory('$mdEditDialog', mdEditDialog);
308 * A decorator for ng.$controller to optionally bind properties to the
309 * controller before invoking the constructor. Stolen from the ngMock.
311 * https://docs.angularjs.org/api/ngMock/service/$controller
313 function controllerDecorator($delegate) {
314 return function(expression, locals, later, ident) {
315 if(later && typeof later === 'object') {
316 var create = $delegate(expression, locals, true, ident);
317 angular.extend(create.instance, later);
320 return $delegate(expression, locals, later, ident);
324 controllerDecorator.$inject = ['$delegate'];
326 function mdEditDialog($compile, $controller, $document, $mdUtil, $q, $rootScope, $templateCache, $templateRequest, $window) {
327 /* jshint validthis: true */
332 var body = angular.element($document.prop('body'));
345 var defaultOptions = {
346 clickOutsideToClose: true,
352 function build(template, options) {
353 var scope = $rootScope.$new();
354 var element = $compile(template)(scope);
355 var backdrop = $mdUtil.createBackdrop(scope, 'md-edit-dialog-backdrop');
358 if(options.controller) {
359 controller = getController(options, scope, {$element: element, $scope: scope});
361 angular.extend(scope, options.scope);
364 if(options.disableScroll) {
365 disableScroll(element);
368 body.prepend(backdrop).append(element.addClass('md-whiteframe-1dp'));
370 positionDialog(element, options.target);
372 if(options.focusOnOpen) {
\r
373 focusOnOpen(element);
\r
376 if(options.clickOutsideToClose) {
377 backdrop.on('click', function () {
382 if(options.escToClose) {
386 element.on('$destroy', function () {
394 function disableScroll(element) {
395 var restoreScroll = $mdUtil.disableScrollAround(element, body);
397 element.on('$destroy', function () {
402 function getController(options, scope, inject) {
403 if(!options.controller) {
407 if(options.resolve) {
408 angular.extend(inject, options.resolve);
412 angular.extend(inject, options.locals);
415 if(options.controllerAs) {
416 scope[options.controllerAs] = {};
418 if(options.bindToController) {
419 angular.extend(scope[options.controllerAs], options.scope);
421 angular.extend(scope, options.scope);
424 angular.extend(scope, options.scope);
427 if(options.bindToController) {
428 return $controller(options.controller, inject, scope[options.controllerAs]);
430 return $controller(options.controller, inject);
434 function getTemplate(options) {
435 return $q(function (resolve, reject) {
436 var template = options.template;
438 function illegalType(type) {
439 reject('Unexpected template value. Expected a string; received a ' + type + '.');
443 return angular.isString(template) ? resolve(template) : illegalType(typeof template);
446 if(options.templateUrl) {
447 template = $templateCache.get(options.templateUrl);
450 return resolve(template);
453 var success = function (template) {
454 return resolve(template);
457 var error = function () {
458 return reject('Error retrieving template from URL.');
461 return $templateRequest(options.templateUrl).then(success, error);
464 reject('Template not provided.');
468 function logError(error) {
470 console.error(error);
473 function escToClose(element) {
474 var keyup = function (event) {
475 if(event.keyCode === ESCAPE) {
480 body.on('keyup', keyup);
482 element.on('$destroy', function () {
483 body.off('keyup', keyup);
487 function focusOnOpen(element) {
\r
488 $mdUtil.nextTick(function () {
\r
489 var autofocus = $mdUtil.findFocusTarget(element);
497 function positionDialog(element, target) {
498 var table = angular.element(target).controller('mdCell').getTable();
500 var getHeight = function () {
501 return element.prop('clientHeight');
504 var getSize = function () {
511 var getTableBounds = function () {
512 var parent = table.parent();
514 if(parent.prop('tagName') === 'MD-TABLE-CONTAINER') {
515 return parent[0].getBoundingClientRect();
517 return table[0].getBoundingClientRect();
521 var getWidth = function () {
522 return element.prop('clientWidth');
525 var reposition = function () {
526 var size = getSize();
527 var cellBounds = target.getBoundingClientRect();
528 var tableBounds = getTableBounds();
530 if(size.width > tableBounds.right - cellBounds.left) {
531 element.css('left', tableBounds.right - size.width + 'px');
533 element.css('left', cellBounds.left + 'px');
536 if(size.height > tableBounds.bottom - cellBounds.top) {
537 element.css('top', tableBounds.bottom - size.height + 'px');
539 element.css('top', cellBounds.top + 1 + 'px');
542 element.css('minWidth', cellBounds.width + 'px');
545 var watchWidth = $rootScope.$watch(getWidth, reposition);
546 var watchHeight = $rootScope.$watch(getHeight, reposition);
548 $window.addEventListener('resize', reposition);
550 element.on('$destroy', function () {
554 $window.removeEventListener('resize', reposition);
558 function preset(size, options) {
560 function getAttrs() {
561 var attrs = 'type="' + (options.type || 'text') + '"';
563 for(var attr in options.validators) {
564 attrs += ' ' + attr + '="' + options.validators[attr] + '"';
571 controller: ['$element', '$q', 'save', '$scope', function ($element, $q, save, $scope) {
573 if($scope.editDialog.$invalid) {
577 if(angular.isFunction(save)) {
578 return $q.when(save($scope.editDialog.input));
584 this.dismiss = function () {
588 this.getInput = function () {
589 return $scope.editDialog.input;
592 $scope.dismiss = this.dismiss;
594 $scope.submit = function () {
595 update().then(function () {
604 cancel: options.cancel || 'Cancel',
605 messages: options.messages,
606 model: options.modelValue,
607 ok: options.ok || 'Save',
608 placeholder: options.placeholder,
609 title: options.title,
614 '<div layout="column" class="md-content">' +
615 '<div ng-if="size === \'large\'" class="md-title">{{title || \'Edit\'}}</div>' +
616 '<form name="editDialog" layout="column" ng-submit="submit(model)">' +
617 '<md-input-container md-no-float>' +
618 '<input name="input" ng-model="model" md-autofocus placeholder="{{placeholder}} "' + getAttrs() + '>' +
619 '<div ng-messages="editDialog.input.$error">' +
620 '<div ng-repeat="(key, message) in messages" ng-message="{{key}}">{{message}}</div>' +
622 '</md-input-container>' +
625 '<div ng-if="size === \'large\'" layout="row" layout-align="end" class="md-actions">' +
626 '<md-button class="md-primary" ng-click="dismiss()">{{cancel}}</md-button>' +
627 '<md-button class="md-primary" ng-click="submit()">{{ok}}</md-button>' +
633 this.show = function (options) {
639 options = angular.extend({}, defaultOptions, options);
641 if(!options.targetEvent) {
642 return logError('options.targetEvent is required to align the dialog with the table cell.');
645 if(!options.targetEvent.currentTarget.classList.contains('md-cell')) {
646 return logError('The event target must be a table cell.');
649 if(options.bindToController && !options.controllerAs) {
650 return logError('You must define options.controllerAs when options.bindToController is true.');
653 options.target = options.targetEvent.currentTarget;
655 var promise = getTemplate(options);
656 var promises = [promise];
658 for(var prop in options.resolve) {
659 promise = options.resolve[prop];
660 promises.push($q.when(angular.isFunction(promise) ? promise() : promise));
663 promise = $q.all(promises);
665 promise['catch'](logError);
667 return promise.then(function (results) {
668 var template = results.shift();
670 for(var prop in options.resolve) {
671 options.resolve[prop] = results.shift();
674 return build(template, options);
678 this.small = function (options) {
679 return this.show(angular.extend({}, options, preset('small', options)));
682 this.large = function (options) {
683 return this.show(angular.extend({}, options, preset('large', options)));
689 mdEditDialog.$inject = ['$compile', '$controller', '$document', '$mdUtil', '$q', '$rootScope', '$templateCache', '$templateRequest', '$window'];
692 angular.module('md.data.table').directive('mdFoot', mdFoot);
696 function compile(tElement) {
697 tElement.addClass('md-foot');
706 angular.module('md.data.table').directive('mdHead', mdHead);
708 function mdHead($compile) {
710 function compile(tElement) {
711 tElement.addClass('md-head');
715 // empty controller to be bind scope properties to
716 function Controller() {
720 function postLink(scope, element, attrs, tableCtrl) {
721 // because scope.$watch is unpredictable
722 var oldValue = new Array(2);
724 function addCheckboxColumn() {
725 element.children().prepend('<th class="md-column md-checkbox-column">');
728 function attatchCheckbox() {
729 element.prop('lastElementChild').firstElementChild.appendChild($compile(createCheckBox())(scope)[0]);
732 function createCheckBox() {
733 return angular.element('<md-checkbox>').attr({
734 'aria-label': 'Select All',
735 'ng-click': 'toggleAll()',
736 'ng-checked': 'allSelected()',
737 'ng-disabled': '!getSelectableRows().length'
741 function detachCheckbox() {
742 var cell = element.prop('lastElementChild').firstElementChild;
744 if(cell.classList.contains('md-checkbox-column')) {
745 angular.element(cell).empty();
749 function enableRowSelection() {
750 return tableCtrl.$$rowSelect;
753 function mdSelectCtrl(row) {
754 return angular.element(row).controller('mdSelect');
757 function removeCheckboxColumn() {
758 Array.prototype.some.call(element.find('th'), function (cell) {
759 return cell.classList.contains('md-checkbox-column') && cell.remove();
763 scope.allSelected = function () {
764 var rows = scope.getSelectableRows();
766 return rows.length && rows.every(function (row) {
767 return row.isSelected();
771 scope.getSelectableRows = function () {
772 return tableCtrl.getBodyRows().map(mdSelectCtrl).filter(function (ctrl) {
773 return ctrl && !ctrl.disabled;
777 scope.selectAll = function () {
778 tableCtrl.getBodyRows().map(mdSelectCtrl).forEach(function (ctrl) {
779 if(ctrl && !ctrl.isSelected()) {
785 scope.toggleAll = function () {
786 return scope.allSelected() ? scope.unSelectAll() : scope.selectAll();
789 scope.unSelectAll = function () {
790 tableCtrl.getBodyRows().map(mdSelectCtrl).forEach(function (ctrl) {
791 if(ctrl && ctrl.isSelected()) {
797 scope.$watchGroup([enableRowSelection, tableCtrl.enableMultiSelect], function (newValue) {
798 if(newValue[0] !== oldValue[0]) {
806 removeCheckboxColumn();
808 } else if(newValue[0] && newValue[1] !== oldValue[1]) {
816 angular.copy(newValue, oldValue);
821 bindToController: true,
823 controller: Controller,
824 controllerAs: '$mdHead',
825 require: '^^mdTable',
829 onReorder: '=?mdOnReorder'
834 mdHead.$inject = ['$compile'];
836 angular.module('md.data.table').directive('mdRow', mdRow);
840 function compile(tElement) {
841 tElement.addClass('md-row');
845 function postLink(scope, element, attrs, tableCtrl) {
846 function enableRowSelection() {
847 return tableCtrl.$$rowSelect;
850 function isBodyRow() {
851 return tableCtrl.getBodyRows().indexOf(element[0]) !== -1;
854 function isChild(node) {
855 return element[0].contains(node[0]);
859 var cell = angular.element('<td class="md-cell">');
861 scope.$watch(enableRowSelection, function (enable) {
862 // if a row is not selectable, prepend an empty cell to it
863 if(enable && !attrs.mdSelect) {
865 element.prepend(cell);
879 require: '^^mdTable',
884 angular.module('md.data.table').directive('mdSelect', mdSelect);
886 function mdSelect($compile, $parse) {
888 // empty controller to bind scope properties to
889 function Controller() {
893 function postLink(scope, element, attrs, ctrls) {
894 var self = ctrls.shift();
895 var tableCtrl = ctrls.shift();
896 var getId = $parse(attrs.mdSelectId);
898 self.id = getId(self.model);
900 if(tableCtrl.$$rowSelect && self.id) {
901 if(tableCtrl.$$hash.has(self.id)) {
902 var index = tableCtrl.selected.indexOf(tableCtrl.$$hash.get(self.id));
904 // if the item is no longer selected remove it
906 tableCtrl.$$hash.purge(self.id);
909 // if the item is not a reference to the current model update the reference
910 else if(!tableCtrl.$$hash.equals(self.id, self.model)) {
911 tableCtrl.$$hash.update(self.id, self.model);
912 tableCtrl.selected.splice(index, 1, self.model);
917 // check if the item has been selected
918 tableCtrl.selected.some(function (item, index) {
919 if(getId(item) === self.id) {
920 tableCtrl.$$hash.update(self.id, self.model);
921 tableCtrl.selected.splice(index, 1, self.model);
929 self.isSelected = function () {
930 if(!tableCtrl.$$rowSelect) {
935 return tableCtrl.$$hash.has(self.id);
938 return tableCtrl.selected.indexOf(self.model) !== -1;
941 self.select = function () {
946 if(tableCtrl.enableMultiSelect()) {
947 tableCtrl.selected.push(self.model);
949 tableCtrl.selected.splice(0, tableCtrl.selected.length, self.model);
952 if(angular.isFunction(self.onSelect)) {
953 self.onSelect(self.model);
957 self.deselect = function () {
962 tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
964 if(angular.isFunction(self.onDeselect)) {
965 self.onDeselect(self.model);
969 self.toggle = function (event) {
970 if(event && event.stopPropagation) {
971 event.stopPropagation();
974 return self.isSelected() ? self.deselect() : self.select();
977 function autoSelect() {
978 return attrs.mdAutoSelect === '' || self.autoSelect;
981 function createCheckbox() {
982 var checkbox = angular.element('<md-checkbox>').attr({
983 'aria-label': 'Select Row',
984 'ng-click': '$mdSelect.toggle($event)',
985 'ng-checked': '$mdSelect.isSelected()',
986 'ng-disabled': '$mdSelect.disabled'
989 return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
992 function disableSelection() {
993 Array.prototype.some.call(element.children(), function (child) {
994 return child.classList.contains('md-checkbox-cell') && element[0].removeChild(child);
998 element.off('click', toggle);
1002 function enableSelection() {
1003 element.prepend(createCheckbox());
1006 element.on('click', toggle);
1010 function enableRowSelection() {
1011 return tableCtrl.$$rowSelect;
1014 function onSelectChange(selected) {
1019 if(tableCtrl.$$hash.has(self.id)) {
1020 // check if the item has been deselected
1021 if(selected.indexOf(tableCtrl.$$hash.get(self.id)) === -1) {
1022 tableCtrl.$$hash.purge(self.id);
1028 // check if the item has been selected
1029 if(selected.indexOf(self.model) !== -1) {
1030 tableCtrl.$$hash.update(self.id, self.model);
1034 function toggle(event) {
1035 scope.$applyAsync(function () {
1040 scope.$watch(enableRowSelection, function (enable) {
1048 scope.$watch(autoSelect, function (newValue, oldValue) {
1049 if(newValue === oldValue) {
1053 if(tableCtrl.$$rowSelect && newValue) {
1054 element.on('click', toggle);
1056 element.off('click', toggle);
1060 scope.$watch(self.isSelected, function (isSelected) {
1061 return isSelected ? element.addClass('md-selected') : element.removeClass('md-selected');
1064 scope.$watch(tableCtrl.enableMultiSelect, function (multiple) {
1065 if(tableCtrl.$$rowSelect && !multiple) {
1066 // remove all but the first selected item
1067 tableCtrl.selected.splice(1);
1071 tableCtrl.registerModelChangeListener(onSelectChange);
1073 element.on('$destroy', function () {
1074 tableCtrl.removeModelChangeListener(onSelectChange);
1079 bindToController: true,
1080 controller: Controller,
1081 controllerAs: '$mdSelect',
1083 require: ['mdSelect', '^^mdTable'],
1087 disabled: '=ngDisabled',
1088 onSelect: '=?mdOnSelect',
1089 onDeselect: '=?mdOnDeselect',
1090 autoSelect: '=mdAutoSelect'
1095 mdSelect.$inject = ['$compile', '$parse'];
1097 angular.module('md.data.table').directive('mdTable', mdTable);
1102 this.equals = function (key, item) {
1103 return keys[key] === item;
1106 this.get = function (key) {
1110 this.has = function (key) {
1111 return keys.hasOwnProperty(key);
1114 this.purge = function (key) {
1118 this.update = function (key, item) {
1123 function mdTable() {
1125 function compile(tElement, tAttrs) {
1126 tElement.addClass('md-table');
1128 if(tAttrs.hasOwnProperty('mdProgress')) {
1129 var body = tElement.find('tbody')[0];
1130 var progress = angular.element('<thead class="md-table-progress" md-table-progress>');
1133 tElement[0].insertBefore(progress[0], body);
1138 function Controller($attrs, $element, $q, $scope) {
1142 var modelChangeListeners = [];
1144 self.$$hash = new Hash();
1145 self.$$columns = {};
1147 function enableRowSelection() {
1148 self.$$rowSelect = true;
1150 watchListener = $scope.$watchCollection('$mdTable.selected', function (selected) {
1151 modelChangeListeners.forEach(function (listener) {
1156 $element.addClass('md-row-select');
1159 function disableRowSelection() {
1160 self.$$rowSelect = false;
1162 if(angular.isFunction(watchListener)) {
1166 $element.removeClass('md-row-select');
1169 function resolvePromises() {
1171 return $scope.$applyAsync();
1174 queue[0]['finally'](function () {
1180 function rowSelect() {
1181 return $attrs.mdRowSelect === '' || self.rowSelect;
1184 function validateModel() {
1185 if(!self.selected) {
1186 return console.error('Row selection: ngModel is not defined.');
1189 if(!angular.isArray(self.selected)) {
1190 return console.error('Row selection: Expected an array. Recived ' + typeof self.selected + '.');
1196 self.columnCount = function () {
1197 return self.getRows($element[0]).reduce(function (count, row) {
1198 return row.cells.length > count ? row.cells.length : count;
1202 self.getRows = function (element) {
1203 return Array.prototype.filter.call(element.rows, function (row) {
1204 return !row.classList.contains('ng-leave');
1208 self.getBodyRows = function () {
1209 return Array.prototype.reduce.call($element.prop('tBodies'), function (result, tbody) {
1210 return result.concat(self.getRows(tbody));
1214 self.getElement = function () {
1218 self.getHeaderRows = function () {
1219 return self.getRows($element.prop('tHead'));
1222 self.enableMultiSelect = function () {
1223 return $attrs.multiple === '' || $scope.$eval($attrs.multiple);
1226 self.waitingOnPromise = function () {
1227 return !!queue.length;
1230 self.queuePromise = function (promise) {
1235 if(queue.push(angular.isArray(promise) ? $q.all(promise) : $q.when(promise)) === 1) {
1240 self.registerModelChangeListener = function (listener) {
1241 modelChangeListeners.push(listener);
1244 self.removeModelChangeListener = function (listener) {
1245 var index = modelChangeListeners.indexOf(listener);
1248 modelChangeListeners.splice(index, 1);
1252 if($attrs.hasOwnProperty('mdProgress')) {
1253 $scope.$watch('$mdTable.progress', self.queuePromise);
1256 $scope.$watch(rowSelect, function (enable) {
1257 if(enable && !!validateModel()) {
1258 enableRowSelection();
1260 disableRowSelection();
1265 Controller.$inject = ['$attrs', '$element', '$q', '$scope'];
1268 bindToController: true,
1270 controller: Controller,
1271 controllerAs: '$mdTable',
1274 progress: '=?mdProgress',
1275 selected: '=ngModel',
1276 rowSelect: '=mdRowSelect'
1281 angular.module('md.data.table').directive('mdTablePagination', mdTablePagination);
1283 function mdTablePagination() {
1285 function compile(tElement) {
1286 tElement.addClass('md-table-pagination');
1289 function Controller($attrs, $mdUtil, $scope) {
1291 var defaultLabel = {
1293 rowsPerPage: 'Rows per page:',
1297 self.label = angular.copy(defaultLabel);
1299 function isPositive(number) {
1300 return parseInt(number, 10) > 0;
1303 self.eval = function (expression) {
1304 return $scope.$eval(expression);
1307 self.first = function () {
1309 self.onPaginationChange();
1312 self.hasNext = function () {
1313 return self.page * self.limit < self.total;
1316 self.hasPrevious = function () {
1317 return self.page > 1;
1320 self.last = function () {
1321 self.page = self.pages();
1322 self.onPaginationChange();
1325 self.max = function () {
1326 return self.hasNext() ? self.page * self.limit : self.total;
1329 self.min = function () {
1330 return isPositive(self.total) ? self.page * self.limit - self.limit + 1 : 0;
1333 self.next = function () {
1335 self.onPaginationChange();
1338 self.onPaginationChange = function () {
1339 if(angular.isFunction(self.onPaginate)) {
1340 $mdUtil.nextTick(function () {
1341 self.onPaginate(self.page, self.limit);
1346 self.pages = function () {
1347 return isPositive(self.total) ? Math.ceil(self.total / (isPositive(self.limit) ? self.limit : 1)) : 1;
1350 self.previous = function () {
1352 self.onPaginationChange();
1355 self.showBoundaryLinks = function () {
1356 return $attrs.mdBoundaryLinks === '' || self.boundaryLinks;
1359 self.showPageSelect = function () {
1360 return $attrs.mdPageSelect === '' || self.pageSelect;
1363 $scope.$watch('$pagination.limit', function (newValue, oldValue) {
1364 if(isNaN(newValue) || isNaN(oldValue) || newValue === oldValue) {
1368 // find closest page from previous min
1369 self.page = Math.floor(((self.page * oldValue - oldValue) + newValue) / (isPositive(newValue) ? newValue : 1));
1370 self.onPaginationChange();
1373 $attrs.$observe('mdLabel', function (label) {
1374 angular.extend(self.label, defaultLabel, $scope.$eval(label));
1377 $scope.$watch('$pagination.total', function (newValue, oldValue) {
1378 if(isNaN(newValue) || newValue === oldValue) {
1382 if(self.page > self.pages()) {
1388 Controller.$inject = ['$attrs', '$mdUtil', '$scope'];
1392 boundaryLinks: '=?mdBoundaryLinks',
1393 disabled: '=ngDisabled',
1396 pageSelect: '=?mdPageSelect',
1397 onPaginate: '=?mdOnPaginate',
1398 limitOptions: '=?mdLimitOptions',
1402 controller: Controller,
1403 controllerAs: '$pagination',
1406 templateUrl: 'md-table-pagination.html'
1410 angular.module('md.data.table').directive('mdTableProgress', mdTableProgress);
1412 function mdTableProgress() {
1414 function postLink(scope, element, attrs, tableCtrl) {
1415 scope.columnCount = tableCtrl.columnCount;
1416 scope.deferred = tableCtrl.waitingOnPromise;
1421 require: '^^mdTable',
1424 templateUrl: 'md-table-progress.html'
1428 angular.module('md.data.table').directive('virtualPageSelect', virtualPageSelect);
1430 function virtualPageSelect() {
1432 function Controller($element, $scope) {
1434 var content = $element.find('md-content');
1438 function getMin(pages, total) {
1439 return Math.min(pages, isFinite(total) && isPositive(total) ? total : 1);
1442 function isPositive(number) {
1446 function setPages(max) {
1447 if(self.pages.length > max) {
1448 return self.pages.splice(max);
1451 for(var i = self.pages.length; i < max; i++) {
1452 self.pages.push(i + 1);
1456 content.on('scroll', function () {
1457 if((content.prop('clientHeight') + content.prop('scrollTop')) >= content.prop('scrollHeight')) {
1458 $scope.$applyAsync(function () {
1459 setPages(getMin(self.pages.length + 10, self.total));
1464 $scope.$watch('$pageSelect.total', function (total) {
1465 setPages(getMin(Math.max(self.pages.length, 10), total));
1468 $scope.$watch('$pagination.page', function (page) {
1469 for(var i = self.pages.length; i < page; i++) {
1470 self.pages.push(i + 1);
1475 Controller.$inject = ['$element', '$scope'];
1481 controller: Controller,
1482 controllerAs: '$pageSelect'
1486 })(window, angular);