navBar.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /*!
  2. * AngularJS Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.8-master-aba7b2b
  6. */
  7. goog.provide('ngmaterial.components.navBar');
  8. goog.require('ngmaterial.core');
  9. /**
  10. * @ngdoc module
  11. * @name material.components.navBar
  12. */
  13. MdNavBarController['$inject'] = ["$element", "$scope", "$timeout", "$mdConstant"];
  14. MdNavItem['$inject'] = ["$mdAria", "$$rAF", "$mdUtil", "$window"];
  15. MdNavItemController['$inject'] = ["$element"];
  16. MdNavBar['$inject'] = ["$mdAria", "$mdTheming"];
  17. angular.module('material.components.navBar', ['material.core'])
  18. .controller('MdNavBarController', MdNavBarController)
  19. .directive('mdNavBar', MdNavBar)
  20. .controller('MdNavItemController', MdNavItemController)
  21. .directive('mdNavItem', MdNavItem);
  22. /*****************************************************************************
  23. * PUBLIC DOCUMENTATION *
  24. *****************************************************************************/
  25. /**
  26. * @ngdoc directive
  27. * @name mdNavBar
  28. * @module material.components.navBar
  29. *
  30. * @restrict E
  31. *
  32. * @description
  33. * The `<md-nav-bar>` directive renders a list of material tabs that can be used
  34. * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
  35. * body and no bar pagination.
  36. *
  37. * Because it deals with page navigation, certain routing concepts are built-in.
  38. * Route changes via `ng-href`, `ui-sref`, or `ng-click` events are supported.
  39. * Alternatively, the user could simply watch the value of `md-selected-nav-item`
  40. * (`currentNavItem` in the below example) for changes.
  41. *
  42. * Accessibility functionality is implemented as a site navigator with a
  43. * listbox, according to the
  44. * <a href="https://www.w3.org/TR/2016/WD-wai-aria-practices-1.1-20160317/#Site_Navigator_Tabbed_Style">
  45. * WAI-ARIA Authoring Practices 1.1 Working Draft from March 2016</a>.
  46. * We've kept the `role="navigation"` on the `<nav>`, for backwards compatibility, even though
  47. * it is not required in the
  48. * <a href="https://www.w3.org/TR/wai-aria-practices/#aria_lh_navigation">
  49. * latest Working Group Note from December 2017</a>.
  50. *
  51. * @param {string=} md-selected-nav-item The name of the current tab; this must
  52. * match the `name` attribute of `<md-nav-item>`.
  53. * @param {boolean=} md-no-ink-bar If set to true, the ink bar will be hidden.
  54. * @param {string=} nav-bar-aria-label An `aria-label` applied to the `md-nav-bar`'s listbox
  55. * for accessibility.
  56. *
  57. * @usage
  58. * <hljs lang="html">
  59. * <md-nav-bar md-selected-nav-item="currentNavItem">
  60. * <md-nav-item md-nav-click="goto('page1')" name="page1">
  61. * Page One
  62. * </md-nav-item>
  63. * <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item>
  64. * <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item>
  65. * <md-nav-item
  66. * md-nav-sref="app.page4"
  67. * sref-opts="{reload: true, notify: true}"
  68. * name="page4">
  69. * Page Four
  70. * </md-nav-item>
  71. * </md-nav-bar>
  72. *</hljs>
  73. * <hljs lang="js">
  74. * (function() {
  75. * 'use strict';
  76. *
  77. * $rootScope.$on('$routeChangeSuccess', function(event, current) {
  78. * $scope.currentLink = getCurrentLinkFromRoute(current);
  79. * });
  80. * });
  81. * </hljs>
  82. */
  83. /*****************************************************************************
  84. * mdNavItem
  85. *****************************************************************************/
  86. /**
  87. * @ngdoc directive
  88. * @name mdNavItem
  89. * @module material.components.navBar
  90. *
  91. * @restrict E
  92. *
  93. * @description
  94. * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>` component.
  95. * It renders an `<md-button>` as the actual link.
  96. *
  97. * Exactly one of the `md-nav-click`, `md-nav-href`, or `md-nav-sref` attributes are required
  98. * to be specified.
  99. *
  100. * @param {string=} aria-label Adds alternative text for accessibility.
  101. * @param {expression=} md-nav-click Expression which will be evaluated when the
  102. * link is clicked to change the page. Renders as an `ng-click`.
  103. * @param {string=} md-nav-href url to transition to when this link is clicked.
  104. * Renders as an `ng-href`.
  105. * @param {string=} md-nav-sref UI-Router state to transition to when this link is
  106. * clicked. Renders as a `ui-sref`.
  107. * @param {string=} name The name of this link. Used by the nav bar to know
  108. * which link is currently selected.
  109. * @param {!object=} sref-opts UI-Router options that are passed to the
  110. * `$state.go()` function. See the [UI-Router documentation for details]
  111. * (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html).
  112. *
  113. * @usage
  114. * See `<md-nav-bar>` for usage.
  115. */
  116. /*****************************************************************************
  117. * IMPLEMENTATION *
  118. *****************************************************************************/
  119. function MdNavBar($mdAria, $mdTheming) {
  120. return {
  121. restrict: 'E',
  122. transclude: true,
  123. controller: MdNavBarController,
  124. controllerAs: 'ctrl',
  125. bindToController: true,
  126. scope: {
  127. 'mdSelectedNavItem': '=?',
  128. 'mdNoInkBar': '=?',
  129. 'navBarAriaLabel': '@?',
  130. },
  131. template:
  132. '<div class="md-nav-bar">' +
  133. '<nav role="navigation">' +
  134. '<ul class="_md-nav-bar-list" ng-transclude role="listbox" ' +
  135. 'tabindex="0" ' +
  136. 'ng-focus="ctrl.onFocus()" ' +
  137. 'ng-keydown="ctrl.onKeydown($event)" ' +
  138. 'aria-label="{{ctrl.navBarAriaLabel}}">' +
  139. '</ul>' +
  140. '</nav>' +
  141. '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
  142. '</div>',
  143. link: function(scope, element, attrs, ctrl) {
  144. $mdTheming(element);
  145. if (!ctrl.navBarAriaLabel) {
  146. $mdAria.expectAsync(element, 'aria-label', angular.noop);
  147. }
  148. },
  149. };
  150. }
  151. /**
  152. * Controller for the nav-bar component.
  153. *
  154. * Accessibility functionality is implemented as a site navigator with a
  155. * listbox, according to
  156. * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
  157. * @param {!angular.JQLite} $element
  158. * @param {!angular.Scope} $scope
  159. * @param {!angular.Timeout} $timeout
  160. * @param {!Object} $mdConstant
  161. * @constructor
  162. * @final
  163. * ngInject
  164. */
  165. function MdNavBarController($element, $scope, $timeout, $mdConstant) {
  166. // Injected variables
  167. /** @private @const {!angular.Timeout} */
  168. this._$timeout = $timeout;
  169. /** @private @const {!angular.Scope} */
  170. this._$scope = $scope;
  171. /** @private @const {!Object} */
  172. this._$mdConstant = $mdConstant;
  173. // Data-bound variables.
  174. /** @type {string} */
  175. this.mdSelectedNavItem;
  176. /** @type {string} */
  177. this.navBarAriaLabel;
  178. // State variables.
  179. /** @type {?angular.JQLite} */
  180. this._navBarEl = $element[0];
  181. /** @type {?angular.JQLite} */
  182. this._inkbar;
  183. var self = this;
  184. // need to wait for transcluded content to be available
  185. var deregisterTabWatch = this._$scope.$watch(function() {
  186. return self._navBarEl.querySelectorAll('._md-nav-button').length;
  187. },
  188. function(newLength) {
  189. if (newLength > 0) {
  190. self._initTabs();
  191. deregisterTabWatch();
  192. }
  193. });
  194. }
  195. /**
  196. * Initializes the tab components once they exist.
  197. * @private
  198. */
  199. MdNavBarController.prototype._initTabs = function() {
  200. this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
  201. var self = this;
  202. this._$timeout(function() {
  203. self._updateTabs(self.mdSelectedNavItem, undefined);
  204. });
  205. this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
  206. // Wait a digest before update tabs for products doing
  207. // anything dynamic in the template.
  208. self._$timeout(function() {
  209. self._updateTabs(newValue, oldValue);
  210. });
  211. });
  212. };
  213. /**
  214. * Set the current tab to be selected.
  215. * @param {string|undefined} newValue New current tab name.
  216. * @param {string|undefined} oldValue Previous tab name.
  217. * @private
  218. */
  219. MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
  220. var self = this;
  221. var tabs = this._getTabs();
  222. // this._getTabs can return null if nav-bar has not yet been initialized
  223. if(!tabs)
  224. return;
  225. var oldIndex = -1;
  226. var newIndex = -1;
  227. var newTab = this._getTabByName(newValue);
  228. var oldTab = this._getTabByName(oldValue);
  229. if (oldTab) {
  230. oldTab.setSelected(false);
  231. oldIndex = tabs.indexOf(oldTab);
  232. }
  233. if (newTab) {
  234. newTab.setSelected(true);
  235. newIndex = tabs.indexOf(newTab);
  236. }
  237. this._$timeout(function() {
  238. self._updateInkBarStyles(newTab, newIndex, oldIndex);
  239. });
  240. };
  241. /**
  242. * Repositions the ink bar to the selected tab.
  243. * @private
  244. */
  245. MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
  246. this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
  247. .toggleClass('_md-right', newIndex > oldIndex);
  248. this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
  249. if (tab) {
  250. var tabEl = tab.getButtonEl();
  251. var left = tabEl.offsetLeft;
  252. this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
  253. }
  254. };
  255. /**
  256. * Returns an array of the current tabs.
  257. * @return {!Array<!NavItemController>}
  258. * @private
  259. */
  260. MdNavBarController.prototype._getTabs = function() {
  261. var controllers = Array.prototype.slice.call(
  262. this._navBarEl.querySelectorAll('.md-nav-item'))
  263. .map(function(el) {
  264. return angular.element(el).controller('mdNavItem');
  265. });
  266. return controllers.indexOf(undefined) ? controllers : null;
  267. };
  268. /**
  269. * Returns the tab with the specified name.
  270. * @param {string} name The name of the tab, found in its name attribute.
  271. * @return {!NavItemController|undefined}
  272. * @private
  273. */
  274. MdNavBarController.prototype._getTabByName = function(name) {
  275. return this._findTab(function(tab) {
  276. return tab.getName() == name;
  277. });
  278. };
  279. /**
  280. * Returns the selected tab.
  281. * @return {!NavItemController|undefined}
  282. * @private
  283. */
  284. MdNavBarController.prototype._getSelectedTab = function() {
  285. return this._findTab(function(tab) {
  286. return tab.isSelected();
  287. });
  288. };
  289. /**
  290. * Returns the focused tab.
  291. * @return {!NavItemController|undefined}
  292. */
  293. MdNavBarController.prototype.getFocusedTab = function() {
  294. return this._findTab(function(tab) {
  295. return tab.hasFocus();
  296. });
  297. };
  298. /**
  299. * Find a tab that matches the specified function.
  300. * @private
  301. */
  302. MdNavBarController.prototype._findTab = function(fn) {
  303. var tabs = this._getTabs();
  304. for (var i = 0; i < tabs.length; i++) {
  305. if (fn(tabs[i])) {
  306. return tabs[i];
  307. }
  308. }
  309. return null;
  310. };
  311. /**
  312. * Direct focus to the selected tab when focus enters the nav bar.
  313. */
  314. MdNavBarController.prototype.onFocus = function() {
  315. var tab = this._getSelectedTab();
  316. if (tab) {
  317. tab.setFocused(true);
  318. }
  319. };
  320. /**
  321. * Move focus from oldTab to newTab.
  322. * @param {!NavItemController} oldTab
  323. * @param {!NavItemController} newTab
  324. * @private
  325. */
  326. MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
  327. oldTab.setFocused(false);
  328. newTab.setFocused(true);
  329. };
  330. /**
  331. * Responds to keypress events.
  332. * @param {!Event} e
  333. */
  334. MdNavBarController.prototype.onKeydown = function(e) {
  335. var keyCodes = this._$mdConstant.KEY_CODE;
  336. var tabs = this._getTabs();
  337. var focusedTab = this.getFocusedTab();
  338. if (!focusedTab) return;
  339. var focusedTabIndex = tabs.indexOf(focusedTab);
  340. // use arrow keys to navigate between tabs
  341. switch (e.keyCode) {
  342. case keyCodes.UP_ARROW:
  343. case keyCodes.LEFT_ARROW:
  344. if (focusedTabIndex > 0) {
  345. this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
  346. }
  347. break;
  348. case keyCodes.DOWN_ARROW:
  349. case keyCodes.RIGHT_ARROW:
  350. if (focusedTabIndex < tabs.length - 1) {
  351. this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
  352. }
  353. break;
  354. case keyCodes.SPACE:
  355. case keyCodes.ENTER:
  356. // timeout to avoid a "digest already in progress" console error
  357. this._$timeout(function() {
  358. focusedTab.getButtonEl().click();
  359. });
  360. break;
  361. }
  362. };
  363. /**
  364. * ngInject
  365. */
  366. function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
  367. return {
  368. restrict: 'E',
  369. require: ['mdNavItem', '^mdNavBar'],
  370. controller: MdNavItemController,
  371. bindToController: true,
  372. controllerAs: 'ctrl',
  373. replace: true,
  374. transclude: true,
  375. template: function(tElement, tAttrs) {
  376. var hasNavClick = tAttrs.mdNavClick;
  377. var hasNavHref = tAttrs.mdNavHref;
  378. var hasNavSref = tAttrs.mdNavSref;
  379. var hasSrefOpts = tAttrs.srefOpts;
  380. var navigationAttribute;
  381. var navigationOptions;
  382. var buttonTemplate;
  383. // Cannot specify more than one nav attribute
  384. if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
  385. throw Error(
  386. 'Must not specify more than one of the md-nav-click, md-nav-href, ' +
  387. 'or md-nav-sref attributes per nav-item directive.'
  388. );
  389. }
  390. if (hasNavClick) {
  391. navigationAttribute = 'ng-click="ctrl.mdNavClick()"';
  392. } else if (hasNavHref) {
  393. navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"';
  394. } else if (hasNavSref) {
  395. navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"';
  396. }
  397. navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
  398. if (navigationAttribute) {
  399. buttonTemplate = '' +
  400. '<md-button class="_md-nav-button md-accent" ' +
  401. 'ng-class="ctrl.getNgClassMap()" ' +
  402. 'ng-blur="ctrl.setFocused(false)" ' +
  403. 'ng-disabled="ctrl.disabled" ' +
  404. 'tabindex="-1" ' +
  405. navigationOptions +
  406. navigationAttribute + '>' +
  407. '<span ng-transclude class="_md-nav-button-text"></span>' +
  408. '</md-button>';
  409. }
  410. return '' +
  411. '<li class="md-nav-item" ' +
  412. 'role="option" ' +
  413. 'aria-selected="{{ctrl.isSelected()}}">' +
  414. (buttonTemplate || '') +
  415. '</li>';
  416. },
  417. scope: {
  418. 'mdNavClick': '&?',
  419. 'mdNavHref': '@?',
  420. 'mdNavSref': '@?',
  421. 'srefOpts': '=?',
  422. 'name': '@',
  423. },
  424. link: function(scope, element, attrs, controllers) {
  425. var disconnect;
  426. // When accessing the element's contents synchronously, they
  427. // may not be defined yet because of transclusion. There is a higher
  428. // chance that it will be accessible if we wait one frame.
  429. $$rAF(function() {
  430. var mdNavItem = controllers[0];
  431. var mdNavBar = controllers[1];
  432. var navButton = angular.element(element[0].querySelector('._md-nav-button'));
  433. if (!mdNavItem.name) {
  434. mdNavItem.name = angular.element(element[0]
  435. .querySelector('._md-nav-button-text')).text().trim();
  436. }
  437. navButton.on('click', function() {
  438. mdNavBar.mdSelectedNavItem = mdNavItem.name;
  439. scope.$apply();
  440. });
  441. // Get the disabled attribute value first, then setup observing of value changes
  442. mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs['disabled'], false);
  443. if ('MutationObserver' in $window) {
  444. var config = {attributes: true, attributeFilter: ['disabled']};
  445. var targetNode = element[0];
  446. var mutationCallback = function(mutationList) {
  447. $mdUtil.nextTick(function() {
  448. mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs[mutationList[0].attributeName], false);
  449. });
  450. };
  451. var observer = new MutationObserver(mutationCallback);
  452. observer.observe(targetNode, config);
  453. disconnect = observer.disconnect.bind(observer);
  454. } else {
  455. attrs.$observe('disabled', function (value) {
  456. mdNavItem.disabled = $mdUtil.parseAttributeBoolean(value, false);
  457. });
  458. }
  459. $mdAria.expectWithText(element, 'aria-label');
  460. });
  461. scope.$on('destroy', function() {
  462. disconnect();
  463. })
  464. }
  465. };
  466. }
  467. /**
  468. * Controller for the nav-item component.
  469. * @param {!angular.JQLite} $element
  470. * @constructor
  471. * @final
  472. * ngInject
  473. */
  474. function MdNavItemController($element) {
  475. /** @private @const {!angular.JQLite} */
  476. this._$element = $element;
  477. // Data-bound variables
  478. /** @const {?Function} */
  479. this.mdNavClick;
  480. /** @const {?string} */
  481. this.mdNavHref;
  482. /** @const {?string} */
  483. this.mdNavSref;
  484. /** @const {?Object} */
  485. this.srefOpts;
  486. /** @const {?string} */
  487. this.name;
  488. // State variables
  489. /** @private {boolean} */
  490. this._selected = false;
  491. /** @private {boolean} */
  492. this._focused = false;
  493. }
  494. /**
  495. * Returns a map of class names and values for use by ng-class.
  496. * @return {!Object<string,boolean>}
  497. */
  498. MdNavItemController.prototype.getNgClassMap = function() {
  499. return {
  500. 'md-active': this._selected,
  501. 'md-primary': this._selected,
  502. 'md-unselected': !this._selected,
  503. 'md-focused': this._focused,
  504. };
  505. };
  506. /**
  507. * Get the name attribute of the tab.
  508. * @return {string}
  509. */
  510. MdNavItemController.prototype.getName = function() {
  511. return this.name;
  512. };
  513. /**
  514. * Get the button element associated with the tab.
  515. * @return {!Element}
  516. */
  517. MdNavItemController.prototype.getButtonEl = function() {
  518. return this._$element[0].querySelector('._md-nav-button');
  519. };
  520. /**
  521. * Set the selected state of the tab.
  522. * @param {boolean} isSelected
  523. */
  524. MdNavItemController.prototype.setSelected = function(isSelected) {
  525. this._selected = isSelected;
  526. };
  527. /**
  528. * @return {boolean}
  529. */
  530. MdNavItemController.prototype.isSelected = function() {
  531. return this._selected;
  532. };
  533. /**
  534. * Set the focused state of the tab.
  535. * @param {boolean} isFocused
  536. */
  537. MdNavItemController.prototype.setFocused = function(isFocused) {
  538. this._focused = isFocused;
  539. if (isFocused) {
  540. this.getButtonEl().focus();
  541. }
  542. };
  543. /**
  544. * @return {boolean}
  545. */
  546. MdNavItemController.prototype.hasFocus = function() {
  547. return this._focused;
  548. };
  549. ngmaterial.components.navBar = angular.module("material.components.navBar");