2  * Angular Material Design
 
   3  * https://github.com/angular/material
 
   7 goog.provide('ngmaterial.components.navBar');
 
   8 goog.require('ngmaterial.core');
 
  11  * @name material.components.navBar
 
  15 MdNavBarController['$inject'] = ["$element", "$scope", "$timeout", "$mdConstant"];
 
  16 MdNavItem['$inject'] = ["$mdAria", "$$rAF"];
 
  17 MdNavItemController['$inject'] = ["$element"];
 
  18 MdNavBar['$inject'] = ["$mdAria", "$mdTheming"];
 
  19 angular.module('material.components.navBar', ['material.core'])
 
  20     .controller('MdNavBarController', MdNavBarController)
 
  21     .directive('mdNavBar', MdNavBar)
 
  22     .controller('MdNavItemController', MdNavItemController)
 
  23     .directive('mdNavItem', MdNavItem);
 
  26 /*****************************************************************************
 
  27  *                            PUBLIC DOCUMENTATION                           *
 
  28  *****************************************************************************/
 
  32  * @module material.components.navBar
 
  37  * The `<md-nav-bar>` directive renders a list of material tabs that can be used
 
  38  * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
 
  39  * body and no bar pagination.
 
  41  * Because it deals with page navigation, certain routing concepts are built-in.
 
  42  * Route changes via via ng-href, ui-sref, or ng-click events are supported.
 
  43  * Alternatively, the user could simply watch currentNavItem for changes.
 
  45  * Accessibility functionality is implemented as a site navigator with a
 
  46  * listbox, according to
 
  47  * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
 
  49  * @param {string=} mdSelectedNavItem The name of the current tab; this must
 
  50  *     match the name attribute of `<md-nav-item>`
 
  51  * @param {boolean=} mdNoInkBar If set to true, the ink bar will be hidden.
 
  52  * @param {string=} navBarAriaLabel An aria-label for the nav-bar
 
  56  *  <md-nav-bar md-selected-nav-item="currentNavItem">
 
  57  *    <md-nav-item md-nav-click="goto('page1')" name="page1">
 
  60  *    <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item>
 
  61  *    <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item>
 
  63  *      md-nav-sref="app.page4"
 
  64  *      sref-opts="{reload: true, notify: true}"
 
  74  *    $rootScope.$on('$routeChangeSuccess', function(event, current) {
 
  75  *      $scope.currentLink = getCurrentLinkFromRoute(current);
 
  81 /*****************************************************************************
 
  83  *****************************************************************************/
 
  87  * @module material.components.navBar
 
  92  * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>`
 
  93  * component. It renders an md-button as the actual link.
 
  95  * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required
 
  98  * @param {Function=} mdNavClick Function which will be called when the
 
  99  *     link is clicked to change the page. Renders as an `ng-click`.
 
 100  * @param {string=} mdNavHref url to transition to when this link is clicked.
 
 101  *     Renders as an `ng-href`.
 
 102  * @param {string=} mdNavSref Ui-router state to transition to when this link is
 
 103  *     clicked. Renders as a `ui-sref`.
 
 104  * @param {!Object=} srefOpts Ui-router options that are passed to the
 
 105  *     `$state.go()` function. See the [Ui-router documentation for details]
 
 106  *     (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html).
 
 107  * @param {string=} name The name of this link. Used by the nav bar to know
 
 108  *     which link is currently selected.
 
 109  * @param {string=} aria-label Adds alternative text for accessibility
 
 112  * See `<md-nav-bar>` for usage.
 
 116 /*****************************************************************************
 
 118  *****************************************************************************/
 
 120 function MdNavBar($mdAria, $mdTheming) {
 
 124     controller: MdNavBarController,
 
 125     controllerAs: 'ctrl',
 
 126     bindToController: true,
 
 128       'mdSelectedNavItem': '=?',
 
 130       'navBarAriaLabel': '@?',
 
 133       '<div class="md-nav-bar">' +
 
 134         '<nav role="navigation">' +
 
 135           '<ul class="_md-nav-bar-list" ng-transclude role="listbox"' +
 
 137             'ng-focus="ctrl.onFocus()"' +
 
 138             'ng-keydown="ctrl.onKeydown($event)"' +
 
 139             'aria-label="{{ctrl.navBarAriaLabel}}">' +
 
 142         '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
 
 144     link: function(scope, element, attrs, ctrl) {
 
 146       if (!ctrl.navBarAriaLabel) {
 
 147         $mdAria.expectAsync(element, 'aria-label', angular.noop);
 
 154  * Controller for the nav-bar component.
 
 156  * Accessibility functionality is implemented as a site navigator with a
 
 157  * listbox, according to
 
 158  * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
 
 159  * @param {!angular.JQLite} $element
 
 160  * @param {!angular.Scope} $scope
 
 161  * @param {!angular.Timeout} $timeout
 
 162  * @param {!Object} $mdConstant
 
 167 function MdNavBarController($element, $scope, $timeout, $mdConstant) {
 
 168   // Injected variables
 
 169   /** @private @const {!angular.Timeout} */
 
 170   this._$timeout = $timeout;
 
 172   /** @private @const {!angular.Scope} */
 
 173   this._$scope = $scope;
 
 175   /** @private @const {!Object} */
 
 176   this._$mdConstant = $mdConstant;
 
 178   // Data-bound variables.
 
 179   /** @type {string} */
 
 180   this.mdSelectedNavItem;
 
 182   /** @type {string} */
 
 183   this.navBarAriaLabel;
 
 187   /** @type {?angular.JQLite} */
 
 188   this._navBarEl = $element[0];
 
 190   /** @type {?angular.JQLite} */
 
 194   // need to wait for transcluded content to be available
 
 195   var deregisterTabWatch = this._$scope.$watch(function() {
 
 196     return self._navBarEl.querySelectorAll('._md-nav-button').length;
 
 198   function(newLength) {
 
 201       deregisterTabWatch();
 
 209  * Initializes the tab components once they exist.
 
 212 MdNavBarController.prototype._initTabs = function() {
 
 213   this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
 
 216   this._$timeout(function() {
 
 217     self._updateTabs(self.mdSelectedNavItem, undefined);
 
 220   this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
 
 221     // Wait a digest before update tabs for products doing
 
 222     // anything dynamic in the template.
 
 223     self._$timeout(function() {
 
 224       self._updateTabs(newValue, oldValue);
 
 230  * Set the current tab to be selected.
 
 231  * @param {string|undefined} newValue New current tab name.
 
 232  * @param {string|undefined} oldValue Previous tab name.
 
 235 MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
 
 237   var tabs = this._getTabs();
 
 239   // this._getTabs can return null if nav-bar has not yet been initialized
 
 245   var newTab = this._getTabByName(newValue);
 
 246   var oldTab = this._getTabByName(oldValue);
 
 249     oldTab.setSelected(false);
 
 250     oldIndex = tabs.indexOf(oldTab);
 
 254     newTab.setSelected(true);
 
 255     newIndex = tabs.indexOf(newTab);
 
 258   this._$timeout(function() {
 
 259     self._updateInkBarStyles(newTab, newIndex, oldIndex);
 
 264  * Repositions the ink bar to the selected tab.
 
 267 MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
 
 268   this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
 
 269       .toggleClass('_md-right', newIndex > oldIndex);
 
 271   this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
 
 274     var tabEl = tab.getButtonEl();
 
 275     var left = tabEl.offsetLeft;
 
 277     this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
 
 282  * Returns an array of the current tabs.
 
 283  * @return {!Array<!NavItemController>}
 
 286 MdNavBarController.prototype._getTabs = function() {
 
 287   var controllers = Array.prototype.slice.call(
 
 288     this._navBarEl.querySelectorAll('.md-nav-item'))
 
 290       return angular.element(el).controller('mdNavItem')
 
 292   return controllers.indexOf(undefined) ? controllers : null;
 
 296  * Returns the tab with the specified name.
 
 297  * @param {string} name The name of the tab, found in its name attribute.
 
 298  * @return {!NavItemController|undefined}
 
 301 MdNavBarController.prototype._getTabByName = function(name) {
 
 302   return this._findTab(function(tab) {
 
 303     return tab.getName() == name;
 
 308  * Returns the selected tab.
 
 309  * @return {!NavItemController|undefined}
 
 312 MdNavBarController.prototype._getSelectedTab = function() {
 
 313   return this._findTab(function(tab) {
 
 314     return tab.isSelected();
 
 319  * Returns the focused tab.
 
 320  * @return {!NavItemController|undefined}
 
 322 MdNavBarController.prototype.getFocusedTab = function() {
 
 323   return this._findTab(function(tab) {
 
 324     return tab.hasFocus();
 
 329  * Find a tab that matches the specified function.
 
 332 MdNavBarController.prototype._findTab = function(fn) {
 
 333   var tabs = this._getTabs();
 
 334   for (var i = 0; i < tabs.length; i++) {
 
 344  * Direct focus to the selected tab when focus enters the nav bar.
 
 346 MdNavBarController.prototype.onFocus = function() {
 
 347   var tab = this._getSelectedTab();
 
 349     tab.setFocused(true);
 
 354  * Move focus from oldTab to newTab.
 
 355  * @param {!NavItemController} oldTab
 
 356  * @param {!NavItemController} newTab
 
 359 MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
 
 360   oldTab.setFocused(false);
 
 361   newTab.setFocused(true);
 
 365  * Responds to keypress events.
 
 368 MdNavBarController.prototype.onKeydown = function(e) {
 
 369   var keyCodes = this._$mdConstant.KEY_CODE;
 
 370   var tabs = this._getTabs();
 
 371   var focusedTab = this.getFocusedTab();
 
 372   if (!focusedTab) return;
 
 374   var focusedTabIndex = tabs.indexOf(focusedTab);
 
 376   // use arrow keys to navigate between tabs
 
 378     case keyCodes.UP_ARROW:
 
 379     case keyCodes.LEFT_ARROW:
 
 380       if (focusedTabIndex > 0) {
 
 381         this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
 
 384     case keyCodes.DOWN_ARROW:
 
 385     case keyCodes.RIGHT_ARROW:
 
 386       if (focusedTabIndex < tabs.length - 1) {
 
 387         this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
 
 392       // timeout to avoid a "digest already in progress" console error
 
 393       this._$timeout(function() {
 
 394         focusedTab.getButtonEl().click();
 
 403 function MdNavItem($mdAria, $$rAF) {
 
 406     require: ['mdNavItem', '^mdNavBar'],
 
 407     controller: MdNavItemController,
 
 408     bindToController: true,
 
 409     controllerAs: 'ctrl',
 
 412     template: function(tElement, tAttrs) {
 
 413       var hasNavClick = tAttrs.mdNavClick;
 
 414       var hasNavHref = tAttrs.mdNavHref;
 
 415       var hasNavSref = tAttrs.mdNavSref;
 
 416       var hasSrefOpts = tAttrs.srefOpts;
 
 417       var navigationAttribute;
 
 418       var navigationOptions;
 
 421       // Cannot specify more than one nav attribute
 
 422       if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
 
 424           'Must not specify more than one of the md-nav-click, md-nav-href, ' +
 
 425           'or md-nav-sref attributes per nav-item directive.'
 
 430         navigationAttribute = 'ng-click="ctrl.mdNavClick()"';
 
 431       } else if (hasNavHref) {
 
 432         navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"';
 
 433       } else if (hasNavSref) {
 
 434         navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"';
 
 437       navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
 
 439       if (navigationAttribute) {
 
 440         buttonTemplate = '' +
 
 441           '<md-button class="_md-nav-button md-accent" ' +
 
 442             'ng-class="ctrl.getNgClassMap()" ' +
 
 443             'ng-blur="ctrl.setFocused(false)" ' +
 
 446             navigationAttribute + '>' +
 
 447             '<span ng-transclude class="_md-nav-button-text"></span>' +
 
 452         '<li class="md-nav-item" ' +
 
 454           'aria-selected="{{ctrl.isSelected()}}">' +
 
 455           (buttonTemplate || '') +
 
 465     link: function(scope, element, attrs, controllers) {
 
 466       // When accessing the element's contents synchronously, they
 
 467       // may not be defined yet because of transclusion. There is a higher
 
 468       // chance that it will be accessible if we wait one frame.
 
 470         var mdNavItem = controllers[0];
 
 471         var mdNavBar = controllers[1];
 
 472         var navButton = angular.element(element[0].querySelector('._md-nav-button'));
 
 474         if (!mdNavItem.name) {
 
 475           mdNavItem.name = angular.element(element[0]
 
 476               .querySelector('._md-nav-button-text')).text().trim();
 
 479         navButton.on('click', function() {
 
 480           mdNavBar.mdSelectedNavItem = mdNavItem.name;
 
 484         $mdAria.expectWithText(element, 'aria-label');
 
 491  * Controller for the nav-item component.
 
 492  * @param {!angular.JQLite} $element
 
 497 function MdNavItemController($element) {
 
 499   /** @private @const {!angular.JQLite} */
 
 500   this._$element = $element;
 
 502   // Data-bound variables
 
 504   /** @const {?Function} */
 
 507   /** @const {?string} */
 
 510   /** @const {?string} */
 
 512   /** @const {?Object} */
 
 514   /** @const {?string} */
 
 518   /** @private {boolean} */
 
 519   this._selected = false;
 
 521   /** @private {boolean} */
 
 522   this._focused = false;
 
 526  * Returns a map of class names and values for use by ng-class.
 
 527  * @return {!Object<string,boolean>}
 
 529 MdNavItemController.prototype.getNgClassMap = function() {
 
 531     'md-active': this._selected,
 
 532     'md-primary': this._selected,
 
 533     'md-unselected': !this._selected,
 
 534     'md-focused': this._focused,
 
 539  * Get the name attribute of the tab.
 
 542 MdNavItemController.prototype.getName = function() {
 
 547  * Get the button element associated with the tab.
 
 550 MdNavItemController.prototype.getButtonEl = function() {
 
 551   return this._$element[0].querySelector('._md-nav-button');
 
 555  * Set the selected state of the tab.
 
 556  * @param {boolean} isSelected
 
 558 MdNavItemController.prototype.setSelected = function(isSelected) {
 
 559   this._selected = isSelected;
 
 565 MdNavItemController.prototype.isSelected = function() {
 
 566   return this._selected;
 
 570  * Set the focused state of the tab.
 
 571  * @param {boolean} isFocused
 
 573 MdNavItemController.prototype.setFocused = function(isFocused) {
 
 574   this._focused = isFocused;
 
 577     this.getButtonEl().focus();
 
 584 MdNavItemController.prototype.hasFocus = function() {
 
 585   return this._focused;
 
 588 ngmaterial.components.navBar = angular.module("material.components.navBar");