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

/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v1.1.1
*/
goog.provide('ngmaterial.components.navBar');
goog.require('ngmaterial.core');
/**
* @ngdoc module
* @name material.components.navBar
*/
MdNavBarController.$inject = ["$element", "$scope", "$timeout", "$mdConstant"];
MdNavItem.$inject = ["$$rAF"];
MdNavItemController.$inject = ["$element"];
MdNavBar.$inject = ["$mdAria", "$mdTheming"];
angular.module('material.components.navBar', ['material.core'])
.controller('MdNavBarController', MdNavBarController)
.directive('mdNavBar', MdNavBar)
.controller('MdNavItemController', MdNavItemController)
.directive('mdNavItem', MdNavItem);
/*****************************************************************************
* PUBLIC DOCUMENTATION *
*****************************************************************************/
/**
* @ngdoc directive
* @name mdNavBar
* @module material.components.navBar
*
* @restrict E
*
* @description
* The `<md-nav-bar>` directive renders a list of material tabs that can be used
* for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
* body and no bar pagination.
*
* Because it deals with page navigation, certain routing concepts are built-in.
* Route changes via via ng-href, ui-sref, or ng-click events are supported.
* Alternatively, the user could simply watch currentNavItem for changes.
*
* Accessibility functionality is implemented as a site navigator with a
* listbox, according to
* https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
*
* @param {string=} mdSelectedNavItem The name of the current tab; this must
* match the name attribute of `<md-nav-item>`
* @param {string=} navBarAriaLabel An aria-label for the nav-bar
*
* @usage
* <hljs lang="html">
* <md-nav-bar md-selected-nav-item="currentNavItem">
* <md-nav-item md-nav-click="goto('page1')" name="page1">Page One</md-nav-item>
* <md-nav-item md-nav-sref="app.page2" name="page2">Page Two</md-nav-item>
* <md-nav-item md-nav-href="#page3" name="page3">Page Three</md-nav-item>
* </md-nav-bar>
*</hljs>
* <hljs lang="js">
* (function() {
* ‘use strict’;
*
* $rootScope.$on('$routeChangeSuccess', function(event, current) {
* $scope.currentLink = getCurrentLinkFromRoute(current);
* });
* });
* </hljs>
*/
/*****************************************************************************
* mdNavItem
*****************************************************************************/
/**
* @ngdoc directive
* @name mdNavItem
* @module material.components.navBar
*
* @restrict E
*
* @description
* `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>`
* component. It renders an md-button as the actual link.
*
* Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required to be
* specified.
*
* @param {Function=} mdNavClick Function which will be called when the
* link is clicked to change the page. Renders as an `ng-click`.
* @param {string=} mdNavHref url to transition to when this link is clicked.
* Renders as an `ng-href`.
* @param {string=} mdNavSref Ui-router state to transition to when this link is
* clicked. Renders as a `ui-sref`.
* @param {string=} name The name of this link. Used by the nav bar to know
* which link is currently selected.
*
* @usage
* See `<md-nav-bar>` for usage.
*/
/*****************************************************************************
* IMPLEMENTATION *
*****************************************************************************/
function MdNavBar($mdAria, $mdTheming) {
return {
restrict: 'E',
transclude: true,
controller: MdNavBarController,
controllerAs: 'ctrl',
bindToController: true,
scope: {
'mdSelectedNavItem': '=?',
'navBarAriaLabel': '@?',
},
template:
'<div class="md-nav-bar">' +
'<nav role="navigation">' +
'<ul class="_md-nav-bar-list" ng-transclude role="listbox"' +
'tabindex="0"' +
'ng-focus="ctrl.onFocus()"' +
'ng-blur="ctrl.onBlur()"' +
'ng-keydown="ctrl.onKeydown($event)"' +
'aria-label="{{ctrl.navBarAriaLabel}}">' +
'</ul>' +
'</nav>' +
'<md-nav-ink-bar></md-nav-ink-bar>' +
'</div>',
link: function(scope, element, attrs, ctrl) {
$mdTheming(element);
if (!ctrl.navBarAriaLabel) {
$mdAria.expectAsync(element, 'aria-label', angular.noop);
}
},
};
}
/**
* Controller for the nav-bar component.
*
* Accessibility functionality is implemented as a site navigator with a
* listbox, according to
* https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
* @param {!angular.JQLite} $element
* @param {!angular.Scope} $scope
* @param {!angular.Timeout} $timeout
* @param {!Object} $mdConstant
* @constructor
* @final
* ngInject
*/
function MdNavBarController($element, $scope, $timeout, $mdConstant) {
// Injected variables
/** @private @const {!angular.Timeout} */
this._$timeout = $timeout;
/** @private @const {!angular.Scope} */
this._$scope = $scope;
/** @private @const {!Object} */
this._$mdConstant = $mdConstant;
// Data-bound variables.
/** @type {string} */
this.mdSelectedNavItem;
/** @type {string} */
this.navBarAriaLabel;
// State variables.
/** @type {?angular.JQLite} */
this._navBarEl = $element[0];
/** @type {?angular.JQLite} */
this._inkbar;
var self = this;
// need to wait for transcluded content to be available
var deregisterTabWatch = this._$scope.$watch(function() {
return self._navBarEl.querySelectorAll('._md-nav-button').length;
},
function(newLength) {
if (newLength > 0) {
self._initTabs();
deregisterTabWatch();
}
});
}
/**
* Initializes the tab components once they exist.
* @private
*/
MdNavBarController.prototype._initTabs = function() {
this._inkbar = angular.element(this._navBarEl.getElementsByTagName('md-nav-ink-bar')[0]);
var self = this;
this._$timeout(function() {
self._updateTabs(self.mdSelectedNavItem, undefined);
});
this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
// Wait a digest before update tabs for products doing
// anything dynamic in the template.
self._$timeout(function() {
self._updateTabs(newValue, oldValue);
});
});
};
/**
* Set the current tab to be selected.
* @param {string|undefined} newValue New current tab name.
* @param {string|undefined} oldValue Previous tab name.
* @private
*/
MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
var self = this;
var tabs = this._getTabs();
var oldIndex = -1;
var newIndex = -1;
var newTab = this._getTabByName(newValue);
var oldTab = this._getTabByName(oldValue);
if (oldTab) {
oldTab.setSelected(false);
oldIndex = tabs.indexOf(oldTab);
}
if (newTab) {
newTab.setSelected(true);
newIndex = tabs.indexOf(newTab);
}
this._$timeout(function() {
self._updateInkBarStyles(newTab, newIndex, oldIndex);
});
};
/**
* Repositions the ink bar to the selected tab.
* @private
*/
MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
.toggleClass('_md-right', newIndex > oldIndex);
this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
if(tab){
var tabEl = tab.getButtonEl();
var left = tabEl.offsetLeft;
this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
}
};
/**
* Returns an array of the current tabs.
* @return {!Array<!NavItemController>}
* @private
*/
MdNavBarController.prototype._getTabs = function() {
var linkArray = Array.prototype.slice.call(
this._navBarEl.querySelectorAll('.md-nav-item'));
return linkArray.map(function(el) {
return angular.element(el).controller('mdNavItem')
});
};
/**
* Returns the tab with the specified name.
* @param {string} name The name of the tab, found in its name attribute.
* @return {!NavItemController|undefined}
* @private
*/
MdNavBarController.prototype._getTabByName = function(name) {
return this._findTab(function(tab) {
return tab.getName() == name;
});
};
/**
* Returns the selected tab.
* @return {!NavItemController|undefined}
* @private
*/
MdNavBarController.prototype._getSelectedTab = function() {
return this._findTab(function(tab) {
return tab.isSelected()
});
};
/**
* Returns the focused tab.
* @return {!NavItemController|undefined}
*/
MdNavBarController.prototype.getFocusedTab = function() {
return this._findTab(function(tab) {
return tab.hasFocus()
});
};
/**
* Find a tab that matches the specified function.
* @private
*/
MdNavBarController.prototype._findTab = function(fn) {
var tabs = this._getTabs();
for (var i = 0; i < tabs.length; i++) {
if (fn(tabs[i])) {
return tabs[i];
}
}
return null;
};
/**
* Direct focus to the selected tab when focus enters the nav bar.
*/
MdNavBarController.prototype.onFocus = function() {
var tab = this._getSelectedTab();
if (tab) {
tab.setFocused(true);
}
};
/**
* Clear tab focus when focus leaves the nav bar.
*/
MdNavBarController.prototype.onBlur = function() {
var tab = this.getFocusedTab();
if (tab) {
tab.setFocused(false);
}
};
/**
* Move focus from oldTab to newTab.
* @param {!NavItemController} oldTab
* @param {!NavItemController} newTab
* @private
*/
MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
oldTab.setFocused(false);
newTab.setFocused(true);
};
/**
* Responds to keypress events.
* @param {!Event} e
*/
MdNavBarController.prototype.onKeydown = function(e) {
var keyCodes = this._$mdConstant.KEY_CODE;
var tabs = this._getTabs();
var focusedTab = this.getFocusedTab();
if (!focusedTab) return;
var focusedTabIndex = tabs.indexOf(focusedTab);
// use arrow keys to navigate between tabs
switch (e.keyCode) {
case keyCodes.UP_ARROW:
case keyCodes.LEFT_ARROW:
if (focusedTabIndex > 0) {
this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
}
break;
case keyCodes.DOWN_ARROW:
case keyCodes.RIGHT_ARROW:
if (focusedTabIndex < tabs.length - 1) {
this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
}
break;
case keyCodes.SPACE:
case keyCodes.ENTER:
// timeout to avoid a "digest already in progress" console error
this._$timeout(function() {
focusedTab.getButtonEl().click();
});
break;
}
};
/**
* ngInject
*/
function MdNavItem($$rAF) {
return {
restrict: 'E',
require: ['mdNavItem', '^mdNavBar'],
controller: MdNavItemController,
bindToController: true,
controllerAs: 'ctrl',
replace: true,
transclude: true,
template:
'<li class="md-nav-item" role="option" aria-selected="{{ctrl.isSelected()}}">' +
'<md-button ng-if="ctrl.mdNavSref" class="_md-nav-button md-accent"' +
'ng-class="ctrl.getNgClassMap()"' +
'tabindex="-1"' +
'ui-sref="{{ctrl.mdNavSref}}">' +
'<span ng-transclude class="_md-nav-button-text"></span>' +
'</md-button>' +
'<md-button ng-if="ctrl.mdNavHref" class="_md-nav-button md-accent"' +
'ng-class="ctrl.getNgClassMap()"' +
'tabindex="-1"' +
'ng-href="{{ctrl.mdNavHref}}">' +
'<span ng-transclude class="_md-nav-button-text"></span>' +
'</md-button>' +
'<md-button ng-if="ctrl.mdNavClick" class="_md-nav-button md-accent"' +
'ng-class="ctrl.getNgClassMap()"' +
'tabindex="-1"' +
'ng-click="ctrl.mdNavClick()">' +
'<span ng-transclude class="_md-nav-button-text"></span>' +
'</md-button>' +
'</li>',
scope: {
'mdNavClick': '&?',
'mdNavHref': '@?',
'mdNavSref': '@?',
'name': '@',
},
link: function(scope, element, attrs, controllers) {
var mdNavItem = controllers[0];
var mdNavBar = controllers[1];
// When accessing the element's contents synchronously, they
// may not be defined yet because of transclusion. There is a higher chance
// that it will be accessible if we wait one frame.
$$rAF(function() {
if (!mdNavItem.name) {
mdNavItem.name = angular.element(element[0].querySelector('._md-nav-button-text'))
.text().trim();
}
var navButton = angular.element(element[0].querySelector('._md-nav-button'));
navButton.on('click', function() {
mdNavBar.mdSelectedNavItem = mdNavItem.name;
scope.$apply();
});
});
}
};
}
/**
* Controller for the nav-item component.
* @param {!angular.JQLite} $element
* @constructor
* @final
* ngInject
*/
function MdNavItemController($element) {
/** @private @const {!angular.JQLite} */
this._$element = $element;
// Data-bound variables
/** @const {?Function} */
this.mdNavClick;
/** @const {?string} */
this.mdNavHref;
/** @const {?string} */
this.name;
// State variables
/** @private {boolean} */
this._selected = false;
/** @private {boolean} */
this._focused = false;
var hasNavClick = !!($element.attr('md-nav-click'));
var hasNavHref = !!($element.attr('md-nav-href'));
var hasNavSref = !!($element.attr('md-nav-sref'));
// Cannot specify more than one nav attribute
if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
throw Error(
'Must specify exactly one of md-nav-click, md-nav-href, ' +
'md-nav-sref for nav-item directive');
}
}
/**
* Returns a map of class names and values for use by ng-class.
* @return {!Object<string,boolean>}
*/
MdNavItemController.prototype.getNgClassMap = function() {
return {
'md-active': this._selected,
'md-primary': this._selected,
'md-unselected': !this._selected,
'md-focused': this._focused,
};
};
/**
* Get the name attribute of the tab.
* @return {string}
*/
MdNavItemController.prototype.getName = function() {
return this.name;
};
/**
* Get the button element associated with the tab.
* @return {!Element}
*/
MdNavItemController.prototype.getButtonEl = function() {
return this._$element[0].querySelector('._md-nav-button');
};
/**
* Set the selected state of the tab.
* @param {boolean} isSelected
*/
MdNavItemController.prototype.setSelected = function(isSelected) {
this._selected = isSelected;
};
/**
* @return {boolean}
*/
MdNavItemController.prototype.isSelected = function() {
return this._selected;
};
/**
* Set the focused state of the tab.
* @param {boolean} isFocused
*/
MdNavItemController.prototype.setFocused = function(isFocused) {
this._focused = isFocused;
};
/**
* @return {boolean}
*/
MdNavItemController.prototype.hasFocus = function() {
return this._focused;
};
ngmaterial.components.navBar = angular.module("material.components.navBar");