Built motion from commit b598105.|2.0.10
[motion2.git] / public / bower_components / angular-material-data-table / dist / md-data-table.js
1 /*
2  * Angular Material Data Table
3  * https://github.com/daniel-nagy/md-data-table
4  * @license MIT
5  * v0.10.9
6  */
7 (function (window, angular, undefined) {
8 'use strict';
9
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']);
11
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' +
16     '\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' +
18     '    <md-content>\n' +
19     '      <md-option ng-repeat="page in $pageSelect.pages" ng-value="page">{{page}}</md-option>\n' +
20     '    </md-content>\n' +
21     '  </md-select>\n' +
22     '</div>\n' +
23     '\n' +
24     '<div class="limit-select" ng-if="$pagination.limitOptions">\n' +
25     '  <div class="label">{{$pagination.label.rowsPerPage}}</div>\n' +
26     '\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' +
29     '  </md-select>\n' +
30     '</div>\n' +
31     '\n' +
32     '<div class="buttons">\n' +
33     '  <div class="label">{{$pagination.min()}} - {{$pagination.max()}} {{$pagination.label.of}} {{$pagination.total}}</div>\n' +
34     '\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' +
37     '  </md-button>\n' +
38     '\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' +
41     '  </md-button>\n' +
42     '\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' +
45     '  </md-button>\n' +
46     '\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' +
49     '  </md-button>\n' +
50     '</div>');
51 }]);
52
53 angular.module('md-table-progress.html', []).run(['$templateCache', function($templateCache) {
54   $templateCache.put('md-table-progress.html',
55     '<tr>\n' +
56     '  <th colspan="{{columnCount()}}">\n' +
57     '    <md-progress-linear ng-show="deferred()" md-mode="indeterminate"></md-progress-linear>\n' +
58     '  </th>\n' +
59     '</tr>');
60 }]);
61
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>');
65 }]);
66
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>');
70 }]);
71
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>');
75 }]);
76
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>');
80 }]);
81
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>');
85 }]);
86
87
88 angular.module('md.data.table', ['md.table.templates']);
89
90 angular.module('md.data.table').directive('mdBody', mdBody);
91
92 function mdBody() {
93
94   function compile(tElement) {
95     tElement.addClass('md-body');
96   }
97
98   return {
99     compile: compile,
100     restrict: 'A'
101   };
102 }
103
104 angular.module('md.data.table').directive('mdCell', mdCell);
105
106 function mdCell() {
107   
108   function compile(tElement) {
109     var select = tElement.find('md-select');
110     
111     if(select.length) {
112       select.addClass('md-table-select').attr('md-container-class', 'md-table-select');
113     }
114     
115     tElement.addClass('md-cell');
116     
117     return postLink;
118   }
119   
120   // empty controller to be bind properties to in postLink function
121   function Controller() {
122     
123   }
124   
125   function postLink(scope, element, attrs, ctrls) {
126     var select = element.find('md-select');
127     var cellCtrl = ctrls.shift();
128     var tableCtrl = ctrls.shift();
129     
130     if(attrs.ngClick) {
131       element.addClass('md-clickable');
132     }
133     
134     if(select.length) {
135       select.on('click', function (event) {
136         event.stopPropagation();
137       });
138       
139       element.addClass('md-clickable').on('click', function (event) {
140         event.stopPropagation();
141         select[0].click();
142       });
143     }
144     
145     cellCtrl.getTable = tableCtrl.getElement;
146     
147     function getColumn() {
148       return tableCtrl.$$columns[getIndex()];
149     }
150     
151     function getIndex() {
152       return Array.prototype.indexOf.call(element.parent().children(), element[0]);
153     }
154     
155     scope.$watch(getColumn, function (column) {
156       if(!column) {
157         return;
158       }
159       
160       if(column.numeric) {
161         element.addClass('md-numeric');
162       } else {
163         element.removeClass('md-numeric');
164       }
165     });
166   }
167   
168   return {
169     controller: Controller,
170     compile: compile,
171     require: ['mdCell', '^^mdTable'],
172     restrict: 'A'
173   };
174 }
175
176 angular.module('md.data.table').directive('mdColumn', mdColumn);
177
178 function mdColumn($compile, $mdUtil) {
179
180   function compile(tElement) {
181     tElement.addClass('md-column');
182     return postLink;
183   }
184
185   function postLink(scope, element, attrs, ctrls) {
186     var headCtrl = ctrls.shift();
187     var tableCtrl = ctrls.shift();
188
189     function attachSortIcon() {
190       var sortIcon = angular.element('<md-icon md-svg-icon="arrow-up.svg">');
191
192       $compile(sortIcon.addClass('md-sort-icon').attr('ng-class', 'getDirection()'))(scope);
193
194       if(element.hasClass('md-numeric')) {
195         element.prepend(sortIcon);
196       } else {
197         element.append(sortIcon);
198       }
199     }
200
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);
204       });
205     }
206
207     function disableSorting() {
208       detachSortIcon();
209       element.removeClass('md-sort').off('click', setOrder);
210     }
211
212     function enableSorting() {
213       attachSortIcon();
214       element.addClass('md-sort').on('click', setOrder);
215     }
216
217     function getIndex() {
218       return Array.prototype.indexOf.call(element.parent().children(), element[0]);
219     }
220
221     function isActive() {
222       return scope.orderBy && (headCtrl.order === scope.orderBy || headCtrl.order === '-' + scope.orderBy);
223     }
224
225     function isNumeric() {
226       return attrs.mdNumeric === '' || scope.numeric;
227     }
228
229     function setOrder() {
230       scope.$applyAsync(function () {
231         if(isActive()) {
232           headCtrl.order = scope.getDirection() === 'md-asc' ? '-' + scope.orderBy : scope.orderBy;
233         } else {
234           headCtrl.order = scope.getDirection() === 'md-asc' ? scope.orderBy : '-' + scope.orderBy;
235         }
236
237         if(angular.isFunction(headCtrl.onReorder)) {
238           $mdUtil.nextTick(function () {
239             headCtrl.onReorder(headCtrl.order);
240           });
241         }
242       });
243     }
244
245     function updateColumn(index, column) {
246       tableCtrl.$$columns[index] = column;
247
248       if(column.numeric) {
249         element.addClass('md-numeric');
250       } else {
251         element.removeClass('md-numeric');
252       }
253     }
254
255     scope.getDirection = function () {
256       if(isActive()) {
257         return headCtrl.order.charAt(0) === '-' ? 'md-desc' : 'md-asc';
258       }
259
260       return attrs.mdDesc === '' || scope.$eval(attrs.mdDesc) ? 'md-desc' : 'md-asc';
261     };
262
263     scope.$watch(isActive, function (active) {
264       if(active) {
265         element.addClass('md-active');
266       } else {
267         element.removeClass('md-active');
268       }
269     });
270
271     scope.$watch(getIndex, function (index) {
272       updateColumn(index, {'numeric': isNumeric()});
273     });
274
275     scope.$watch(isNumeric, function (numeric) {
276       updateColumn(getIndex(), {'numeric': numeric});
277     });
278
279     scope.$watch('orderBy', function (orderBy) {
280       if(orderBy) {
281         if(!element.hasClass('md-sort')) {
282           enableSorting();
283         }
284       } else if(element.hasClass('md-sort')) {
285         disableSorting();
286       }
287     });
288   }
289
290   return {
291     compile: compile,
292     require: ['^^mdHead', '^^mdTable'],
293     restrict: 'A',
294     scope: {
295       numeric: '=?mdNumeric',
296       orderBy: '@?mdOrderBy'
297     }
298   };
299 }
300
301 mdColumn.$inject = ['$compile', '$mdUtil'];
302
303 angular.module('md.data.table')
304   .decorator('$controller', controllerDecorator)
305   .factory('$mdEditDialog', mdEditDialog);
306
307 /*
308  * A decorator for ng.$controller to optionally bind properties to the
309  * controller before invoking the constructor. Stolen from the ngMock.
310  *
311  * https://docs.angularjs.org/api/ngMock/service/$controller
312  */
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);
318       return create();
319     }
320     return $delegate(expression, locals, later, ident);
321   };
322 }
323
324 controllerDecorator.$inject = ['$delegate'];
325   
326 function mdEditDialog($compile, $controller, $document, $mdUtil, $q, $rootScope, $templateCache, $templateRequest, $window) {
327   /* jshint validthis: true */
328   
329   var ESCAPE = 27;
330   
331   var busy = false;
332   var body = angular.element($document.prop('body'));
333   
334   /*
335    * bindToController
336    * controller
337    * controllerAs
338    * locals
339    * resolve
340    * scope
341    * targetEvent
342    * template
343    * templateUrl
344    */
345   var defaultOptions = {
346     clickOutsideToClose: true,
347     disableScroll: true,
348     escToClose: true,
349     focusOnOpen: true
350   };
351   
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');
356     var controller;
357     
358     if(options.controller) {
359       controller = getController(options, scope, {$element: element, $scope: scope});
360     } else {
361       angular.extend(scope, options.scope);
362     }
363     
364     if(options.disableScroll) {
365       disableScroll(element);
366     }
367     
368     body.prepend(backdrop).append(element.addClass('md-whiteframe-1dp'));
369     
370     positionDialog(element, options.target);
371     
372     if(options.focusOnOpen) {\r
373       focusOnOpen(element);\r
374     }\r
375     
376     if(options.clickOutsideToClose) {
377       backdrop.on('click', function () {
378         element.remove();
379       });
380     }
381     
382     if(options.escToClose) {
383       escToClose(element);
384     }
385     
386     element.on('$destroy', function () {
387       busy = false;
388       backdrop.remove();
389     });
390     
391     return controller;
392   }
393   
394   function disableScroll(element) {
395     var restoreScroll = $mdUtil.disableScrollAround(element, body);
396     
397     element.on('$destroy', function () {
398       restoreScroll();
399     });
400   }
401   
402   function getController(options, scope, inject) {
403     if(!options.controller) {
404       return;
405     }
406     
407     if(options.resolve) {
408       angular.extend(inject, options.resolve);
409     }
410     
411     if(options.locals) {
412       angular.extend(inject, options.locals);
413     }
414     
415     if(options.controllerAs) {
416       scope[options.controllerAs] = {};
417       
418       if(options.bindToController) {
419         angular.extend(scope[options.controllerAs], options.scope);
420       } else {
421         angular.extend(scope, options.scope);
422       }
423     } else {
424       angular.extend(scope, options.scope);
425     }
426     
427     if(options.bindToController) {
428       return $controller(options.controller, inject, scope[options.controllerAs]);
429     } else {
430       return $controller(options.controller, inject);
431     }
432   }
433   
434   function getTemplate(options) {
435     return $q(function (resolve, reject) {
436       var template = options.template;
437       
438       function illegalType(type) {
439         reject('Unexpected template value. Expected a string; received a ' + type + '.');
440       }
441       
442       if(template) {
443         return angular.isString(template) ? resolve(template) : illegalType(typeof template);
444       }
445       
446       if(options.templateUrl) {
447         template = $templateCache.get(options.templateUrl);
448         
449         if(template) {
450           return resolve(template);
451         }
452         
453         var success = function (template) {
454           return resolve(template);
455         };
456         
457         var error = function () {
458           return reject('Error retrieving template from URL.');
459         };
460         
461         return $templateRequest(options.templateUrl).then(success, error);
462       }
463       
464       reject('Template not provided.');
465     });
466   }
467   
468   function logError(error) {
469     busy = false;
470     console.error(error);
471   }
472   
473   function escToClose(element) {
474     var keyup = function (event) {
475       if(event.keyCode === ESCAPE) {
476         element.remove();
477       }
478     };
479     
480     body.on('keyup', keyup);
481     
482     element.on('$destroy', function () {
483       body.off('keyup', keyup);
484     });
485   }
486
487   function focusOnOpen(element) {\r
488     $mdUtil.nextTick(function () {\r
489       var autofocus = $mdUtil.findFocusTarget(element);
490       
491       if(autofocus) {\r
492         autofocus.focus();\r
493       }\r
494     }, false);\r
495   }
496
497   function positionDialog(element, target) {
498     var table = angular.element(target).controller('mdCell').getTable();
499     
500     var getHeight = function () {
501       return element.prop('clientHeight');
502     };
503     
504     var getSize = function () {
505       return {
506         width: getWidth(),
507         height: getHeight()
508       };
509     };
510     
511     var getTableBounds = function () {
512       var parent = table.parent();
513       
514       if(parent.prop('tagName') === 'MD-TABLE-CONTAINER') {
515         return parent[0].getBoundingClientRect();
516       } else {
517         return table[0].getBoundingClientRect();
518       }
519     };
520     
521     var getWidth = function () {
522       return element.prop('clientWidth');
523     };
524     
525     var reposition = function () {
526       var size = getSize();
527       var cellBounds = target.getBoundingClientRect();
528       var tableBounds = getTableBounds();
529       
530       if(size.width > tableBounds.right - cellBounds.left) {
531         element.css('left', tableBounds.right - size.width + 'px');
532       } else {
533         element.css('left', cellBounds.left + 'px');
534       }
535       
536       if(size.height > tableBounds.bottom - cellBounds.top) {
537         element.css('top', tableBounds.bottom - size.height + 'px');
538       } else {
539         element.css('top', cellBounds.top + 1 + 'px');
540       }
541       
542       element.css('minWidth', cellBounds.width + 'px');
543     };
544     
545     var watchWidth = $rootScope.$watch(getWidth, reposition);
546     var watchHeight = $rootScope.$watch(getHeight, reposition);
547     
548     $window.addEventListener('resize', reposition);
549     
550     element.on('$destroy', function () {
551       watchWidth();
552       watchHeight();
553       
554       $window.removeEventListener('resize', reposition);
555     });
556   }
557   
558   function preset(size, options) {
559     
560     function getAttrs() {
561       var attrs = 'type="' + (options.type || 'text') + '"';
562       
563       for(var attr in options.validators) {
564         attrs += ' ' + attr + '="' + options.validators[attr] + '"';
565       }
566       
567       return attrs;
568     }
569     
570     return {
571       controller: ['$element', '$q', 'save', '$scope', function ($element, $q, save, $scope) {
572         function update() {
573           if($scope.editDialog.$invalid) {
574             return $q.reject();
575           }
576           
577           if(angular.isFunction(save)) {
578             return $q.when(save($scope.editDialog.input));
579           }
580           
581           return $q.resolve();
582         }
583         
584         this.dismiss = function () {
585           $element.remove();
586         };
587         
588         this.getInput = function () {
589           return $scope.editDialog.input;
590         };
591         
592         $scope.dismiss = this.dismiss;
593         
594         $scope.submit = function () {
595           update().then(function () {
596             $scope.dismiss();
597           });
598         };
599       }],
600       locals: {
601         save: options.save
602       },
603       scope: {
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,
610         size: size
611       },
612       template:
613         '<md-edit-dialog>' +
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>' +
621                 '</div>' +
622               '</md-input-container>' +
623             '</form>' +
624           '</div>' +
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>' +
628           '</div>' +
629         '</md-edit-dialog>'
630     };
631   }
632   
633   this.show = function (options) {
634     if(busy) {
635       return $q.reject();
636     }
637     
638     busy = true;
639     options = angular.extend({}, defaultOptions, options);
640     
641     if(!options.targetEvent) {
642       return logError('options.targetEvent is required to align the dialog with the table cell.');
643     }
644     
645     if(!options.targetEvent.currentTarget.classList.contains('md-cell')) {
646       return logError('The event target must be a table cell.');
647     }
648     
649     if(options.bindToController && !options.controllerAs) {
650       return logError('You must define options.controllerAs when options.bindToController is true.');
651     }
652     
653     options.target = options.targetEvent.currentTarget;
654     
655     var promise = getTemplate(options);
656     var promises = [promise];
657     
658     for(var prop in options.resolve) {
659       promise = options.resolve[prop];
660       promises.push($q.when(angular.isFunction(promise) ? promise() : promise));
661     }
662     
663     promise = $q.all(promises);
664     
665     promise['catch'](logError);
666     
667     return promise.then(function (results) {
668       var template = results.shift();
669       
670       for(var prop in options.resolve) {
671         options.resolve[prop] = results.shift();
672       }
673       
674       return build(template, options);
675     });
676   };
677   
678   this.small = function (options) {
679     return this.show(angular.extend({}, options, preset('small', options)));
680   }.bind(this);
681   
682   this.large = function (options) {
683     return this.show(angular.extend({}, options, preset('large', options)));
684   }.bind(this);
685   
686   return this;
687 }
688
689 mdEditDialog.$inject = ['$compile', '$controller', '$document', '$mdUtil', '$q', '$rootScope', '$templateCache', '$templateRequest', '$window'];
690
691
692 angular.module('md.data.table').directive('mdFoot', mdFoot);
693
694 function mdFoot() {
695
696   function compile(tElement) {
697     tElement.addClass('md-foot');
698   }
699
700   return {
701     compile: compile,
702     restrict: 'A'
703   };
704 }
705
706 angular.module('md.data.table').directive('mdHead', mdHead);
707
708 function mdHead($compile) {
709
710   function compile(tElement) {
711     tElement.addClass('md-head');
712     return postLink;
713   }
714   
715   // empty controller to be bind scope properties to
716   function Controller() {
717     
718   }
719   
720   function postLink(scope, element, attrs, tableCtrl) {
721     // because scope.$watch is unpredictable
722     var oldValue = new Array(2);
723     
724     function addCheckboxColumn() {
725       element.children().prepend('<th class="md-column md-checkbox-column">');
726     }
727     
728     function attatchCheckbox() {
729       element.prop('lastElementChild').firstElementChild.appendChild($compile(createCheckBox())(scope)[0]);
730     }
731     
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'
738       });
739     }
740     
741     function detachCheckbox() {
742       var cell = element.prop('lastElementChild').firstElementChild;
743       
744       if(cell.classList.contains('md-checkbox-column')) {
745         angular.element(cell).empty();
746       }
747     }
748     
749     function enableRowSelection() {
750       return tableCtrl.$$rowSelect;
751     }
752     
753     function mdSelectCtrl(row) {
754       return angular.element(row).controller('mdSelect');
755     }
756     
757     function removeCheckboxColumn() {
758       Array.prototype.some.call(element.find('th'), function (cell) {
759         return cell.classList.contains('md-checkbox-column') && cell.remove();
760       });
761     }
762     
763     scope.allSelected = function () {
764       var rows = scope.getSelectableRows();
765       
766       return rows.length && rows.every(function (row) {
767         return row.isSelected();
768       });
769     };
770     
771     scope.getSelectableRows = function () {
772       return tableCtrl.getBodyRows().map(mdSelectCtrl).filter(function (ctrl) {
773         return ctrl && !ctrl.disabled;
774       });
775     };
776     
777     scope.selectAll = function () {
778       tableCtrl.getBodyRows().map(mdSelectCtrl).forEach(function (ctrl) {
779         if(ctrl && !ctrl.isSelected()) {
780           ctrl.select();
781         }
782       });
783     };
784     
785     scope.toggleAll = function () {
786       return scope.allSelected() ? scope.unSelectAll() : scope.selectAll();
787     };
788     
789     scope.unSelectAll = function () {
790       tableCtrl.getBodyRows().map(mdSelectCtrl).forEach(function (ctrl) {
791         if(ctrl && ctrl.isSelected()) {
792           ctrl.deselect();
793         }
794       });
795     };
796     
797     scope.$watchGroup([enableRowSelection, tableCtrl.enableMultiSelect], function (newValue) {
798       if(newValue[0] !== oldValue[0]) {
799         if(newValue[0]) {
800           addCheckboxColumn();
801           
802           if(newValue[1]) {
803             attatchCheckbox();
804           }
805         } else {
806           removeCheckboxColumn();
807         }
808       } else if(newValue[0] && newValue[1] !== oldValue[1]) {
809         if(newValue[1]) {
810           attatchCheckbox();
811         } else {
812           detachCheckbox();
813         }
814       }
815       
816       angular.copy(newValue, oldValue);
817     });
818   }
819   
820   return {
821     bindToController: true,
822     compile: compile,
823     controller: Controller,
824     controllerAs: '$mdHead',
825     require: '^^mdTable',
826     restrict: 'A',
827     scope: {
828       order: '=?mdOrder',
829       onReorder: '=?mdOnReorder'
830     }
831   };
832 }
833
834 mdHead.$inject = ['$compile'];
835
836 angular.module('md.data.table').directive('mdRow', mdRow);
837
838 function mdRow() {
839
840   function compile(tElement) {
841     tElement.addClass('md-row');
842     return postLink;
843   }
844   
845   function postLink(scope, element, attrs, tableCtrl) {
846     function enableRowSelection() {
847       return tableCtrl.$$rowSelect;
848     }
849     
850     function isBodyRow() {
851       return tableCtrl.getBodyRows().indexOf(element[0]) !== -1;
852     }
853     
854     function isChild(node) {
855       return element[0].contains(node[0]);
856     }
857     
858     if(isBodyRow()) {
859       var cell = angular.element('<td class="md-cell">');
860       
861       scope.$watch(enableRowSelection, function (enable) {
862         // if a row is not selectable, prepend an empty cell to it
863         if(enable && !attrs.mdSelect) {
864           if(!isChild(cell)) {
865             element.prepend(cell);
866           }
867           return;
868         }
869         
870         if(isChild(cell)) {
871           cell.remove();
872         }
873       });
874     }
875   }
876
877   return {
878     compile: compile,
879     require: '^^mdTable',
880     restrict: 'A'
881   };
882 }
883
884 angular.module('md.data.table').directive('mdSelect', mdSelect);
885
886 function mdSelect($compile, $parse) {
887
888   // empty controller to bind scope properties to
889   function Controller() {
890
891   }
892
893   function postLink(scope, element, attrs, ctrls) {
894     var self = ctrls.shift();
895     var tableCtrl = ctrls.shift();
896     var getId = $parse(attrs.mdSelectId);
897
898     self.id = getId(self.model);
899
900     if(tableCtrl.$$rowSelect && self.id) {
901       if(tableCtrl.$$hash.has(self.id)) {
902         var index = tableCtrl.selected.indexOf(tableCtrl.$$hash.get(self.id));
903
904         // if the item is no longer selected remove it
905         if(index === -1) {
906           tableCtrl.$$hash.purge(self.id);
907         }
908
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);
913         }
914
915       } else {
916
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);
922
923             return true;
924           }
925         });
926       }
927     }
928
929     self.isSelected = function () {
930       if(!tableCtrl.$$rowSelect) {
931         return false;
932       }
933
934       if(self.id) {
935         return tableCtrl.$$hash.has(self.id);
936       }
937
938       return tableCtrl.selected.indexOf(self.model) !== -1;
939     };
940
941     self.select = function () {
942       if(self.disabled) {
943         return;
944       }
945
946       if(tableCtrl.enableMultiSelect()) {
947         tableCtrl.selected.push(self.model);
948       } else {
949         tableCtrl.selected.splice(0, tableCtrl.selected.length, self.model);
950       }
951
952       if(angular.isFunction(self.onSelect)) {
953         self.onSelect(self.model);
954       }
955     };
956
957     self.deselect = function () {
958       if(self.disabled) {
959         return;
960       }
961
962       tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
963
964       if(angular.isFunction(self.onDeselect)) {
965         self.onDeselect(self.model);
966       }
967     };
968
969     self.toggle = function (event) {
970       if(event && event.stopPropagation) {
971         event.stopPropagation();
972       }
973
974       return self.isSelected() ? self.deselect() : self.select();
975     };
976
977     function autoSelect() {
978       return attrs.mdAutoSelect === '' || self.autoSelect;
979     }
980
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'
987       });
988
989       return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
990     }
991
992     function disableSelection() {
993       Array.prototype.some.call(element.children(), function (child) {
994         return child.classList.contains('md-checkbox-cell') && element[0].removeChild(child);
995       });
996
997       if(autoSelect()) {
998         element.off('click', toggle);
999       }
1000     }
1001
1002     function enableSelection() {
1003       element.prepend(createCheckbox());
1004
1005       if(autoSelect()) {
1006         element.on('click', toggle);
1007       }
1008     }
1009
1010     function enableRowSelection() {
1011       return tableCtrl.$$rowSelect;
1012     }
1013
1014     function onSelectChange(selected) {
1015       if(!self.id) {
1016         return;
1017       }
1018
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);
1023         }
1024
1025         return;
1026       }
1027
1028       // check if the item has been selected
1029       if(selected.indexOf(self.model) !== -1) {
1030         tableCtrl.$$hash.update(self.id, self.model);
1031       }
1032     }
1033
1034     function toggle(event) {
1035       scope.$applyAsync(function () {
1036         self.toggle(event);
1037       });
1038     }
1039
1040     scope.$watch(enableRowSelection, function (enable) {
1041       if(enable) {
1042         enableSelection();
1043       } else {
1044         disableSelection();
1045       }
1046     });
1047
1048     scope.$watch(autoSelect, function (newValue, oldValue) {
1049       if(newValue === oldValue) {
1050         return;
1051       }
1052
1053       if(tableCtrl.$$rowSelect && newValue) {
1054         element.on('click', toggle);
1055       } else {
1056         element.off('click', toggle);
1057       }
1058     });
1059
1060     scope.$watch(self.isSelected, function (isSelected) {
1061       return isSelected ? element.addClass('md-selected') : element.removeClass('md-selected');
1062     });
1063
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);
1068       }
1069     });
1070
1071     tableCtrl.registerModelChangeListener(onSelectChange);
1072
1073     element.on('$destroy', function () {
1074       tableCtrl.removeModelChangeListener(onSelectChange);
1075     });
1076   }
1077
1078   return {
1079     bindToController: true,
1080     controller: Controller,
1081     controllerAs: '$mdSelect',
1082     link: postLink,
1083     require: ['mdSelect', '^^mdTable'],
1084     restrict: 'A',
1085     scope: {
1086       model: '=mdSelect',
1087       disabled: '=ngDisabled',
1088       onSelect: '=?mdOnSelect',
1089       onDeselect: '=?mdOnDeselect',
1090       autoSelect: '=mdAutoSelect'
1091     }
1092   };
1093 }
1094
1095 mdSelect.$inject = ['$compile', '$parse'];
1096
1097 angular.module('md.data.table').directive('mdTable', mdTable);
1098
1099 function Hash() {
1100   var keys = {};
1101   
1102   this.equals = function (key, item) {
1103     return keys[key] === item;
1104   };
1105
1106   this.get = function (key) {
1107     return keys[key];
1108   };
1109   
1110   this.has = function (key) {
1111     return keys.hasOwnProperty(key);
1112   };
1113
1114   this.purge = function (key) {
1115     delete keys[key];
1116   };
1117   
1118   this.update = function (key, item) {
1119     keys[key] = item;
1120   };
1121 }
1122
1123 function mdTable() {
1124   
1125   function compile(tElement, tAttrs) {
1126     tElement.addClass('md-table');
1127     
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>');
1131       
1132       if(body) {
1133         tElement[0].insertBefore(progress[0], body);
1134       }
1135     }
1136   }
1137   
1138   function Controller($attrs, $element, $q, $scope) {
1139     var self = this;
1140     var queue = [];
1141     var watchListener;
1142     var modelChangeListeners = [];
1143     
1144     self.$$hash = new Hash();
1145     self.$$columns = {};
1146     
1147     function enableRowSelection() {
1148       self.$$rowSelect = true;
1149       
1150       watchListener = $scope.$watchCollection('$mdTable.selected', function (selected) {
1151         modelChangeListeners.forEach(function (listener) {
1152           listener(selected);
1153         });
1154       });
1155       
1156       $element.addClass('md-row-select');
1157     }
1158     
1159     function disableRowSelection() {
1160       self.$$rowSelect = false;
1161       
1162       if(angular.isFunction(watchListener)) {
1163         watchListener();
1164       }
1165       
1166       $element.removeClass('md-row-select');
1167     }
1168     
1169     function resolvePromises() {
1170       if(!queue.length) {
1171         return $scope.$applyAsync();
1172       }
1173       
1174       queue[0]['finally'](function () {
1175         queue.shift();
1176         resolvePromises();
1177       });
1178     }
1179     
1180     function rowSelect() {
1181       return $attrs.mdRowSelect === '' || self.rowSelect;
1182     }
1183     
1184     function validateModel() {
1185       if(!self.selected) {
1186         return console.error('Row selection: ngModel is not defined.');
1187       }
1188       
1189       if(!angular.isArray(self.selected)) {
1190         return console.error('Row selection: Expected an array. Recived ' + typeof self.selected + '.');
1191       }
1192       
1193       return true;
1194     }
1195     
1196     self.columnCount = function () {
1197       return self.getRows($element[0]).reduce(function (count, row) {
1198         return row.cells.length > count ? row.cells.length : count;
1199       }, 0);
1200     };
1201     
1202     self.getRows = function (element) {
1203       return Array.prototype.filter.call(element.rows, function (row) {
1204         return !row.classList.contains('ng-leave');
1205       });
1206     };
1207     
1208     self.getBodyRows = function () {
1209       return Array.prototype.reduce.call($element.prop('tBodies'), function (result, tbody) {
1210         return result.concat(self.getRows(tbody));
1211       }, []);
1212     };
1213     
1214     self.getElement = function () {
1215       return $element;
1216     };
1217     
1218     self.getHeaderRows = function () {
1219       return self.getRows($element.prop('tHead'));
1220     };
1221     
1222     self.enableMultiSelect = function () {
1223       return $attrs.multiple === '' || $scope.$eval($attrs.multiple);
1224     };
1225     
1226     self.waitingOnPromise = function () {
1227       return !!queue.length;
1228     };
1229     
1230     self.queuePromise = function (promise) {
1231       if(!promise) {
1232         return;
1233       }
1234       
1235       if(queue.push(angular.isArray(promise) ? $q.all(promise) : $q.when(promise)) === 1) {
1236         resolvePromises();
1237       }
1238     };
1239     
1240     self.registerModelChangeListener = function (listener) {
1241       modelChangeListeners.push(listener);
1242     };
1243     
1244     self.removeModelChangeListener = function (listener) {
1245       var index = modelChangeListeners.indexOf(listener);
1246       
1247       if(index !== -1) {
1248         modelChangeListeners.splice(index, 1);
1249       }
1250     };
1251     
1252     if($attrs.hasOwnProperty('mdProgress')) {
1253       $scope.$watch('$mdTable.progress', self.queuePromise);
1254     }
1255     
1256     $scope.$watch(rowSelect, function (enable) {
1257       if(enable && !!validateModel()) {
1258         enableRowSelection();
1259       } else {
1260         disableRowSelection();
1261       }
1262     });
1263   }
1264   
1265   Controller.$inject = ['$attrs', '$element', '$q', '$scope'];
1266   
1267   return {
1268     bindToController: true,
1269     compile: compile,
1270     controller: Controller,
1271     controllerAs: '$mdTable',
1272     restrict: 'A',
1273     scope: {
1274       progress: '=?mdProgress',
1275       selected: '=ngModel',
1276       rowSelect: '=mdRowSelect'
1277     }
1278   };
1279 }
1280
1281 angular.module('md.data.table').directive('mdTablePagination', mdTablePagination);
1282
1283 function mdTablePagination() {
1284
1285   function compile(tElement) {
1286     tElement.addClass('md-table-pagination');
1287   }
1288
1289   function Controller($attrs, $mdUtil, $scope) {
1290     var self = this;
1291     var defaultLabel = {
1292       page: 'Page:',
1293       rowsPerPage: 'Rows per page:',
1294       of: 'of'
1295     };
1296
1297     self.label = angular.copy(defaultLabel);
1298
1299     function isPositive(number) {
1300       return parseInt(number, 10) > 0;
1301     }
1302
1303     self.eval = function (expression) {
1304       return $scope.$eval(expression);
1305     };
1306
1307     self.first = function () {
1308       self.page = 1;
1309       self.onPaginationChange();
1310     };
1311
1312     self.hasNext = function () {
1313       return self.page * self.limit < self.total;
1314     };
1315
1316     self.hasPrevious = function () {
1317       return self.page > 1;
1318     };
1319
1320     self.last = function () {
1321       self.page = self.pages();
1322       self.onPaginationChange();
1323     };
1324
1325     self.max = function () {
1326       return self.hasNext() ? self.page * self.limit : self.total;
1327     };
1328
1329     self.min = function () {
1330       return isPositive(self.total) ? self.page * self.limit - self.limit + 1 : 0;
1331     };
1332
1333     self.next = function () {
1334       self.page++;
1335       self.onPaginationChange();
1336     };
1337
1338     self.onPaginationChange = function () {
1339       if(angular.isFunction(self.onPaginate)) {
1340         $mdUtil.nextTick(function () {
1341           self.onPaginate(self.page, self.limit);
1342         });
1343       }
1344     };
1345
1346     self.pages = function () {
1347       return isPositive(self.total) ? Math.ceil(self.total / (isPositive(self.limit) ? self.limit : 1)) : 1;
1348     };
1349
1350     self.previous = function () {
1351       self.page--;
1352       self.onPaginationChange();
1353     };
1354
1355     self.showBoundaryLinks = function () {
1356       return $attrs.mdBoundaryLinks === '' || self.boundaryLinks;
1357     };
1358
1359     self.showPageSelect = function () {
1360       return $attrs.mdPageSelect === '' || self.pageSelect;
1361     };
1362
1363     $scope.$watch('$pagination.limit', function (newValue, oldValue) {
1364       if(isNaN(newValue) || isNaN(oldValue) || newValue === oldValue) {
1365         return;
1366       }
1367
1368       // find closest page from previous min
1369       self.page = Math.floor(((self.page * oldValue - oldValue) + newValue) / (isPositive(newValue) ? newValue : 1));
1370       self.onPaginationChange();
1371     });
1372
1373     $attrs.$observe('mdLabel', function (label) {
1374       angular.extend(self.label, defaultLabel, $scope.$eval(label));
1375     });
1376
1377     $scope.$watch('$pagination.total', function (newValue, oldValue) {
1378       if(isNaN(newValue) || newValue === oldValue) {
1379         return;
1380       }
1381
1382       if(self.page > self.pages()) {
1383         self.last();
1384       }
1385     });
1386   }
1387
1388   Controller.$inject = ['$attrs', '$mdUtil', '$scope'];
1389
1390   return {
1391     bindToController: {
1392       boundaryLinks: '=?mdBoundaryLinks',
1393       disabled: '=ngDisabled',
1394       limit: '=mdLimit',
1395       page: '=mdPage',
1396       pageSelect: '=?mdPageSelect',
1397       onPaginate: '=?mdOnPaginate',
1398       limitOptions: '=?mdLimitOptions',
1399       total: '@mdTotal'
1400     },
1401     compile: compile,
1402     controller: Controller,
1403     controllerAs: '$pagination',
1404     restrict: 'E',
1405     scope: {},
1406     templateUrl: 'md-table-pagination.html'
1407   };
1408 }
1409
1410 angular.module('md.data.table').directive('mdTableProgress', mdTableProgress);
1411
1412 function mdTableProgress() {
1413
1414   function postLink(scope, element, attrs, tableCtrl) {
1415     scope.columnCount = tableCtrl.columnCount;
1416     scope.deferred = tableCtrl.waitingOnPromise;
1417   }
1418
1419   return {
1420     link: postLink,
1421     require: '^^mdTable',
1422     restrict: 'A',
1423     scope: {},
1424     templateUrl: 'md-table-progress.html'
1425   };
1426 }
1427
1428 angular.module('md.data.table').directive('virtualPageSelect', virtualPageSelect);
1429
1430 function virtualPageSelect() {
1431
1432   function Controller($element, $scope) {
1433     var self = this;
1434     var content = $element.find('md-content');
1435
1436     self.pages = [];
1437
1438     function getMin(pages, total) {
1439       return Math.min(pages, isFinite(total) && isPositive(total) ? total : 1);
1440     }
1441
1442     function isPositive(number) {
1443       return number > 0;
1444     }
1445
1446     function setPages(max) {
1447       if(self.pages.length > max) {
1448         return self.pages.splice(max);
1449       }
1450
1451       for(var i = self.pages.length; i < max; i++) {
1452         self.pages.push(i + 1);
1453       }
1454     }
1455
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));
1460         });
1461       }
1462     });
1463
1464     $scope.$watch('$pageSelect.total', function (total) {
1465       setPages(getMin(Math.max(self.pages.length, 10), total));
1466     });
1467
1468     $scope.$watch('$pagination.page', function (page) {
1469       for(var i = self.pages.length; i < page; i++) {
1470         self.pages.push(i + 1);
1471       }
1472     });
1473   }
1474
1475   Controller.$inject = ['$element', '$scope'];
1476
1477   return {
1478     bindToController: {
1479       total: '@'
1480     },
1481     controller: Controller,
1482     controllerAs: '$pageSelect'
1483   };
1484 }
1485
1486 })(window, angular);