2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.tabs');
8 goog.require('ng.material.components.icon');
9 goog.require('ng.material.core');
12 * @name material.components.tabs
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.
18 * Features include support for:
20 * - static or dynamic tabs,
21 * - responsive designs,
22 * - accessibility support (ARIA),
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.
31 * @see js folder for tabs implementation
33 angular.module('material.components.tabs', [
35 'material.components.icon'
41 * @module material.components.tabs
46 * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
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.
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.
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.
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.
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.
71 * <md-tab label="" disabled="" md-on-select="" md-on-deselect="" >
72 * <h3>My Tab content</h3>
77 * <h3>My Tab content</h3>
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.
92 .module('material.components.tabs')
93 .directive('mdTab', MdTab);
99 template: function (element, attr) {
100 var label = getLabel(),
101 body = getTemplate();
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();
112 function getElementContents () {
113 var html = element.html();
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();
127 active: '=?mdActive',
128 disabled: '=?ngDisabled',
129 select: '&?mdOnSelect',
130 deselect: '&?mdOnDeselect'
135 function postLink (scope, element, attr, ctrl) {
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({
143 parent: scope.$parent,
146 template: body.html(),
150 scope.select = scope.select || angular.noop;
151 scope.deselect = scope.deselect || angular.noop;
153 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex()); });
154 scope.$watch('disabled', function () { ctrl.refreshIndex(); });
157 return Array.prototype.indexOf.call(tabs, element[0]);
159 function (newIndex) {
160 data.index = newIndex;
161 ctrl.updateTabOrder();
164 scope.$on('$destroy', function () { ctrl.removeTab(data); });
170 .module('material.components.tabs')
171 .directive('mdTabItem', MdTabItem);
173 function MdTabItem () {
176 link: function link (scope, element, attr, ctrl) {
178 ctrl.attachRipple(scope, element);
184 .module('material.components.tabs')
185 .directive('mdTabLabel', MdTabLabel);
187 function MdTabLabel () {
188 return { terminal: true };
192 angular.module('material.components.tabs')
193 .directive('mdTabScroll', MdTabScroll);
195 function MdTabScroll ($parse) {
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 }); });
208 MdTabScroll.$inject = ["$parse"];
211 .module('material.components.tabs')
212 .controller('MdTabsController', MdTabsController);
217 function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $mdTabInkRipple,
221 elements = getElements(),
226 ctrl.parent = $scope.$parent;
228 ctrl.lastSelectedIndex = null;
229 ctrl.focusIndex = $scope.selectedIndex || 0;
231 ctrl.hasContent = false;
232 ctrl.hasFocus = false;
233 ctrl.lastClick = true;
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);
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);
268 function cleanup () {
270 angular.element($window).off('resize', handleWindowResize);
271 angular.element(elements.paging).off('DOMSubtreeModified', ctrl.updateInkBarStyles);
276 function handleHasContent (hasContent) {
277 $element[hasContent ? 'removeClass' : 'addClass']('md-no-tab-content');
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');
286 function handleFocusIndexChange (newIndex, oldIndex) {
287 if (newIndex === oldIndex) return;
288 if (!elements.tabs[newIndex]) return;
293 function handleSelectedIndexChange (newValue, oldValue) {
294 if (newValue === oldValue) return;
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();
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;
315 if ($element.prop('offsetParent')) {
316 handleResizeWhenVisible.watcher();
317 handleResizeWhenVisible.watcher = null;
319 //-- we have to trigger our own $apply so that the DOM bindings will update
320 handleWindowResize();
326 //-- Event handlers / actions
328 function keydown (event) {
329 switch (event.keyCode) {
330 case $mdConstant.KEY_CODE.LEFT_ARROW:
331 event.preventDefault();
332 incrementSelectedIndex(-1, true);
334 case $mdConstant.KEY_CODE.RIGHT_ARROW:
335 event.preventDefault();
336 incrementSelectedIndex(1, true);
338 case $mdConstant.KEY_CODE.SPACE:
339 case $mdConstant.KEY_CODE.ENTER:
340 event.preventDefault();
341 if (!locked) $scope.selectedIndex = ctrl.focusIndex;
344 ctrl.lastClick = false;
347 function select (index) {
348 if (!locked) ctrl.focusIndex = $scope.selectedIndex = index;
349 ctrl.lastClick = true;
350 ctrl.tabs[index].element.triggerHandler('click');
353 function scroll (event) {
354 if (!shouldPaginate()) return;
355 event.preventDefault();
356 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
359 function nextPage () {
360 var viewportWidth = elements.canvas.clientWidth,
361 totalWidth = viewportWidth + ctrl.offsetLeft,
363 for (i = 0; i < elements.tabs.length; i++) {
364 tab = elements.tabs[i];
365 if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
367 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
370 function previousPage () {
372 for (i = 0; i < elements.tabs.length; i++) {
373 tab = elements.tabs[i];
374 if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
376 ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
379 function handleWindowResize () {
380 $scope.$apply(function () {
381 ctrl.lastSelectedIndex = $scope.selectedIndex;
382 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
383 $timeout(ctrl.updateInkBarStyles, 0, false);
387 function removeTab (tabData) {
388 var selectedIndex = $scope.selectedIndex,
389 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[0];
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();
397 $timeout(function () {
398 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
402 function insertTab (tabData, index) {
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()
412 tab = angular.extend(proto, tabData);
413 if (angular.isDefined(index)) {
414 ctrl.tabs.splice(index, 0, tab);
425 function getElements () {
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];
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');
443 function canPageBack () {
444 return ctrl.offsetLeft > 0;
447 function canPageForward () {
448 var lastTab = elements.tabs[elements.tabs.length - 1];
449 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft;
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;
460 function shouldCenterTabs () {
461 return $scope.centerTabs && !shouldPaginate();
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;
471 function getNearestSafeIndex(newIndex) {
472 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
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();
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;
491 $scope.selectedIndex = ctrl.tabs.indexOf(selectedItem);
492 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
495 function incrementSelectedIndex (inc, focus) {
497 index = focus ? ctrl.focusIndex : $scope.selectedIndex;
498 for (newIndex = index + inc;
499 ctrl.tabs[newIndex] && ctrl.tabs[newIndex].scope.disabled;
501 if (ctrl.tabs[newIndex]) {
502 if (focus) ctrl.focusIndex = newIndex;
503 else $scope.selectedIndex = newIndex;
507 function redirectFocus () {
508 elements.dummies[ctrl.focusIndex].focus();
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));
520 function processQueue () {
521 queue.forEach(function (func) { $timeout(func); });
525 function updateHasContent () {
526 var hasContent = false;
527 angular.forEach(ctrl.tabs, function (tab) {
528 if (tab.template) hasContent = true;
530 ctrl.hasContent = hasContent;
533 function refreshIndex () {
534 $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex);
535 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
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;
551 { height: currentHeight + 'px' },
552 { height: newHeight + 'px'}
555 $element.css('height', '');
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' });
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');
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);
597 function attachRipple (scope, element) {
598 var options = { colorElement: angular.element(elements.inkBar) };
599 $mdTabInkRipple.attach(scope, element, options);
602 MdTabsController.$inject = ["$scope", "$element", "$window", "$timeout", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animate"];
607 * @module material.components.tabs
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.
616 * Below is the markup for its simplest usage:
620 * <md-tab label="Tab #1"></md-tab>
621 * <md-tab label="Tab #2"></md-tab>
622 * <md-tab label="Tab #3"></md-tab>
626 * Tabs supports three (3) usage scenarios:
628 * 1. Tabs (buttons only)
629 * 2. Tabs with internal view content
630 * 3. Tabs with external view content
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.
636 * Additional features also include:
638 * * Content can include any markup.
639 * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
641 * ### Explanation of tab stretching
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.
645 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
647 * On desktops, by default, stretching will never occur.
649 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
651 * `md-stretch-tabs` | mobile | desktop
652 * ------------------|-----------|--------
653 * `auto` | stretched | ---
654 * `always` | stretched | stretched
655 * `never` | --- | ---
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
670 * <md-tabs md-selected="selectedIndex" >
671 * <img ng-src="img/angular.png" class="centered">
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">
679 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
690 .module('material.components.tabs')
691 .directive('mdTabs', MdTabs);
693 function MdTabs ($mdTheming, $mdUtil, $compile) {
696 noPagination: '=?mdNoPagination',
697 dynamicHeight: '=?mdDynamicHeight',
698 centerTabs: '=?mdCenterTabs',
699 selectedIndex: '=?mdSelected',
700 stretchTabs: '@?mdStretchTabs',
701 swipeContent: '=?mdSwipeContent',
702 noDisconnect: '=?mdNoDisconnect'
704 template: function (element, attr) {
705 attr["$mdTabsTemplate"] = element.html();
707 <md-tabs-wrapper ng-class="{ \'md-stretch-tabs\': $mdTabsCtrl.shouldStretchTabs() }">\
708 <md-tab-data></md-tab-data>\
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>\
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>\
731 aria-activedescendant="tab-item-{{$mdTabsCtrl.tabs[$mdTabsCtrl.focusIndex].id}}"\
732 ng-focus="$mdTabsCtrl.redirectFocus()"\
734 \'md-paginated\': $mdTabsCtrl.shouldPaginate(),\
735 \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs()\
737 ng-keydown="$mdTabsCtrl.keydown($event)"\
739 <md-pagination-wrapper\
740 ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs() }"\
741 md-tab-scroll="$mdTabsCtrl.scroll($event)">\
745 style="max-width: {{ tabWidth ? tabWidth + \'px\' : \'none\' }}"\
746 ng-repeat="tab in $mdTabsCtrl.tabs"\
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())"\
753 \'md-active\': tab.isActive(),\
754 \'md-focused\': tab.hasFocus(),\
755 \'md-disabled\': tab.scope.disabled\
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">\
767 id="tab-item-{{tab.id}}"\
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>\
780 <md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent">\
782 id="tab-content-{{tab.id}}"\
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()"\
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\
798 md-template="tab.template"\
799 md-scope="tab.parent"\
800 ng-if="tab.shouldRender()"></div>\
802 </md-tabs-content-wrapper>\
805 controller: 'MdTabsController',
806 controllerAs: '$mdTabsCtrl',
807 link: function (scope, element, attr) {
808 compileTabData(attr.$mdTabsTemplate);
809 delete attr.$mdTabsTemplate;
811 $mdUtil.initOptionalProperties(scope, attr);
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;
820 function compileTabData (template) {
821 var dataElement = element.find('md-tab-data');
822 dataElement.html(template);
823 $compile(dataElement.contents())(scope.$parent);
828 MdTabs.$inject = ["$mdTheming", "$mdUtil", "$compile"];
831 .module('material.components.tabs')
832 .directive('mdTemplate', MdTemplate);
834 function MdTemplate ($compile, $mdUtil, $timeout) {
839 template: '=mdTemplate',
840 compileScope: '=mdScope',
841 connected: '=?mdConnectedIf'
845 function link (scope, element, attr, ctrl) {
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);
855 function disconnect () {
856 if (ctrl.scope.noDisconnect) return;
857 $mdUtil.disconnectScope(compileScope);
859 function reconnect () {
860 if (ctrl.scope.noDisconnect) return;
861 $mdUtil.reconnectScope(compileScope);
865 MdTemplate.$inject = ["$compile", "$mdUtil", "$timeout"];
867 ng.material.components.tabs = angular.module("material.components.tabs");