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