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