source: trip-planner-front/node_modules/angular-material/modules/js/tabs/tabs.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

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