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