nexus site path corrected
[portal.git] / ecomp-portal-FE / client / bower_components / angular-material / modules / js / tabs / tabs.js
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v0.9.8
6  */
7 (function( window, angular, undefined ){
8 "use strict";
9
10 /**
11  * @ngdoc module
12  * @name material.components.tabs
13  * @description
14  *
15  *  Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
16  *  The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
17  *
18  *  Features include support for:
19  *
20  *  - static or dynamic tabs,
21  *  - responsive designs,
22  *  - accessibility support (ARIA),
23  *  - tab pagination,
24  *  - external or internal tab content,
25  *  - focus indicators and arrow-key navigations,
26  *  - programmatic lookup and access to tab controllers, and
27  *  - dynamic transitions through different tab contents.
28  *
29  */
30 /*
31  * @see js folder for tabs implementation
32  */
33 angular.module('material.components.tabs', [
34   'material.core',
35   'material.components.icon'
36 ]);
37
38 /**
39  * @ngdoc directive
40  * @name mdTab
41  * @module material.components.tabs
42  *
43  * @restrict E
44  *
45  * @description
46  * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
47  *
48  * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
49  * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
50  * markup of the `<md-tab>` is used as the tab header markup.
51  *
52  * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag.  This
53  * is to define a clear separation between the tab content and the tab label.
54  *
55  * If a tab **label** has been identified, then any **non-**`<md-tab-label>` markup
56  * will be considered tab content and will be transcluded to the internal `<div class="md-tabs-content">` container.
57  *
58  * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
59  * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
60  * be initiated via data binding changes, programmatic invocation, or user gestures.
61  *
62  * @param {string=} label Optional attribute to specify a simple string as the tab label
63  * @param {boolean=} disabled If present, disabled tab selection.
64  * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
65  * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
66  *
67  *
68  * @usage
69  *
70  * <hljs lang="html">
71  * <md-tab label="" disabled="" md-on-select="" md-on-deselect="" >
72  *   <h3>My Tab content</h3>
73  * </md-tab>
74  *
75  * <md-tab >
76  *   <md-tab-label>
77  *     <h3>My Tab content</h3>
78  *   </md-tab-label>
79  *   <md-tab-body>
80  *     <p>
81  *       Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
82  *       totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
83  *       dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
84  *       sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
85  *     </p>
86  *   </md-tab-body>
87  * </md-tab>
88  * </hljs>
89  *
90  */
91 angular
92     .module('material.components.tabs')
93     .directive('mdTab', MdTab);
94
95 function MdTab () {
96   return {
97     require: '^?mdTabs',
98     terminal: true,
99     template: function (element, attr) {
100       var label = getLabel(),
101           body  = getTemplate();
102       return '' +
103           '<md-tab-label>' + label + '</md-tab-label>' +
104           '<md-tab-body>' + body + '</md-tab-body>';
105       function getLabel () {
106         return getLabelElement() || getLabelAttribute() || getElementContents();
107         function getLabelAttribute () { return attr.label; }
108         function getLabelElement () {
109           var label = element.find('md-tab-label');
110           if (label.length) return label.remove().html();
111         }
112         function getElementContents () {
113           var html = element.html();
114           element.empty();
115           return html;
116         }
117       }
118       function getTemplate () {
119         var content = element.find('md-tab-body'),
120             template = content.length ? content.html() : attr.label ? element.html() : '';
121         if (content.length) content.remove();
122         else if (attr.label) element.empty();
123         return template;
124       }
125     },
126     scope: {
127       active:   '=?mdActive',
128       disabled: '=?ngDisabled',
129       select:   '&?mdOnSelect',
130       deselect: '&?mdOnDeselect'
131     },
132     link: postLink
133   };
134
135   function postLink (scope, element, attr, ctrl) {
136     if (!ctrl) return;
137     var tabs = element.parent()[0].getElementsByTagName('md-tab'),
138         index = Array.prototype.indexOf.call(tabs, element[0]),
139         body = element.find('md-tab-body').remove(),
140         label = element.find('md-tab-label').remove(),
141         data = ctrl.insertTab({
142           scope:    scope,
143           parent:   scope.$parent,
144           index:    index,
145           element:  element,
146           template: body.html(),
147           label:    label.html()
148         }, index);
149
150     scope.select   = scope.select   || angular.noop;
151     scope.deselect = scope.deselect || angular.noop;
152
153     scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex()); });
154     scope.$watch('disabled', function () { ctrl.refreshIndex(); });
155     scope.$watch(
156         function () {
157           return Array.prototype.indexOf.call(tabs, element[0]);
158         },
159         function (newIndex) {
160           data.index = newIndex;
161           ctrl.updateTabOrder();
162         }
163     );
164     scope.$on('$destroy', function () { ctrl.removeTab(data); });
165
166   }
167 }
168
169 angular
170     .module('material.components.tabs')
171     .directive('mdTabItem', MdTabItem);
172
173 function MdTabItem () {
174   return {
175     require: '^?mdTabs',
176     link: function link (scope, element, attr, ctrl) {
177       if (!ctrl) return;
178       ctrl.attachRipple(scope, element);
179     }
180   };
181 }
182
183 angular
184     .module('material.components.tabs')
185     .directive('mdTabLabel', MdTabLabel);
186
187 function MdTabLabel () {
188   return { terminal: true };
189 }
190
191
192 angular.module('material.components.tabs')
193     .directive('mdTabScroll', MdTabScroll);
194
195 function MdTabScroll ($parse) {
196   return {
197     restrict: 'A',
198     compile: function ($element, attr) {
199       var fn = $parse(attr.mdTabScroll, null, true);
200       return function ngEventHandler (scope, element) {
201         element.on('mousewheel', function (event) {
202           scope.$apply(function () { fn(scope, { $event: event }); });
203         });
204       };
205     }
206   }
207 }
208 MdTabScroll.$inject = ["$parse"];
209
210 angular
211     .module('material.components.tabs')
212     .controller('MdTabsController', MdTabsController);
213
214 /**
215  * ngInject
216  */
217 function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdTabInkRipple,
218                            $mdUtil, $animate) {
219   var ctrl      = this,
220       locked    = false,
221       elements  = getElements(),
222       queue     = [],
223       destroyed = false;
224
225   ctrl.scope = $scope;
226   ctrl.parent = $scope.$parent;
227   ctrl.tabs = [];
228   ctrl.lastSelectedIndex = null;
229   ctrl.focusIndex = $scope.selectedIndex || 0;
230   ctrl.offsetLeft = 0;
231   ctrl.hasContent = false;
232   ctrl.hasFocus = false;
233   ctrl.lastClick = true;
234
235   ctrl.redirectFocus = redirectFocus;
236   ctrl.attachRipple = attachRipple;
237   ctrl.shouldStretchTabs = shouldStretchTabs;
238   ctrl.shouldPaginate = shouldPaginate;
239   ctrl.shouldCenterTabs = shouldCenterTabs;
240   ctrl.insertTab = insertTab;
241   ctrl.removeTab = removeTab;
242   ctrl.select = select;
243   ctrl.scroll = scroll;
244   ctrl.nextPage = nextPage;
245   ctrl.previousPage = previousPage;
246   ctrl.keydown = keydown;
247   ctrl.canPageForward = canPageForward;
248   ctrl.canPageBack = canPageBack;
249   ctrl.refreshIndex = refreshIndex;
250   ctrl.incrementSelectedIndex = incrementSelectedIndex;
251   ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
252   ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
253
254   init();
255
256   function init () {
257     $scope.$watch('selectedIndex', handleSelectedIndexChange);
258     $scope.$watch('$mdTabsCtrl.focusIndex', handleFocusIndexChange);
259     $scope.$watch('$mdTabsCtrl.offsetLeft', handleOffsetChange);
260     $scope.$watch('$mdTabsCtrl.hasContent', handleHasContent);
261     angular.element($window).on('resize', handleWindowResize);
262     angular.element(elements.paging).on('DOMSubtreeModified', ctrl.updateInkBarStyles);
263     $timeout(updateHeightFromContent, 0, false);
264     $timeout(adjustOffset);
265     $scope.$on('$destroy', cleanup);
266   }
267
268   function cleanup () {
269     destroyed = true;
270     angular.element($window).off('resize', handleWindowResize);
271     angular.element(elements.paging).off('DOMSubtreeModified', ctrl.updateInkBarStyles);
272   }
273
274   //-- Change handlers
275
276   function handleHasContent (hasContent) {
277     $element[hasContent ? 'removeClass' : 'addClass']('md-no-tab-content');
278   }
279
280   function handleOffsetChange (left) {
281     var newValue = shouldCenterTabs() ? '' : '-' + left + 'px';
282     angular.element(elements.paging).css('transform', 'translate3d(' + newValue + ', 0, 0)');
283     $scope.$broadcast('$mdTabsPaginationChanged');
284   }
285
286   function handleFocusIndexChange (newIndex, oldIndex) {
287     if (newIndex === oldIndex) return;
288     if (!elements.tabs[newIndex]) return;
289     adjustOffset();
290     redirectFocus();
291   }
292
293   function handleSelectedIndexChange (newValue, oldValue) {
294     if (newValue === oldValue) return;
295
296     $scope.selectedIndex = getNearestSafeIndex(newValue);
297     ctrl.lastSelectedIndex = oldValue;
298     ctrl.updateInkBarStyles();
299     updateHeightFromContent();
300     $scope.$broadcast('$mdTabsChanged');
301     ctrl.tabs[oldValue] && ctrl.tabs[oldValue].scope.deselect();
302     ctrl.tabs[newValue] && ctrl.tabs[newValue].scope.select();
303   }
304
305   function handleResizeWhenVisible () {
306     //-- if there is already a watcher waiting for resize, do nothing
307     if (handleResizeWhenVisible.watcher) return;
308     //-- otherwise, we will abuse the $watch function to check for visible
309     handleResizeWhenVisible.watcher = $scope.$watch(function () {
310       //-- since we are checking for DOM size, we use $timeout to wait for after the DOM updates
311       $timeout(function () {
312         //-- if the watcher has already run (ie. multiple digests in one cycle), do nothing
313         if (!handleResizeWhenVisible.watcher) return;
314
315         if ($element.prop('offsetParent')) {
316           handleResizeWhenVisible.watcher();
317           handleResizeWhenVisible.watcher = null;
318
319           //-- we have to trigger our own $apply so that the DOM bindings will update
320           handleWindowResize();
321         }
322       }, 0, false);
323     });
324   }
325
326   //-- Event handlers / actions
327
328   function keydown (event) {
329     switch (event.keyCode) {
330       case $mdConstant.KEY_CODE.LEFT_ARROW:
331         event.preventDefault();
332         incrementSelectedIndex(-1, true);
333         break;
334       case $mdConstant.KEY_CODE.RIGHT_ARROW:
335         event.preventDefault();
336         incrementSelectedIndex(1, true);
337         break;
338       case $mdConstant.KEY_CODE.SPACE:
339       case $mdConstant.KEY_CODE.ENTER:
340         event.preventDefault();
341         if (!locked) $scope.selectedIndex = ctrl.focusIndex;
342         break;
343     }
344     ctrl.lastClick = false;
345   }
346
347   function select (index) {
348     if (!locked) ctrl.focusIndex = $scope.selectedIndex = index;
349     ctrl.lastClick = true;
350     ctrl.tabs[index].element.triggerHandler('click');
351   }
352
353   function scroll (event) {
354     if (!shouldPaginate()) return;
355     event.preventDefault();
356     ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
357   }
358
359   function nextPage () {
360     var viewportWidth = elements.canvas.clientWidth,
361         totalWidth = viewportWidth + ctrl.offsetLeft,
362         i, tab;
363     for (i = 0; i < elements.tabs.length; i++) {
364       tab = elements.tabs[i];
365       if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
366     }
367     ctrl.offsetLeft = fixOffset(tab.offsetLeft);
368   }
369
370   function previousPage () {
371     var i, tab;
372     for (i = 0; i < elements.tabs.length; i++) {
373       tab = elements.tabs[i];
374       if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
375     }
376     ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
377   }
378
379   function handleWindowResize () {
380     $scope.$apply(function () {
381       ctrl.lastSelectedIndex = $scope.selectedIndex;
382       ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
383       $timeout(ctrl.updateInkBarStyles, 0, false);
384     });
385   }
386
387   function removeTab (tabData) {
388     var selectedIndex = $scope.selectedIndex,
389         tab = ctrl.tabs.splice(tabData.getIndex(), 1)[0];
390     refreshIndex();
391     //-- when removing a tab, if the selected index did not change, we have to manually trigger the
392     //   tab select/deselect events
393     if ($scope.selectedIndex === selectedIndex && !destroyed) {
394       tab.scope.deselect();
395       ctrl.tabs[$scope.selectedIndex] && ctrl.tabs[$scope.selectedIndex].scope.select();
396     }
397     $timeout(function () {
398       ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
399     });
400   }
401
402   function insertTab (tabData, index) {
403     var proto = {
404           getIndex: function () { return ctrl.tabs.indexOf(tab); },
405           isActive: function () { return this.getIndex() === $scope.selectedIndex; },
406           isLeft:   function () { return this.getIndex() < $scope.selectedIndex; },
407           isRight:  function () { return this.getIndex() > $scope.selectedIndex; },
408           shouldRender: function () { return !$scope.noDisconnect || this.isActive(); },
409           hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; },
410           id:       $mdUtil.nextUid()
411         },
412         tab = angular.extend(proto, tabData);
413     if (angular.isDefined(index)) {
414       ctrl.tabs.splice(index, 0, tab);
415     } else {
416       ctrl.tabs.push(tab);
417     }
418     processQueue();
419     updateHasContent();
420     return tab;
421   }
422
423   //-- Getter methods
424
425   function getElements () {
426     var elements      = {};
427
428     //-- gather tab bar elements
429     elements.wrapper  = $element[0].getElementsByTagName('md-tabs-wrapper')[0];
430     elements.canvas   = elements.wrapper.getElementsByTagName('md-tabs-canvas')[0];
431     elements.paging   = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0];
432     elements.tabs     = elements.paging.getElementsByTagName('md-tab-item');
433     elements.dummies  = elements.canvas.getElementsByTagName('md-dummy-tab');
434     elements.inkBar   = elements.paging.getElementsByTagName('md-ink-bar')[0];
435
436     //-- gather tab content elements
437     elements.contentsWrapper = $element[0].getElementsByTagName('md-tabs-content-wrapper')[0];
438     elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content');
439
440     return elements;
441   }
442
443   function canPageBack () {
444     return ctrl.offsetLeft > 0;
445   }
446
447   function canPageForward () {
448     var lastTab = elements.tabs[elements.tabs.length - 1];
449     return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft;
450   }
451
452   function shouldStretchTabs () {
453     switch ($scope.stretchTabs) {
454       case 'always': return true;
455       case 'never':  return false;
456       default:       return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches;
457     }
458   }
459
460   function shouldCenterTabs () {
461     return $scope.centerTabs && !shouldPaginate();
462   }
463
464   function shouldPaginate () {
465     if ($scope.noPagination) return false;
466     var canvasWidth = $element.prop('clientWidth');
467     angular.forEach(elements.tabs, function (tab) { canvasWidth -= tab.offsetWidth; });
468     return canvasWidth < 0;
469   }
470
471   function getNearestSafeIndex(newIndex) {
472     var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
473         i, tab;
474     for (i = 0; i <= maxOffset; i++) {
475       tab = ctrl.tabs[newIndex + i];
476       if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
477       tab = ctrl.tabs[newIndex - i];
478       if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
479     }
480     return newIndex;
481   }
482
483   //-- Utility methods
484
485   function updateTabOrder () {
486     var selectedItem = ctrl.tabs[$scope.selectedIndex],
487         focusItem = ctrl.tabs[ctrl.focusIndex];
488     ctrl.tabs = ctrl.tabs.sort(function (a, b) {
489       return a.index - b.index;
490     });
491     $scope.selectedIndex = ctrl.tabs.indexOf(selectedItem);
492     ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
493   }
494
495   function incrementSelectedIndex (inc, focus) {
496     var newIndex,
497         index = focus ? ctrl.focusIndex : $scope.selectedIndex;
498     for (newIndex = index + inc;
499          ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled;
500          newIndex += inc) {}
501     if (ctrl.tabs[newIndex]) {
502       if (focus) ctrl.focusIndex = newIndex;
503       else $scope.selectedIndex = newIndex;
504     }
505   }
506
507   function redirectFocus () {
508     elements.dummies[ctrl.focusIndex].focus();
509   }
510
511   function adjustOffset () {
512     if (shouldCenterTabs()) return;
513     var tab = elements.tabs[ctrl.focusIndex],
514         left = tab.offsetLeft,
515         right = tab.offsetWidth + left;
516     ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth));
517     ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
518   }
519
520   function processQueue () {
521     queue.forEach(function (func) { $timeout(func); });
522     queue = [];
523   }
524
525   function updateHasContent () {
526     var hasContent = false;
527     angular.forEach(ctrl.tabs, function (tab) {
528       if (tab.template) hasContent = true;
529     });
530     ctrl.hasContent = hasContent;
531   }
532
533   function refreshIndex () {
534     $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex);
535     ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
536   }
537
538   function updateHeightFromContent () {
539     if (!$scope.dynamicHeight) return $element.css('height', '');
540     if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
541     var tabContent    = elements.contents[$scope.selectedIndex],
542         contentHeight = tabContent ? tabContent.offsetHeight : 0,
543         tabsHeight    = elements.wrapper.offsetHeight,
544         newHeight     = contentHeight + tabsHeight,
545         currentHeight = $element.prop('clientHeight');
546     if (currentHeight === newHeight) return;
547     locked = true;
548     $animate
549         .animate(
550           $element,
551           { height: currentHeight + 'px' },
552           { height: newHeight + 'px'}
553         )
554         .then(function () {
555           $element.css('height', '');
556           locked = false;
557         });
558   }
559
560   function updateInkBarStyles () {
561     if (!elements.tabs[$scope.selectedIndex]) return;
562     if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
563     //-- if the element is not visible, we will not be able to calculate sizes until it is
564     //-- we should treat that as a resize event rather than just updating the ink bar
565     if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
566     var index = $scope.selectedIndex,
567         totalWidth = elements.paging.offsetWidth,
568         tab = elements.tabs[index],
569         left = tab.offsetLeft,
570         right = totalWidth - left - tab.offsetWidth;
571     updateInkBarClassName();
572     angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
573   }
574
575   function updateInkBarClassName () {
576     var newIndex = $scope.selectedIndex,
577         oldIndex = ctrl.lastSelectedIndex,
578         ink = angular.element(elements.inkBar);
579     ink.removeClass('md-left md-right');
580     if (!angular.isNumber(oldIndex)) return;
581     if (newIndex < oldIndex) {
582       ink.addClass('md-left');
583     } else if (newIndex > oldIndex) {
584       ink.addClass('md-right');
585     }
586   }
587
588   function fixOffset (value) {
589     if (!elements.tabs.length || !shouldPaginate()) return 0;
590     var lastTab = elements.tabs[elements.tabs.length - 1],
591         totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
592     value = Math.max(0, value);
593     value = Math.min(totalWidth - elements.canvas.clientWidth, value);
594     return value;
595   }
596
597   function attachRipple (scope, element) {
598     var options = { colorElement: angular.element(elements.inkBar) };
599     $mdTabInkRipple.attach(scope, element, options);
600   }
601 }
602 MdTabsController.$inject = ["$scope", "$element", "$window", "$timeout", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animate"];
603
604 /**
605  * @ngdoc directive
606  * @name mdTabs
607  * @module material.components.tabs
608  *
609  * @restrict E
610  *
611  * @description
612  * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to produces a Tabs components.
613  * In turn, the nested `<md-tab>` directive is used to specify a tab label for the **header button** and a [optional] tab view
614  * content that will be associated with each tab button.
615  *
616  * Below is the markup for its simplest usage:
617  *
618  *  <hljs lang="html">
619  *  <md-tabs>
620  *    <md-tab label="Tab #1"></md-tab>
621  *    <md-tab label="Tab #2"></md-tab>
622  *    <md-tab label="Tab #3"></md-tab>
623  *  </md-tabs>
624  *  </hljs>
625  *
626  * Tabs supports three (3) usage scenarios:
627  *
628  *  1. Tabs (buttons only)
629  *  2. Tabs with internal view content
630  *  3. Tabs with external view content
631  *
632  * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any other components, content, or views.
633  * **Tabs with internal views** are the traditional usages where each tab has associated view content and the view switching is managed internally by the Tabs component.
634  * **Tabs with external view content** is often useful when content associated with each tab is independently managed and data-binding notifications announce tab selection changes.
635  *
636  * Additional features also include:
637  *
638  * *  Content can include any markup.
639  * *  If a tab is disabled while active/selected, then the next tab will be auto-selected.
640  *
641  * ### Explanation of tab stretching
642  *
643  * Initially, tabs will have an inherent size.  This size will either be defined by how much space is needed to accommodate their text or set by the user through CSS.  Calculations will be based on this size.
644  *
645  * On mobile devices, tabs will be expanded to fill the available horizontal space.  When this happens, all tabs will become the same size.
646  *
647  * On desktops, by default, stretching will never occur.
648  *
649  * This default behavior can be overridden through the `md-stretch-tabs` attribute.  Here is a table showing when stretching will occur:
650  *
651  * `md-stretch-tabs` | mobile    | desktop
652  * ------------------|-----------|--------
653  * `auto`            | stretched | ---
654  * `always`          | stretched | stretched
655  * `never`           | ---       | ---
656  *
657  * @param {integer=} md-selected Index of the active/selected tab
658  * @param {boolean=} md-no-ink If present, disables ink ripple effects.
659  * @param {boolean=} md-no-bar If present, disables the selection ink bar.
660  * @param {string=}  md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
661  * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
662  * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
663  * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
664  * @param {boolean=} md-no-pagination When enabled, pagination will remain off
665  * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
666  * @param {boolean=} md-no-disconnect If your tab content has background tasks (ie. event listeners), you will want to include this to prevent the scope from being disconnected
667  *
668  * @usage
669  * <hljs lang="html">
670  * <md-tabs md-selected="selectedIndex" >
671  *   <img ng-src="img/angular.png" class="centered">
672  *   <md-tab
673  *       ng-repeat="tab in tabs | orderBy:predicate:reversed"
674  *       md-on-select="onTabSelected(tab)"
675  *       md-on-deselect="announceDeselected(tab)"
676  *       ng-disabled="tab.disabled">
677  *     <md-tab-label>
678  *       {{tab.title}}
679  *       <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
680  *     </md-tab-label>
681  *     <md-tab-body>
682  *       {{tab.content}}
683  *     </md-tab-body>
684  *   </md-tab>
685  * </md-tabs>
686  * </hljs>
687  *
688  */
689 angular
690     .module('material.components.tabs')
691     .directive('mdTabs', MdTabs);
692
693 function MdTabs ($mdTheming, $mdUtil, $compile) {
694   return {
695     scope: {
696       noPagination:  '=?mdNoPagination',
697       dynamicHeight: '=?mdDynamicHeight',
698       centerTabs:    '=?mdCenterTabs',
699       selectedIndex: '=?mdSelected',
700       stretchTabs:   '@?mdStretchTabs',
701       swipeContent:  '=?mdSwipeContent',
702       noDisconnect:  '=?mdNoDisconnect'
703     },
704     template: function (element, attr) {
705       attr["$mdTabsTemplate"] = element.html();
706       return '\
707         <md-tabs-wrapper ng-class="{ \'md-stretch-tabs\': $mdTabsCtrl.shouldStretchTabs() }">\
708           <md-tab-data></md-tab-data>\
709           <md-prev-button\
710               tabindex="-1"\
711               role="button"\
712               aria-label="Previous Page"\
713               aria-disabled="{{!$mdTabsCtrl.canPageBack()}}"\
714               ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }"\
715               ng-if="$mdTabsCtrl.shouldPaginate()"\
716               ng-click="$mdTabsCtrl.previousPage()">\
717             <md-icon md-svg-icon="md-tabs-arrow"></md-icon>\
718           </md-prev-button>\
719           <md-next-button\
720               tabindex="-1"\
721               role="button"\
722               aria-label="Next Page"\
723               aria-disabled="{{!$mdTabsCtrl.canPageForward()}}"\
724               ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }"\
725               ng-if="$mdTabsCtrl.shouldPaginate()"\
726               ng-click="$mdTabsCtrl.nextPage()">\
727             <md-icon md-svg-icon="md-tabs-arrow"></md-icon>\
728           </md-next-button>\
729           <md-tabs-canvas\
730               tabindex="0"\
731               aria-activedescendant="tab-item-{{$mdTabsCtrl.tabs[$mdTabsCtrl.focusIndex].id}}"\
732               ng-focus="$mdTabsCtrl.redirectFocus()"\
733               ng-class="{\
734                   \'md-paginated\': $mdTabsCtrl.shouldPaginate(),\
735                   \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs()\
736               }"\
737               ng-keydown="$mdTabsCtrl.keydown($event)"\
738               role="tablist">\
739             <md-pagination-wrapper\
740                 ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs() }"\
741                 md-tab-scroll="$mdTabsCtrl.scroll($event)">\
742               <md-tab-item\
743                   tabindex="-1"\
744                   class="md-tab"\
745                   style="max-width: {{ tabWidth ? tabWidth + \'px\' : \'none\' }}"\
746                   ng-repeat="tab in $mdTabsCtrl.tabs"\
747                   role="tab"\
748                   aria-controls="tab-content-{{tab.id}}"\
749                   aria-selected="{{tab.isActive()}}"\
750                   aria-disabled="{{tab.scope.disabled || \'false\'}}"\
751                   ng-click="$mdTabsCtrl.select(tab.getIndex())"\
752                   ng-class="{\
753                       \'md-active\':    tab.isActive(),\
754                       \'md-focused\':   tab.hasFocus(),\
755                       \'md-disabled\':  tab.scope.disabled\
756                   }"\
757                   ng-disabled="tab.scope.disabled"\
758                   md-swipe-left="$mdTabsCtrl.nextPage()"\
759                   md-swipe-right="$mdTabsCtrl.previousPage()"\
760                   md-template="tab.label"\
761                   md-scope="tab.parent"></md-tab-item>\
762               <md-ink-bar ng-hide="noInkBar"></md-ink-bar>\
763             </md-pagination-wrapper>\
764             <div class="md-visually-hidden md-dummy-wrapper">\
765               <md-dummy-tab\
766                   tabindex="-1"\
767                   id="tab-item-{{tab.id}}"\
768                   role="tab"\
769                   aria-controls="tab-content-{{tab.id}}"\
770                   aria-selected="{{tab.isActive()}}"\
771                   aria-disabled="{{tab.scope.disabled || \'false\'}}"\
772                   ng-focus="$mdTabsCtrl.hasFocus = true"\
773                   ng-blur="$mdTabsCtrl.hasFocus = false"\
774                   ng-repeat="tab in $mdTabsCtrl.tabs"\
775                   md-template="tab.label"\
776                   md-scope="tab.parent"></md-dummy-tab>\
777             </div>\
778           </md-tabs-canvas>\
779         </md-tabs-wrapper>\
780         <md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent">\
781           <md-tab-content\
782               id="tab-content-{{tab.id}}"\
783               role="tabpanel"\
784               aria-labelledby="tab-item-{{tab.id}}"\
785               md-swipe-left="swipeContent && $mdTabsCtrl.incrementSelectedIndex(1)"\
786               md-swipe-right="swipeContent && $mdTabsCtrl.incrementSelectedIndex(-1)"\
787               ng-if="$mdTabsCtrl.hasContent"\
788               ng-repeat="(index, tab) in $mdTabsCtrl.tabs"\
789               md-connected-if="tab.isActive()"\
790               ng-class="{\
791                 \'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null,\
792                 \'md-active\':        tab.isActive(),\
793                 \'md-left\':          tab.isLeft(),\
794                 \'md-right\':         tab.isRight(),\
795                 \'md-no-scroll\':     dynamicHeight\
796               }">\
797             <div\
798                 md-template="tab.template"\
799                 md-scope="tab.parent"\
800                 ng-if="tab.shouldRender()"></div>\
801           </md-tab-content>\
802         </md-tabs-content-wrapper>\
803       ';
804     },
805     controller: 'MdTabsController',
806     controllerAs: '$mdTabsCtrl',
807     link: function (scope, element, attr) {
808       compileTabData(attr.$mdTabsTemplate);
809       delete attr.$mdTabsTemplate;
810
811       $mdUtil.initOptionalProperties(scope, attr);
812
813       //-- watch attributes
814       attr.$observe('mdNoBar', function (value) { scope.noInkBar = angular.isDefined(value); });
815       //-- set default value for selectedIndex
816       scope.selectedIndex = angular.isNumber(scope.selectedIndex) ? scope.selectedIndex : 0;
817       //-- apply themes
818       $mdTheming(element);
819
820       function compileTabData (template) {
821         var dataElement = element.find('md-tab-data');
822         dataElement.html(template);
823         $compile(dataElement.contents())(scope.$parent);
824       }
825     }
826   };
827 }
828 MdTabs.$inject = ["$mdTheming", "$mdUtil", "$compile"];
829
830 angular
831     .module('material.components.tabs')
832     .directive('mdTemplate', MdTemplate);
833
834 function MdTemplate ($compile, $mdUtil, $timeout) {
835   return {
836     restrict: 'A',
837     link: link,
838     scope: {
839       template: '=mdTemplate',
840       compileScope: '=mdScope',
841       connected: '=?mdConnectedIf'
842     },
843     require: '^?mdTabs'
844   };
845   function link (scope, element, attr, ctrl) {
846     if (!ctrl) return;
847     var compileScope = scope.compileScope.$new();
848     element.html(scope.template);
849     $compile(element.contents())(compileScope);
850     return $timeout(handleScope);
851     function handleScope () {
852       scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
853       scope.$on('$destroy', reconnect);
854     }
855     function disconnect () {
856       if (ctrl.scope.noDisconnect) return;
857       $mdUtil.disconnectScope(compileScope);
858     }
859     function reconnect () {
860       if (ctrl.scope.noDisconnect) return;
861       $mdUtil.reconnectScope(compileScope);
862     }
863   }
864 }
865 MdTemplate.$inject = ["$compile", "$mdUtil", "$timeout"];
866
867 })(window, window.angular);