socketioMEANseed/www/bower_components/angular-material/modules/closure/tabs/tabs.js
www/bower_components/angular-material/modules/closure/tabs/tabs.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.3
6 */
7goog.provide('ngmaterial.components.tabs');
8goog.require('ngmaterial.components.icon');
9goog.require('ngmaterial.core');
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 */
33angular.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 * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
56 * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
57 * be initiated via data binding changes, programmatic invocation, or user gestures.
58 *
59 * @param {string=} label Optional attribute to specify a simple string as the tab label
60 * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection.
61 * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
62 * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
63 * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active tab at a time.
64 *
65 *
66 * @usage
67 *
68 * <hljs lang="html">
69 * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" >
70 * <h3>My Tab content</h3>
71 * </md-tab>
72 *
73 * <md-tab >
74 * <md-tab-label>
75 * <h3>My Tab content</h3>
76 * </md-tab-label>
77 * <md-tab-body>
78 * <p>
79 * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
80 * totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
81 * dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
82 * sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
83 * </p>
84 * </md-tab-body>
85 * </md-tab>
86 * </hljs>
87 *
88 */
89angular
90 .module('material.components.tabs')
91 .directive('mdTab', MdTab);
92
93function MdTab () {
94 return {
95 require: '^?mdTabs',
96 terminal: true,
97 compile: function (element, attr) {
98 var label = firstChild(element, 'md-tab-label'),
99 body = firstChild(element, 'md-tab-body');
100
101 if (label.length === 0) {
102 label = angular.element('<md-tab-label></md-tab-label>');
103 if (attr.label) label.text(attr.label);
104 else label.append(element.contents());
105
106 if (body.length === 0) {
107 var contents = element.contents().detach();
108 body = angular.element('<md-tab-body></md-tab-body>');
109 body.append(contents);
110 }
111 }
112
113 element.append(label);
114 if (body.html()) element.append(body);
115
116 return postLink;
117 },
118 scope: {
119 active: '=?mdActive',
120 disabled: '=?ngDisabled',
121 select: '&?mdOnSelect',
122 deselect: '&?mdOnDeselect'
123 }
124 };
125
126 function postLink (scope, element, attr, ctrl) {
127 if (!ctrl) return;
128 var index = ctrl.getTabElementIndex(element),
129 body = firstChild(element, 'md-tab-body').remove(),
130 label = firstChild(element, 'md-tab-label').remove(),
131 data = ctrl.insertTab({
132 scope: scope,
133 parent: scope.$parent,
134 index: index,
135 element: element,
136 template: body.html(),
137 label: label.html()
138 }, index);
139
140 scope.select = scope.select || angular.noop;
141 scope.deselect = scope.deselect || angular.noop;
142
143 scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
144 scope.$watch('disabled', function () { ctrl.refreshIndex(); });
145 scope.$watch(
146 function () {
147 return ctrl.getTabElementIndex(element);
148 },
149 function (newIndex) {
150 data.index = newIndex;
151 ctrl.updateTabOrder();
152 }
153 );
154 scope.$on('$destroy', function () { ctrl.removeTab(data); });
155 }
156
157 function firstChild (element, tagName) {
158 var children = element[0].children;
159 for (var i = 0, len = children.length; i < len; i++) {
160 var child = children[i];
161 if (child.tagName === tagName.toUpperCase()) return angular.element(child);
162 }
163 return angular.element();
164 }
165}
166
167angular
168 .module('material.components.tabs')
169 .directive('mdTabItem', MdTabItem);
170
171function MdTabItem () {
172 return {
173 require: '^?mdTabs',
174 link: function link (scope, element, attr, ctrl) {
175 if (!ctrl) return;
176 ctrl.attachRipple(scope, element);
177 }
178 };
179}
180
181angular
182 .module('material.components.tabs')
183 .directive('mdTabLabel', MdTabLabel);
184
185function MdTabLabel () {
186 return { terminal: true };
187}
188
189
190
191MdTabScroll['$inject'] = ["$parse"];angular.module('material.components.tabs')
192 .directive('mdTabScroll', MdTabScroll);
193
194function MdTabScroll ($parse) {
195 return {
196 restrict: 'A',
197 compile: function ($element, attr) {
198 var fn = $parse(attr.mdTabScroll, null, true);
199 return function ngEventHandler (scope, element) {
200 element.on('mousewheel', function (event) {
201 scope.$apply(function () { fn(scope, { $event: event }); });
202 });
203 };
204 }
205 };
206}
207
208
209MdTabsController['$inject'] = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming", "$mdInteraction"];angular
210 .module('material.components.tabs')
211 .controller('MdTabsController', MdTabsController);
212
213/**
214 * ngInject
215 */
216function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, $mdUtil,
217 $animateCss, $attrs, $compile, $mdTheming, $mdInteraction) {
218 // define private properties
219 var ctrl = this,
220 locked = false,
221 elements = getElements(),
222 queue = [],
223 destroyed = false,
224 loaded = false;
225
226
227 // Define public methods
228 ctrl.$onInit = $onInit;
229 ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100);
230 ctrl.redirectFocus = redirectFocus;
231 ctrl.attachRipple = attachRipple;
232 ctrl.insertTab = insertTab;
233 ctrl.removeTab = removeTab;
234 ctrl.select = select;
235 ctrl.scroll = scroll;
236 ctrl.nextPage = nextPage;
237 ctrl.previousPage = previousPage;
238 ctrl.keydown = keydown;
239 ctrl.canPageForward = canPageForward;
240 ctrl.canPageBack = canPageBack;
241 ctrl.refreshIndex = refreshIndex;
242 ctrl.incrementIndex = incrementIndex;
243 ctrl.getTabElementIndex = getTabElementIndex;
244 ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
245 ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
246 ctrl.getFocusedTabId = getFocusedTabId;
247
248 // For Angular 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
249 // manually call the $onInit hook.
250 if (angular.version.major === 1 && angular.version.minor <= 4) {
251 this.$onInit();
252 }
253
254 /**
255 * Angular Lifecycle hook for newer Angular versions.
256 * Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
257 */
258 function $onInit() {
259 // Define one-way bindings
260 defineOneWayBinding('stretchTabs', handleStretchTabs);
261
262 // Define public properties with change handlers
263 defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
264 defineProperty('offsetLeft', handleOffsetChange, 0);
265 defineProperty('hasContent', handleHasContent, false);
266 defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
267 defineProperty('shouldPaginate', handleShouldPaginate, false);
268
269 // Define boolean attributes
270 defineBooleanAttribute('noInkBar', handleInkBar);
271 defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
272 defineBooleanAttribute('noPagination');
273 defineBooleanAttribute('swipeContent');
274 defineBooleanAttribute('noDisconnect');
275 defineBooleanAttribute('autoselect');
276 defineBooleanAttribute('noSelectClick');
277 defineBooleanAttribute('centerTabs', handleCenterTabs, false);
278 defineBooleanAttribute('enableDisconnect');
279
280 // Define public properties
281 ctrl.scope = $scope;
282 ctrl.parent = $scope.$parent;
283 ctrl.tabs = [];
284 ctrl.lastSelectedIndex = null;
285 ctrl.hasFocus = false;
286 ctrl.styleTabItemFocus = false;
287 ctrl.shouldCenterTabs = shouldCenterTabs();
288 ctrl.tabContentPrefix = 'tab-content-';
289
290 // Setup the tabs controller after all bindings are available.
291 setupTabsController();
292 }
293
294 /**
295 * Perform setup for the controller, setup events and watcher(s)
296 */
297 function setupTabsController () {
298 ctrl.selectedIndex = ctrl.selectedIndex || 0;
299 compileTemplate();
300 configureWatchers();
301 bindEvents();
302 $mdTheming($element);
303 $mdUtil.nextTick(function () {
304 // Note that the element references need to be updated, because certain "browsers"
305 // (IE/Edge) lose them and start throwing "Invalid calling object" errors, when we
306 // compile the element contents down in `compileElement`.
307 elements = getElements();
308 updateHeightFromContent();
309 adjustOffset();
310 updateInkBarStyles();
311 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
312 loaded = true;
313 updatePagination();
314 });
315 }
316
317 /**
318 * Compiles the template provided by the user. This is passed as an attribute from the tabs
319 * directive's template function.
320 */
321 function compileTemplate () {
322 var template = $attrs.$mdTabsTemplate,
323 element = angular.element($element[0].querySelector('md-tab-data'));
324
325 element.html(template);
326 $compile(element.contents())(ctrl.parent);
327 delete $attrs.$mdTabsTemplate;
328 }
329
330 /**
331 * Binds events used by the tabs component.
332 */
333 function bindEvents () {
334 angular.element($window).on('resize', handleWindowResize);
335 $scope.$on('$destroy', cleanup);
336 }
337
338 /**
339 * Configure watcher(s) used by Tabs
340 */
341 function configureWatchers () {
342 $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
343 }
344
345 /**
346 * Creates a one-way binding manually rather than relying on Angular's isolated scope
347 * @param key
348 * @param handler
349 */
350 function defineOneWayBinding (key, handler) {
351 var attr = $attrs.$normalize('md-' + key);
352 if (handler) defineProperty(key, handler);
353 $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
354 }
355
356 /**
357 * Defines boolean attributes with default value set to true. (ie. md-stretch-tabs with no value
358 * will be treated as being truthy)
359 * @param key
360 * @param handler
361 */
362 function defineBooleanAttribute (key, handler) {
363 var attr = $attrs.$normalize('md-' + key);
364 if (handler) defineProperty(key, handler);
365 if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
366 $attrs.$observe(attr, updateValue);
367 function updateValue (newValue) {
368 ctrl[ key ] = newValue !== 'false';
369 }
370 }
371
372 /**
373 * Remove any events defined by this controller
374 */
375 function cleanup () {
376 destroyed = true;
377 angular.element($window).off('resize', handleWindowResize);
378 }
379
380 // Change handlers
381
382 /**
383 * Toggles stretch tabs class and updates inkbar when tab stretching changes
384 * @param stretchTabs
385 */
386 function handleStretchTabs (stretchTabs) {
387 var elements = getElements();
388 angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
389 updateInkBarStyles();
390 }
391
392 function handleCenterTabs (newValue) {
393 ctrl.shouldCenterTabs = shouldCenterTabs();
394 }
395
396 function handleMaxTabWidth (newWidth, oldWidth) {
397 if (newWidth !== oldWidth) {
398 var elements = getElements();
399
400 angular.forEach(elements.tabs, function(tab) {
401 tab.style.maxWidth = newWidth + 'px';
402 });
403 $mdUtil.nextTick(ctrl.updateInkBarStyles);
404 }
405 }
406
407 function handleShouldPaginate (newValue, oldValue) {
408 if (newValue !== oldValue) {
409 ctrl.maxTabWidth = getMaxTabWidth();
410 ctrl.shouldCenterTabs = shouldCenterTabs();
411 $mdUtil.nextTick(function () {
412 ctrl.maxTabWidth = getMaxTabWidth();
413 adjustOffset(ctrl.selectedIndex);
414 });
415 }
416 }
417
418 /**
419 * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
420 * @param hasContent
421 */
422 function handleHasContent (hasContent) {
423 $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
424 }
425
426 /**
427 * Apply ctrl.offsetLeft to the paging element when it changes
428 * @param left
429 */
430 function handleOffsetChange (left) {
431 var elements = getElements();
432 var newValue = ctrl.shouldCenterTabs ? '' : '-' + left + 'px';
433
434 angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)');
435 $scope.$broadcast('$mdTabsPaginationChanged');
436 }
437
438 /**
439 * Update the UI whenever `ctrl.focusIndex` is updated
440 * @param newIndex
441 * @param oldIndex
442 */
443 function handleFocusIndexChange (newIndex, oldIndex) {
444 if (newIndex === oldIndex) return;
445 if (!getElements().tabs[ newIndex ]) return;
446 adjustOffset();
447 redirectFocus();
448 }
449
450 /**
451 * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
452 * @param newValue
453 * @param oldValue
454 */
455 function handleSelectedIndexChange (newValue, oldValue) {
456 if (newValue === oldValue) return;
457
458 ctrl.selectedIndex = getNearestSafeIndex(newValue);
459 ctrl.lastSelectedIndex = oldValue;
460 ctrl.updateInkBarStyles();
461 updateHeightFromContent();
462 adjustOffset(newValue);
463 $scope.$broadcast('$mdTabsChanged');
464 ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
465 ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
466 }
467
468 function getTabElementIndex(tabEl){
469 var tabs = $element[0].getElementsByTagName('md-tab');
470 return Array.prototype.indexOf.call(tabs, tabEl[0]);
471 }
472
473 /**
474 * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
475 * hidden.
476 */
477 function handleResizeWhenVisible () {
478 // if there is already a watcher waiting for resize, do nothing
479 if (handleResizeWhenVisible.watcher) return;
480 // otherwise, we will abuse the $watch function to check for visible
481 handleResizeWhenVisible.watcher = $scope.$watch(function () {
482 // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
483 $mdUtil.nextTick(function () {
484 // if the watcher has already run (ie. multiple digests in one cycle), do nothing
485 if (!handleResizeWhenVisible.watcher) return;
486
487 if ($element.prop('offsetParent')) {
488 handleResizeWhenVisible.watcher();
489 handleResizeWhenVisible.watcher = null;
490
491 handleWindowResize();
492 }
493 }, false);
494 });
495 }
496
497 // Event handlers / actions
498
499 /**
500 * Handle user keyboard interactions
501 * @param event
502 */
503 function keydown (event) {
504 switch (event.keyCode) {
505 case $mdConstant.KEY_CODE.LEFT_ARROW:
506 event.preventDefault();
507 incrementIndex(-1, true);
508 break;
509 case $mdConstant.KEY_CODE.RIGHT_ARROW:
510 event.preventDefault();
511 incrementIndex(1, true);
512 break;
513 case $mdConstant.KEY_CODE.SPACE:
514 case $mdConstant.KEY_CODE.ENTER:
515 event.preventDefault();
516 if (!locked) select(ctrl.focusIndex);
517 break;
518 }
519 }
520
521 /**
522 * Update the selected index. Triggers a click event on the original `md-tab` element in order
523 * to fire user-added click events if canSkipClick or `md-no-select-click` are false.
524 * @param index
525 * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
526 */
527 function select (index, canSkipClick) {
528 if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
529 // skip the click event if noSelectClick is enabled
530 if (canSkipClick && ctrl.noSelectClick) return;
531 // nextTick is required to prevent errors in user-defined click events
532 $mdUtil.nextTick(function () {
533 ctrl.tabs[ index ].element.triggerHandler('click');
534 }, false);
535 }
536
537 /**
538 * When pagination is on, this makes sure the selected index is in view.
539 * @param event
540 */
541 function scroll (event) {
542 if (!ctrl.shouldPaginate) return;
543 event.preventDefault();
544 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
545 }
546
547 /**
548 * Slides the tabs over approximately one page forward.
549 */
550 function nextPage () {
551 var elements = getElements();
552 var viewportWidth = elements.canvas.clientWidth,
553 totalWidth = viewportWidth + ctrl.offsetLeft,
554 i, tab;
555 for (i = 0; i < elements.tabs.length; i++) {
556 tab = elements.tabs[ i ];
557 if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
558 }
559
560 if (viewportWidth > tab.offsetWidth) {
561 //Canvas width *greater* than tab width: usual positioning
562 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
563 } else {
564 /**
565 * Canvas width *smaller* than tab width: positioning at the *end* of current tab to let
566 * pagination "for loop" to proceed correctly on next tab when nextPage() is called again
567 */
568 ctrl.offsetLeft = fixOffset(tab.offsetLeft + (tab.offsetWidth - viewportWidth + 1));
569 }
570 }
571
572 /**
573 * Slides the tabs over approximately one page backward.
574 */
575 function previousPage () {
576 var i, tab, elements = getElements();
577
578 for (i = 0; i < elements.tabs.length; i++) {
579 tab = elements.tabs[ i ];
580 if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
581 }
582
583 if (elements.canvas.clientWidth > tab.offsetWidth) {
584 //Canvas width *greater* than tab width: usual positioning
585 ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
586 } else {
587 /**
588 * Canvas width *smaller* than tab width: positioning at the *beginning* of current tab to let
589 * pagination "for loop" to break correctly on previous tab when previousPage() is called again
590 */
591 ctrl.offsetLeft = fixOffset(tab.offsetLeft);
592 }
593 }
594
595 /**
596 * Update size calculations when the window is resized.
597 */
598 function handleWindowResize () {
599 ctrl.lastSelectedIndex = ctrl.selectedIndex;
600 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
601 $mdUtil.nextTick(function () {
602 ctrl.updateInkBarStyles();
603 updatePagination();
604 });
605 }
606
607 function handleInkBar (hide) {
608 angular.element(getElements().inkBar).toggleClass('ng-hide', hide);
609 }
610
611 /**
612 * Toggle dynamic height class when value changes
613 * @param value
614 */
615 function handleDynamicHeight (value) {
616 $element.toggleClass('md-dynamic-height', value);
617 }
618
619 /**
620 * Remove a tab from the data and select the nearest valid tab.
621 * @param tabData
622 */
623 function removeTab (tabData) {
624 if (destroyed) return;
625 var selectedIndex = ctrl.selectedIndex,
626 tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
627 refreshIndex();
628 // when removing a tab, if the selected index did not change, we have to manually trigger the
629 // tab select/deselect events
630 if (ctrl.selectedIndex === selectedIndex) {
631 tab.scope.deselect();
632 ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
633 }
634 $mdUtil.nextTick(function () {
635 updatePagination();
636 ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
637 });
638 }
639
640 /**
641 * Create an entry in the tabs array for a new tab at the specified index.
642 * @param tabData
643 * @param index
644 * @returns {*}
645 */
646 function insertTab (tabData, index) {
647 var hasLoaded = loaded;
648 var proto = {
649 getIndex: function () { return ctrl.tabs.indexOf(tab); },
650 isActive: function () { return this.getIndex() === ctrl.selectedIndex; },
651 isLeft: function () { return this.getIndex() < ctrl.selectedIndex; },
652 isRight: function () { return this.getIndex() > ctrl.selectedIndex; },
653 shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); },
654 hasFocus: function () {
655 return ctrl.styleTabItemFocus
656 && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
657 },
658 id: $mdUtil.nextUid(),
659 hasContent: !!(tabData.template && tabData.template.trim())
660 },
661 tab = angular.extend(proto, tabData);
662 if (angular.isDefined(index)) {
663 ctrl.tabs.splice(index, 0, tab);
664 } else {
665 ctrl.tabs.push(tab);
666 }
667
668 processQueue();
669 updateHasContent();
670 $mdUtil.nextTick(function () {
671 updatePagination();
672 setAriaControls(tab);
673
674 // if autoselect is enabled, select the newly added tab
675 if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () {
676 $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
677 });
678 });
679 return tab;
680 }
681
682 // Getter methods
683
684 /**
685 * Gathers references to all of the DOM elements used by this controller.
686 * @returns {{}}
687 */
688 function getElements () {
689 var elements = {};
690 var node = $element[0];
691
692 // gather tab bar elements
693 elements.wrapper = node.querySelector('md-tabs-wrapper');
694 elements.canvas = elements.wrapper.querySelector('md-tabs-canvas');
695 elements.paging = elements.canvas.querySelector('md-pagination-wrapper');
696 elements.inkBar = elements.paging.querySelector('md-ink-bar');
697
698 elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content');
699 elements.tabs = elements.paging.querySelectorAll('md-tab-item');
700 elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab');
701
702 return elements;
703 }
704
705 /**
706 * Determines whether or not the left pagination arrow should be enabled.
707 * @returns {boolean}
708 */
709 function canPageBack () {
710 return ctrl.offsetLeft > 0;
711 }
712
713 /**
714 * Determines whether or not the right pagination arrow should be enabled.
715 * @returns {*|boolean}
716 */
717 function canPageForward () {
718 var elements = getElements();
719 var lastTab = elements.tabs[ elements.tabs.length - 1 ];
720 return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
721 ctrl.offsetLeft;
722 }
723
724 /**
725 * Returns currently focused tab item's element ID
726 */
727 function getFocusedTabId() {
728 var focusedTab = ctrl.tabs[ctrl.focusIndex];
729 if (!focusedTab || !focusedTab.id) {
730 return null;
731 }
732 return 'tab-item-' + focusedTab.id;
733 }
734
735 /**
736 * Determines if the UI should stretch the tabs to fill the available space.
737 * @returns {*}
738 */
739 function shouldStretchTabs () {
740 switch (ctrl.stretchTabs) {
741 case 'always':
742 return true;
743 case 'never':
744 return false;
745 default:
746 return !ctrl.shouldPaginate
747 && $window.matchMedia('(max-width: 600px)').matches;
748 }
749 }
750
751 /**
752 * Determines if the tabs should appear centered.
753 * @returns {string|boolean}
754 */
755 function shouldCenterTabs () {
756 return ctrl.centerTabs && !ctrl.shouldPaginate;
757 }
758
759 /**
760 * Determines if pagination is necessary to display the tabs within the available space.
761 * @returns {boolean}
762 */
763 function shouldPaginate () {
764 if (ctrl.noPagination || !loaded) return false;
765 var canvasWidth = $element.prop('clientWidth');
766
767 angular.forEach(getElements().dummies, function (tab) {
768 canvasWidth -= tab.offsetWidth;
769 });
770
771 return canvasWidth < 0;
772 }
773
774 /**
775 * Finds the nearest tab index that is available. This is primarily used for when the active
776 * tab is removed.
777 * @param newIndex
778 * @returns {*}
779 */
780 function getNearestSafeIndex (newIndex) {
781 if (newIndex === -1) return -1;
782 var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
783 i, tab;
784 for (i = 0; i <= maxOffset; i++) {
785 tab = ctrl.tabs[ newIndex + i ];
786 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
787 tab = ctrl.tabs[ newIndex - i ];
788 if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
789 }
790 return newIndex;
791 }
792
793 // Utility methods
794
795 /**
796 * Defines a property using a getter and setter in order to trigger a change handler without
797 * using `$watch` to observe changes.
798 * @param key
799 * @param handler
800 * @param value
801 */
802 function defineProperty (key, handler, value) {
803 Object.defineProperty(ctrl, key, {
804 get: function () { return value; },
805 set: function (newValue) {
806 var oldValue = value;
807 value = newValue;
808 handler && handler(newValue, oldValue);
809 }
810 });
811 }
812
813 /**
814 * Updates whether or not pagination should be displayed.
815 */
816 function updatePagination () {
817 ctrl.maxTabWidth = getMaxTabWidth();
818 ctrl.shouldPaginate = shouldPaginate();
819 }
820
821 /**
822 * Calculates the width of the pagination wrapper by summing the widths of the dummy tabs.
823 * @returns {number}
824 */
825 function calcPagingWidth () {
826 return calcTabsWidth(getElements().dummies);
827 }
828
829 function calcTabsWidth(tabs) {
830 var width = 0;
831
832 angular.forEach(tabs, function (tab) {
833 //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This
834 // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
835 // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
836 // of a dialog)
837 width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width);
838 });
839
840 return Math.ceil(width);
841 }
842
843 function getMaxTabWidth () {
844 return $element.prop('clientWidth');
845 }
846
847 /**
848 * Re-orders the tabs and updates the selected and focus indexes to their new positions.
849 * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
850 */
851 function updateTabOrder () {
852 var selectedItem = ctrl.tabs[ ctrl.selectedIndex ],
853 focusItem = ctrl.tabs[ ctrl.focusIndex ];
854 ctrl.tabs = ctrl.tabs.sort(function (a, b) {
855 return a.index - b.index;
856 });
857 ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
858 ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
859 }
860
861 /**
862 * This moves the selected or focus index left or right. This is used by the keydown handler.
863 * @param inc
864 */
865 function incrementIndex (inc, focus) {
866 var newIndex,
867 key = focus ? 'focusIndex' : 'selectedIndex',
868 index = ctrl[ key ];
869 for (newIndex = index + inc;
870 ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
871 newIndex += inc) {}
872 if (ctrl.tabs[ newIndex ]) {
873 ctrl[ key ] = newIndex;
874 }
875 }
876
877 /**
878 * This is used to forward focus to dummy elements. This method is necessary to avoid animation
879 * issues when attempting to focus an item that is out of view.
880 */
881 function redirectFocus () {
882 ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
883 getElements().dummies[ ctrl.focusIndex ].focus();
884 }
885
886 /**
887 * Forces the pagination to move the focused tab into view.
888 */
889 function adjustOffset (index) {
890 var elements = getElements();
891
892 if (!angular.isNumber(index)) index = ctrl.focusIndex;
893 if (!elements.tabs[ index ]) return;
894 if (ctrl.shouldCenterTabs) return;
895 var tab = elements.tabs[ index ],
896 left = tab.offsetLeft,
897 right = tab.offsetWidth + left;
898 ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + 32 * 2));
899 ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
900 }
901
902 /**
903 * Iterates through all queued functions and clears the queue. This is used for functions that
904 * are called before the UI is ready, such as size calculations.
905 */
906 function processQueue () {
907 queue.forEach(function (func) { $mdUtil.nextTick(func); });
908 queue = [];
909 }
910
911 /**
912 * Determines if the tab content area is needed.
913 */
914 function updateHasContent () {
915 var hasContent = false;
916
917 for (var i = 0; i < ctrl.tabs.length; i++) {
918 if (ctrl.tabs[i].hasContent) {
919 hasContent = true;
920 break;
921 }
922 }
923
924 ctrl.hasContent = hasContent;
925 }
926
927 /**
928 * Moves the indexes to their nearest valid values.
929 */
930 function refreshIndex () {
931 ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
932 ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
933 }
934
935 /**
936 * Calculates the content height of the current tab.
937 * @returns {*}
938 */
939 function updateHeightFromContent () {
940 if (!ctrl.dynamicHeight) return $element.css('height', '');
941 if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
942
943 var elements = getElements();
944
945 var tabContent = elements.contents[ ctrl.selectedIndex ],
946 contentHeight = tabContent ? tabContent.offsetHeight : 0,
947 tabsHeight = elements.wrapper.offsetHeight,
948 newHeight = contentHeight + tabsHeight,
949 currentHeight = $element.prop('clientHeight');
950
951 if (currentHeight === newHeight) return;
952
953 // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
954 // positioning. This should probably be cleaned up if a cleaner solution is possible.
955 if ($element.attr('md-align-tabs') === 'bottom') {
956 currentHeight -= tabsHeight;
957 newHeight -= tabsHeight;
958 // Need to include bottom border in these calculations
959 if ($element.attr('md-border-bottom') !== undefined) ++currentHeight;
960 }
961
962 // Lock during animation so the user can't change tabs
963 locked = true;
964
965 var fromHeight = { height: currentHeight + 'px' },
966 toHeight = { height: newHeight + 'px' };
967
968 // Set the height to the current, specific pixel height to fix a bug on iOS where the height
969 // first animates to 0, then back to the proper height causing a visual glitch
970 $element.css(fromHeight);
971
972 // Animate the height from the old to the new
973 $animateCss($element, {
974 from: fromHeight,
975 to: toHeight,
976 easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
977 duration: 0.5
978 }).start().done(function () {
979 // Then (to fix the same iOS issue as above), disable transitions and remove the specific
980 // pixel height so the height can size with browser width/content changes, etc.
981 $element.css({
982 transition: 'none',
983 height: ''
984 });
985
986 // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
987 // enough to batch it for us instead of doing it immediately, which undoes the original
988 // transition: none)
989 $mdUtil.nextTick(function() {
990 $element.css('transition', '');
991 });
992
993 // And unlock so tab changes can occur
994 locked = false;
995 });
996 }
997
998 /**
999 * Repositions the ink bar to the selected tab.
1000 * @returns {*}
1001 */
1002 function updateInkBarStyles () {
1003 var elements = getElements();
1004
1005 if (!elements.tabs[ ctrl.selectedIndex ]) {
1006 angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
1007 return;
1008 }
1009
1010 if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
1011 // if the element is not visible, we will not be able to calculate sizes until it is
1012 // we should treat that as a resize event rather than just updating the ink bar
1013 if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
1014
1015 var index = ctrl.selectedIndex,
1016 totalWidth = elements.paging.offsetWidth,
1017 tab = elements.tabs[ index ],
1018 left = tab.offsetLeft,
1019 right = totalWidth - left - tab.offsetWidth;
1020
1021 if (ctrl.shouldCenterTabs) {
1022 // We need to use the same calculate process as in the pagination wrapper, to avoid rounding deviations.
1023 var tabWidth = calcTabsWidth(elements.tabs);
1024
1025 if (totalWidth > tabWidth) {
1026 $mdUtil.nextTick(updateInkBarStyles, false);
1027 }
1028 }
1029 updateInkBarClassName();
1030 angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
1031 }
1032
1033 /**
1034 * Adds left/right classes so that the ink bar will animate properly.
1035 */
1036 function updateInkBarClassName () {
1037 var elements = getElements();
1038 var newIndex = ctrl.selectedIndex,
1039 oldIndex = ctrl.lastSelectedIndex,
1040 ink = angular.element(elements.inkBar);
1041 if (!angular.isNumber(oldIndex)) return;
1042 ink
1043 .toggleClass('md-left', newIndex < oldIndex)
1044 .toggleClass('md-right', newIndex > oldIndex);
1045 }
1046
1047 /**
1048 * Takes an offset value and makes sure that it is within the min/max allowed values.
1049 * @param value
1050 * @returns {*}
1051 */
1052 function fixOffset (value) {
1053 var elements = getElements();
1054
1055 if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
1056 var lastTab = elements.tabs[ elements.tabs.length - 1 ],
1057 totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
1058 value = Math.max(0, value);
1059 value = Math.min(totalWidth - elements.canvas.clientWidth, value);
1060 return value;
1061 }
1062
1063 /**
1064 * Attaches a ripple to the tab item element.
1065 * @param scope
1066 * @param element
1067 */
1068 function attachRipple (scope, element) {
1069 var elements = getElements();
1070 var options = { colorElement: angular.element(elements.inkBar) };
1071 $mdTabInkRipple.attach(scope, element, options);
1072 }
1073
1074 /**
1075 * Sets the `aria-controls` attribute to the elements that
1076 * correspond to the passed-in tab.
1077 * @param tab
1078 */
1079 function setAriaControls (tab) {
1080 if (tab.hasContent) {
1081 var nodes = $element[0].querySelectorAll('[md-tab-id="' + tab.id + '"]');
1082 angular.element(nodes).attr('aria-controls', ctrl.tabContentPrefix + tab.id);
1083 }
1084 }
1085}
1086
1087/**
1088 * @ngdoc directive
1089 * @name mdTabs
1090 * @module material.components.tabs
1091 *
1092 * @restrict E
1093 *
1094 * @description
1095 * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to
1096 * produces a Tabs components. In turn, the nested `<md-tab>` directive is used to specify a tab
1097 * label for the **header button** and a [optional] tab view content that will be associated with
1098 * each tab button.
1099 *
1100 * Below is the markup for its simplest usage:
1101 *
1102 * <hljs lang="html">
1103 * <md-tabs>
1104 * <md-tab label="Tab #1"></md-tab>
1105 * <md-tab label="Tab #2"></md-tab>
1106 * <md-tab label="Tab #3"></md-tab>
1107 * </md-tabs>
1108 * </hljs>
1109 *
1110 * Tabs supports three (3) usage scenarios:
1111 *
1112 * 1. Tabs (buttons only)
1113 * 2. Tabs with internal view content
1114 * 3. Tabs with external view content
1115 *
1116 * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any
1117 * other components, content, or views.
1118 *
1119 * <i><b>Note:</b> If you are using the Tabs component for page-level navigation, please take a look
1120 * at the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead as it can handle this
1121 * case a bit more natively.</i>
1122 *
1123 * **Tabs with internal views** are the traditional usages where each tab has associated view
1124 * content and the view switching is managed internally by the Tabs component.
1125 *
1126 * **Tabs with external view content** is often useful when content associated with each tab is
1127 * independently managed and data-binding notifications announce tab selection changes.
1128 *
1129 * Additional features also include:
1130 *
1131 * * Content can include any markup.
1132 * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
1133 *
1134 * ### Explanation of tab stretching
1135 *
1136 * 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.
1137 *
1138 * On mobile devices, tabs will be expanded to fill the available horizontal space. When this happens, all tabs will become the same size.
1139 *
1140 * On desktops, by default, stretching will never occur.
1141 *
1142 * This default behavior can be overridden through the `md-stretch-tabs` attribute. Here is a table showing when stretching will occur:
1143 *
1144 * `md-stretch-tabs` | mobile | desktop
1145 * ------------------|-----------|--------
1146 * `auto` | stretched | ---
1147 * `always` | stretched | stretched
1148 * `never` | --- | ---
1149 *
1150 * @param {integer=} md-selected Index of the active/selected tab
1151 * @param {boolean=} md-no-ink If present, disables ink ripple effects.
1152 * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar.
1153 * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
1154 * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
1155 * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
1156 * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content
1157 * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
1158 * @param {boolean=} md-no-pagination When enabled, pagination will remain off
1159 * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
1160 * @param {boolean=} md-enable-disconnect When enabled, scopes will be disconnected for tabs that are not being displayed. This provides a performance boost, but may also cause unexpected issues and is not recommended for most users.
1161 * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected
1162 * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs
1163 *
1164 * @usage
1165 * <hljs lang="html">
1166 * <md-tabs md-selected="selectedIndex" >
1167 * <img ng-src="img/angular.png" class="centered">
1168 * <md-tab
1169 * ng-repeat="tab in tabs | orderBy:predicate:reversed"
1170 * md-on-select="onTabSelected(tab)"
1171 * md-on-deselect="announceDeselected(tab)"
1172 * ng-disabled="tab.disabled">
1173 * <md-tab-label>
1174 * {{tab.title}}
1175 * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
1176 * </md-tab-label>
1177 * <md-tab-body>
1178 * {{tab.content}}
1179 * </md-tab-body>
1180 * </md-tab>
1181 * </md-tabs>
1182 * </hljs>
1183 *
1184 */
1185MdTabs['$inject'] = ["$$mdSvgRegistry"];
1186angular
1187 .module('material.components.tabs')
1188 .directive('mdTabs', MdTabs);
1189
1190function MdTabs ($$mdSvgRegistry) {
1191 return {
1192 scope: {
1193 selectedIndex: '=?mdSelected'
1194 },
1195 template: function (element, attr) {
1196 attr.$mdTabsTemplate = element.html();
1197 return '' +
1198 '<md-tabs-wrapper> ' +
1199 '<md-tab-data></md-tab-data> ' +
1200 '<md-prev-button ' +
1201 'tabindex="-1" ' +
1202 'role="button" ' +
1203 'aria-label="Previous Page" ' +
1204 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
1205 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
1206 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
1207 'ng-click="$mdTabsCtrl.previousPage()"> ' +
1208 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
1209 '</md-prev-button> ' +
1210 '<md-next-button ' +
1211 'tabindex="-1" ' +
1212 'role="button" ' +
1213 'aria-label="Next Page" ' +
1214 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
1215 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
1216 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
1217 'ng-click="$mdTabsCtrl.nextPage()"> ' +
1218 '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
1219 '</md-next-button> ' +
1220 '<md-tabs-canvas ' +
1221 'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
1222 'aria-activedescendant="{{$mdTabsCtrl.getFocusedTabId()}}" ' +
1223 'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
1224 'ng-class="{ ' +
1225 '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
1226 '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
1227 '}" ' +
1228 'ng-keydown="$mdTabsCtrl.keydown($event)" ' +
1229 'role="tablist"> ' +
1230 '<md-pagination-wrapper ' +
1231 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
1232 'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' +
1233 '<md-tab-item ' +
1234 'tabindex="-1" ' +
1235 'class="md-tab" ' +
1236 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
1237 'role="tab" ' +
1238 'md-tab-id="{{::tab.id}}"' +
1239 'aria-selected="{{tab.isActive()}}" ' +
1240 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
1241 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
1242 'ng-class="{ ' +
1243 '\'md-active\': tab.isActive(), ' +
1244 '\'md-focused\': tab.hasFocus(), ' +
1245 '\'md-disabled\': tab.scope.disabled ' +
1246 '}" ' +
1247 'ng-disabled="tab.scope.disabled" ' +
1248 'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
1249 'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
1250 'md-tabs-template="::tab.label" ' +
1251 'md-scope="::tab.parent"></md-tab-item> ' +
1252 '<md-ink-bar></md-ink-bar> ' +
1253 '</md-pagination-wrapper> ' +
1254 '<md-tabs-dummy-wrapper class="md-visually-hidden md-dummy-wrapper"> ' +
1255 '<md-dummy-tab ' +
1256 'class="md-tab" ' +
1257 'tabindex="-1" ' +
1258 'id="tab-item-{{::tab.id}}" ' +
1259 'md-tab-id="{{::tab.id}}"' +
1260 'aria-selected="{{tab.isActive()}}" ' +
1261 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
1262 'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
1263 'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
1264 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
1265 'md-tabs-template="::tab.label" ' +
1266 'md-scope="::tab.parent"></md-dummy-tab> ' +
1267 '</md-tabs-dummy-wrapper> ' +
1268 '</md-tabs-canvas> ' +
1269 '</md-tabs-wrapper> ' +
1270 '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' +
1271 '<md-tab-content ' +
1272 'id="{{:: $mdTabsCtrl.tabContentPrefix + tab.id}}" ' +
1273 'class="_md" ' +
1274 'role="tabpanel" ' +
1275 'aria-labelledby="tab-item-{{::tab.id}}" ' +
1276 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
1277 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
1278 'ng-if="tab.hasContent" ' +
1279 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
1280 'ng-class="{ ' +
1281 '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
1282 '\'md-active\': tab.isActive(), ' +
1283 '\'md-left\': tab.isLeft(), ' +
1284 '\'md-right\': tab.isRight(), ' +
1285 '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' +
1286 '}"> ' +
1287 '<div ' +
1288 'md-tabs-template="::tab.template" ' +
1289 'md-connected-if="tab.isActive()" ' +
1290 'md-scope="::tab.parent" ' +
1291 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
1292 '</md-tab-content> ' +
1293 '</md-tabs-content-wrapper>';
1294 },
1295 controller: 'MdTabsController',
1296 controllerAs: '$mdTabsCtrl',
1297 bindToController: true
1298 };
1299}
1300
1301
1302MdTabsDummyWrapper['$inject'] = ["$mdUtil", "$window"];angular
1303 .module('material.components.tabs')
1304 .directive('mdTabsDummyWrapper', MdTabsDummyWrapper);
1305
1306/**
1307 * @private
1308 *
1309 * @param $mdUtil
1310 * @param $window
1311 * @returns {{require: string, link: link}}
1312 * @constructor
1313 *
1314 * ngInject
1315 */
1316function MdTabsDummyWrapper ($mdUtil, $window) {
1317 return {
1318 require: '^?mdTabs',
1319 link: function link (scope, element, attr, ctrl) {
1320 if (!ctrl) return;
1321
1322 var observer;
1323 var disconnect;
1324
1325 var mutationCallback = function() {
1326 ctrl.updatePagination();
1327 ctrl.updateInkBarStyles();
1328 };
1329
1330 if('MutationObserver' in $window) {
1331 var config = {
1332 childList: true,
1333 subtree: true,
1334 // Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire
1335 // the childList mutation, once a <span> element's innerText changes.
1336 // The characterData of the <span> element will change.
1337 characterData: true
1338 };
1339
1340 observer = new MutationObserver(mutationCallback);
1341 observer.observe(element[0], config);
1342 disconnect = observer.disconnect.bind(observer);
1343 } else {
1344 var debounced = $mdUtil.debounce(mutationCallback, 15, null, false);
1345
1346 element.on('DOMSubtreeModified', debounced);
1347 disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced);
1348 }
1349
1350 // Disconnect the observer
1351 scope.$on('$destroy', function() {
1352 disconnect();
1353 });
1354 }
1355 };
1356}
1357
1358
1359MdTabsTemplate['$inject'] = ["$compile", "$mdUtil"];angular
1360 .module('material.components.tabs')
1361 .directive('mdTabsTemplate', MdTabsTemplate);
1362
1363function MdTabsTemplate ($compile, $mdUtil) {
1364 return {
1365 restrict: 'A',
1366 link: link,
1367 scope: {
1368 template: '=mdTabsTemplate',
1369 connected: '=?mdConnectedIf',
1370 compileScope: '=mdScope'
1371 },
1372 require: '^?mdTabs'
1373 };
1374 function link (scope, element, attr, ctrl) {
1375 if (!ctrl) return;
1376
1377 var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
1378
1379 element.html(scope.template);
1380 $compile(element.contents())(compileScope);
1381
1382 return $mdUtil.nextTick(handleScope);
1383
1384 function handleScope () {
1385 scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
1386 scope.$on('$destroy', reconnect);
1387 }
1388
1389 function disconnect () {
1390 if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
1391 }
1392
1393 function reconnect () {
1394 if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
1395 }
1396 }
1397}
1398
1399ngmaterial.components.tabs = angular.module("material.components.tabs");