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