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.

551 lines
15 KiB

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