You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

588 lines
16 KiB

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