39ace7bdeac3eb4ca33b97bf81f87bfca76c3201
[vnfsdk/refrepo.git] /
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v1.1.3
6  */
7 goog.provide('ngmaterial.components.navBar');
8 goog.require('ngmaterial.core');
9 /**
10  * @ngdoc module
11  * @name material.components.navBar
12  */
13
14
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);
24
25
26 /*****************************************************************************
27  *                            PUBLIC DOCUMENTATION                           *
28  *****************************************************************************/
29 /**
30  * @ngdoc directive
31  * @name mdNavBar
32  * @module material.components.navBar
33  *
34  * @restrict E
35  *
36  * @description
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.
40  *
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.
44  *
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
48  *
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
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">
58  *      Page One
59  *    </md-nav-item>
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>
62  *    <md-nav-item
63  *      md-nav-sref="app.page4"
64  *      sref-opts="{reload: true, notify: true}"
65  *      name="page4">
66  *      Page Four
67  *    </md-nav-item>
68  *  </md-nav-bar>
69  *</hljs>
70  * <hljs lang="js">
71  * (function() {
72  *   'use strict';
73  *
74  *    $rootScope.$on('$routeChangeSuccess', function(event, current) {
75  *      $scope.currentLink = getCurrentLinkFromRoute(current);
76  *    });
77  * });
78  * </hljs>
79  */
80
81 /*****************************************************************************
82  *                            mdNavItem
83  *****************************************************************************/
84 /**
85  * @ngdoc directive
86  * @name mdNavItem
87  * @module material.components.navBar
88  *
89  * @restrict E
90  *
91  * @description
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.
94  *
95  * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required
96  * to be specified.
97  *
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
110  *
111  * @usage
112  * See `<md-nav-bar>` for usage.
113  */
114
115
116 /*****************************************************************************
117  *                                IMPLEMENTATION                             *
118  *****************************************************************************/
119
120 function MdNavBar($mdAria, $mdTheming) {
121   return {
122     restrict: 'E',
123     transclude: true,
124     controller: MdNavBarController,
125     controllerAs: 'ctrl',
126     bindToController: true,
127     scope: {
128       'mdSelectedNavItem': '=?',
129       'mdNoInkBar': '=?',
130       'navBarAriaLabel': '@?',
131     },
132     template:
133       '<div class="md-nav-bar">' +
134         '<nav role="navigation">' +
135           '<ul class="_md-nav-bar-list" ng-transclude role="listbox"' +
136             'tabindex="0"' +
137             'ng-focus="ctrl.onFocus()"' +
138             'ng-keydown="ctrl.onKeydown($event)"' +
139             'aria-label="{{ctrl.navBarAriaLabel}}">' +
140           '</ul>' +
141         '</nav>' +
142         '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
143       '</div>',
144     link: function(scope, element, attrs, ctrl) {
145       $mdTheming(element);
146       if (!ctrl.navBarAriaLabel) {
147         $mdAria.expectAsync(element, 'aria-label', angular.noop);
148       }
149     },
150   };
151 }
152
153 /**
154  * Controller for the nav-bar component.
155  *
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
163  * @constructor
164  * @final
165  * ngInject
166  */
167 function MdNavBarController($element, $scope, $timeout, $mdConstant) {
168   // Injected variables
169   /** @private @const {!angular.Timeout} */
170   this._$timeout = $timeout;
171
172   /** @private @const {!angular.Scope} */
173   this._$scope = $scope;
174
175   /** @private @const {!Object} */
176   this._$mdConstant = $mdConstant;
177
178   // Data-bound variables.
179   /** @type {string} */
180   this.mdSelectedNavItem;
181
182   /** @type {string} */
183   this.navBarAriaLabel;
184
185   // State variables.
186
187   /** @type {?angular.JQLite} */
188   this._navBarEl = $element[0];
189
190   /** @type {?angular.JQLite} */
191   this._inkbar;
192
193   var self = this;
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;
197   },
198   function(newLength) {
199     if (newLength > 0) {
200       self._initTabs();
201       deregisterTabWatch();
202     }
203   });
204 }
205
206
207
208 /**
209  * Initializes the tab components once they exist.
210  * @private
211  */
212 MdNavBarController.prototype._initTabs = function() {
213   this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
214
215   var self = this;
216   this._$timeout(function() {
217     self._updateTabs(self.mdSelectedNavItem, undefined);
218   });
219
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);
225     });
226   });
227 };
228
229 /**
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.
233  * @private
234  */
235 MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
236   var self = this;
237   var tabs = this._getTabs();
238
239   // this._getTabs can return null if nav-bar has not yet been initialized
240   if(!tabs)
241     return;
242
243   var oldIndex = -1;
244   var newIndex = -1;
245   var newTab = this._getTabByName(newValue);
246   var oldTab = this._getTabByName(oldValue);
247
248   if (oldTab) {
249     oldTab.setSelected(false);
250     oldIndex = tabs.indexOf(oldTab);
251   }
252
253   if (newTab) {
254     newTab.setSelected(true);
255     newIndex = tabs.indexOf(newTab);
256   }
257
258   this._$timeout(function() {
259     self._updateInkBarStyles(newTab, newIndex, oldIndex);
260   });
261 };
262
263 /**
264  * Repositions the ink bar to the selected tab.
265  * @private
266  */
267 MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
268   this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
269       .toggleClass('_md-right', newIndex > oldIndex);
270
271   this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
272
273   if (tab) {
274     var tabEl = tab.getButtonEl();
275     var left = tabEl.offsetLeft;
276
277     this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
278   }
279 };
280
281 /**
282  * Returns an array of the current tabs.
283  * @return {!Array<!NavItemController>}
284  * @private
285  */
286 MdNavBarController.prototype._getTabs = function() {
287   var controllers = Array.prototype.slice.call(
288     this._navBarEl.querySelectorAll('.md-nav-item'))
289     .map(function(el) {
290       return angular.element(el).controller('mdNavItem')
291     });
292   return controllers.indexOf(undefined) ? controllers : null;
293 };
294
295 /**
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}
299  * @private
300  */
301 MdNavBarController.prototype._getTabByName = function(name) {
302   return this._findTab(function(tab) {
303     return tab.getName() == name;
304   });
305 };
306
307 /**
308  * Returns the selected tab.
309  * @return {!NavItemController|undefined}
310  * @private
311  */
312 MdNavBarController.prototype._getSelectedTab = function() {
313   return this._findTab(function(tab) {
314     return tab.isSelected();
315   });
316 };
317
318 /**
319  * Returns the focused tab.
320  * @return {!NavItemController|undefined}
321  */
322 MdNavBarController.prototype.getFocusedTab = function() {
323   return this._findTab(function(tab) {
324     return tab.hasFocus();
325   });
326 };
327
328 /**
329  * Find a tab that matches the specified function.
330  * @private
331  */
332 MdNavBarController.prototype._findTab = function(fn) {
333   var tabs = this._getTabs();
334   for (var i = 0; i < tabs.length; i++) {
335     if (fn(tabs[i])) {
336       return tabs[i];
337     }
338   }
339
340   return null;
341 };
342
343 /**
344  * Direct focus to the selected tab when focus enters the nav bar.
345  */
346 MdNavBarController.prototype.onFocus = function() {
347   var tab = this._getSelectedTab();
348   if (tab) {
349     tab.setFocused(true);
350   }
351 };
352
353 /**
354  * Move focus from oldTab to newTab.
355  * @param {!NavItemController} oldTab
356  * @param {!NavItemController} newTab
357  * @private
358  */
359 MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
360   oldTab.setFocused(false);
361   newTab.setFocused(true);
362 };
363
364 /**
365  * Responds to keypress events.
366  * @param {!Event} e
367  */
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;
373
374   var focusedTabIndex = tabs.indexOf(focusedTab);
375
376   // use arrow keys to navigate between tabs
377   switch (e.keyCode) {
378     case keyCodes.UP_ARROW:
379     case keyCodes.LEFT_ARROW:
380       if (focusedTabIndex > 0) {
381         this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
382       }
383       break;
384     case keyCodes.DOWN_ARROW:
385     case keyCodes.RIGHT_ARROW:
386       if (focusedTabIndex < tabs.length - 1) {
387         this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
388       }
389       break;
390     case keyCodes.SPACE:
391     case keyCodes.ENTER:
392       // timeout to avoid a "digest already in progress" console error
393       this._$timeout(function() {
394         focusedTab.getButtonEl().click();
395       });
396       break;
397   }
398 };
399
400 /**
401  * ngInject
402  */
403 function MdNavItem($mdAria, $$rAF) {
404   return {
405     restrict: 'E',
406     require: ['mdNavItem', '^mdNavBar'],
407     controller: MdNavItemController,
408     bindToController: true,
409     controllerAs: 'ctrl',
410     replace: true,
411     transclude: true,
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;
419       var buttonTemplate;
420
421       // Cannot specify more than one nav attribute
422       if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
423         throw Error(
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.'
426         );
427       }
428
429       if (hasNavClick) {
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}}"';
435       }
436
437       navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
438
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)" ' +
444             'tabindex="-1" ' +
445             navigationOptions +
446             navigationAttribute + '>' +
447             '<span ng-transclude class="_md-nav-button-text"></span>' +
448           '</md-button>';
449       }
450
451       return '' +
452         '<li class="md-nav-item" ' +
453           'role="option" ' +
454           'aria-selected="{{ctrl.isSelected()}}">' +
455           (buttonTemplate || '') +
456         '</li>';
457     },
458     scope: {
459       'mdNavClick': '&?',
460       'mdNavHref': '@?',
461       'mdNavSref': '@?',
462       'srefOpts': '=?',
463       'name': '@',
464     },
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.
469       $$rAF(function() {
470         var mdNavItem = controllers[0];
471         var mdNavBar = controllers[1];
472         var navButton = angular.element(element[0].querySelector('._md-nav-button'));
473
474         if (!mdNavItem.name) {
475           mdNavItem.name = angular.element(element[0]
476               .querySelector('._md-nav-button-text')).text().trim();
477         }
478
479         navButton.on('click', function() {
480           mdNavBar.mdSelectedNavItem = mdNavItem.name;
481           scope.$apply();
482         });
483
484         $mdAria.expectWithText(element, 'aria-label');
485       });
486     }
487   };
488 }
489
490 /**
491  * Controller for the nav-item component.
492  * @param {!angular.JQLite} $element
493  * @constructor
494  * @final
495  * ngInject
496  */
497 function MdNavItemController($element) {
498
499   /** @private @const {!angular.JQLite} */
500   this._$element = $element;
501
502   // Data-bound variables
503
504   /** @const {?Function} */
505   this.mdNavClick;
506
507   /** @const {?string} */
508   this.mdNavHref;
509
510   /** @const {?string} */
511   this.mdNavSref;
512   /** @const {?Object} */
513   this.srefOpts;
514   /** @const {?string} */
515   this.name;
516
517   // State variables
518   /** @private {boolean} */
519   this._selected = false;
520
521   /** @private {boolean} */
522   this._focused = false;
523 }
524
525 /**
526  * Returns a map of class names and values for use by ng-class.
527  * @return {!Object<string,boolean>}
528  */
529 MdNavItemController.prototype.getNgClassMap = function() {
530   return {
531     'md-active': this._selected,
532     'md-primary': this._selected,
533     'md-unselected': !this._selected,
534     'md-focused': this._focused,
535   };
536 };
537
538 /**
539  * Get the name attribute of the tab.
540  * @return {string}
541  */
542 MdNavItemController.prototype.getName = function() {
543   return this.name;
544 };
545
546 /**
547  * Get the button element associated with the tab.
548  * @return {!Element}
549  */
550 MdNavItemController.prototype.getButtonEl = function() {
551   return this._$element[0].querySelector('._md-nav-button');
552 };
553
554 /**
555  * Set the selected state of the tab.
556  * @param {boolean} isSelected
557  */
558 MdNavItemController.prototype.setSelected = function(isSelected) {
559   this._selected = isSelected;
560 };
561
562 /**
563  * @return {boolean}
564  */
565 MdNavItemController.prototype.isSelected = function() {
566   return this._selected;
567 };
568
569 /**
570  * Set the focused state of the tab.
571  * @param {boolean} isFocused
572  */
573 MdNavItemController.prototype.setFocused = function(isFocused) {
574   this._focused = isFocused;
575
576   if (isFocused) {
577     this.getButtonEl().focus();
578   }
579 };
580
581 /**
582  * @return {boolean}
583  */
584 MdNavItemController.prototype.hasFocus = function() {
585   return this._focused;
586 };
587
588 ngmaterial.components.navBar = angular.module("material.components.navBar");