function parseStateRef(ref, current) { var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; if (preparsed) ref = current + '(' + preparsed[1] + ')'; parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); return { state: parsed[1], paramExpr: parsed[3] || null }; } function stateContext(el) { var stateData = el.parent().inheritedData('$uiView'); if (stateData && stateData.state && stateData.state.name) { return stateData.state; } } function getTypeInfo(el) { // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. var isSvg = Object.prototype.toString.call(el.prop('href')) === '[object SVGAnimatedString]'; var isForm = el[0].nodeName === "FORM"; return { attr: isForm ? "action" : (isSvg ? 'xlink:href' : 'href'), isAnchor: el.prop("tagName").toUpperCase() === "A", clickable: !isForm }; } function clickHook(el, $state, $timeout, type, current) { return function(e) { var button = e.which || e.button, target = current(); if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || el.attr('target'))) { // HACK: This is to allow ng-clicks to be processed before the transition is initiated: var transition = $timeout(function() { $state.go(target.state, target.params, target.options); }); e.preventDefault(); // if the state has no URL, ignore one preventDefault from the directive. var ignorePreventDefaultCount = type.isAnchor && !target.href ? 1: 0; e.preventDefault = function() { if (ignorePreventDefaultCount-- <= 0) $timeout.cancel(transition); }; } }; } function defaultOpts(el, $state) { return { relative: stateContext(el) || $state.$current, inherit: true }; } /** * @ngdoc directive * @name ui.router.state.directive:ui-sref * * @requires ui.router.state.$state * @requires $timeout * * @restrict A * * @description * A directive that binds a link (`` tag) to a state. If the state has an associated * URL, the directive will automatically generate & update the `href` attribute via * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking * the link will trigger a state transition with optional parameters. * * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be * handled natively by the browser. * * You can also use relative state paths within ui-sref, just like the relative * paths passed to `$state.go()`. You just need to be aware that the path is relative * to the state that the link lives in, in other words the state that loaded the * template containing the link. * * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, * and `reload`. * * @example * Here's an example of how you'd use ui-sref and how it would compile. If you have the * following template: *
 * Home | About | Next page
 *
 * 
 * 
* * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): *
 * Home | About | Next page
 *
 * 
 *
 * Home
 * 
* * @param {string} ui-sref 'stateName' can be any valid absolute or relative state * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} */ $StateRefDirective.$inject = ['$state', '$timeout']; function $StateRefDirective($state, $timeout) { return { restrict: 'A', require: ['?^uiSrefActive', '?^uiSrefActiveEq'], link: function(scope, element, attrs, uiSrefActive) { var ref = parseStateRef(attrs.uiSref, $state.current.name); var def = { state: ref.state, href: null, params: null }; var type = getTypeInfo(element); var active = uiSrefActive[1] || uiSrefActive[0]; def.options = extend(defaultOpts(element, $state), attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {}); var update = function(val) { if (val) def.params = angular.copy(val); def.href = $state.href(ref.state, def.params, def.options); if (active) active.$$addStateInfo(ref.state, def.params); if (def.href !== null) attrs.$set(type.attr, def.href); }; if (ref.paramExpr) { scope.$watch(ref.paramExpr, function(val) { if (val !== def.params) update(val); }, true); def.params = angular.copy(scope.$eval(ref.paramExpr)); } update(); if (!type.clickable) return; element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; })); } }; } /** * @ngdoc directive * @name ui.router.state.directive:ui-state * * @requires ui.router.state.uiSref * * @restrict A * * @description * Much like ui-sref, but will accept named $scope properties to evaluate for a state definition, * params and override options. * * @param {string} ui-state 'stateName' can be any valid absolute or relative state * @param {Object} ui-state-params params to pass to {@link ui.router.state.$state#href $state.href()} * @param {Object} ui-state-opts options to pass to {@link ui.router.state.$state#go $state.go()} */ $StateRefDynamicDirective.$inject = ['$state', '$timeout']; function $StateRefDynamicDirective($state, $timeout) { return { restrict: 'A', require: ['?^uiSrefActive', '?^uiSrefActiveEq'], link: function(scope, element, attrs, uiSrefActive) { var type = getTypeInfo(element); var active = uiSrefActive[1] || uiSrefActive[0]; var group = [attrs.uiState, attrs.uiStateParams || null, attrs.uiStateOpts || null]; var watch = '[' + group.map(function(val) { return val || 'null'; }).join(', ') + ']'; var def = { state: null, params: null, options: null, href: null }; function runStateRefLink (group) { def.state = group[0]; def.params = group[1]; def.options = group[2]; def.href = $state.href(def.state, def.params, def.options); if (active) active.$$addStateInfo(def.state, def.params); if (def.href) attrs.$set(type.attr, def.href); } scope.$watch(watch, runStateRefLink, true); runStateRefLink(scope.$eval(watch)); if (!type.clickable) return; element.bind("click", clickHook(element, $state, $timeout, type, function() { return def; })); } }; } /** * @ngdoc directive * @name ui.router.state.directive:ui-sref-active * * @requires ui.router.state.$state * @requires ui.router.state.$stateParams * @requires $interpolate * * @restrict A * * @description * A directive working alongside ui-sref to add classes to an element when the * related ui-sref directive's state is active, and removing them when it is inactive. * The primary use-case is to simplify the special appearance of navigation menus * relying on `ui-sref`, by having the "active" state's menu button appear different, * distinguishing it from the inactive menu items. * * ui-sref-active can live on the same element as ui-sref or on a parent element. The first * ui-sref-active found at the same level or above the ui-sref will be used. * * Will activate when the ui-sref's target state or any child state is active. If you * need to activate only when the ui-sref target state is active and *not* any of * it's children, then you will use * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} * * @example * Given the following template: *
 * 
 * 
* * * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", * the resulting HTML will appear as (note the 'active' class): *
 * 
 * 
* * The class name is interpolated **once** during the directives link time (any further changes to the * interpolated value are ignored). * * Multiple classes may be specified in a space-separated format: *
 * 
 * 
* * It is also possible to pass ui-sref-active an expression that evaluates * to an object hash, whose keys represent active class names and whose * values represent the respective state names/globs. * ui-sref-active will match if the current active state **includes** any of * the specified state names/globs, even the abstract ones. * * @Example * Given the following template, with "admin" being an abstract state: *
 * 
* Roles *
*
* * When the current state is "admin.roles" the "active" class will be applied * to both the
and elements. It is important to note that the state * names/globs passed to ui-sref-active shadow the state provided by ui-sref. */ /** * @ngdoc directive * @name ui.router.state.directive:ui-sref-active-eq * * @requires ui.router.state.$state * @requires ui.router.state.$stateParams * @requires $interpolate * * @restrict A * * @description * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will only activate * when the exact target state used in the `ui-sref` is active; no child states. * */ $StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; function $StateRefActiveDirective($state, $stateParams, $interpolate) { return { restrict: "A", controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) { var states = [], activeClasses = {}, activeEqClass, uiSrefActive; // There probably isn't much point in $observing this // uiSrefActive and uiSrefActiveEq share the same directive object with some // slight difference in logic routing activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope); try { uiSrefActive = $scope.$eval($attrs.uiSrefActive); } catch (e) { // Do nothing. uiSrefActive is not a valid expression. // Fall back to using $interpolate below } uiSrefActive = uiSrefActive || $interpolate($attrs.uiSrefActive || '', false)($scope); if (isObject(uiSrefActive)) { forEach(uiSrefActive, function(stateOrName, activeClass) { if (isString(stateOrName)) { var ref = parseStateRef(stateOrName, $state.current.name); addState(ref.state, $scope.$eval(ref.paramExpr), activeClass); } }); } // Allow uiSref to communicate with uiSrefActive[Equals] this.$$addStateInfo = function (newState, newParams) { // we already got an explicit state provided by ui-sref-active, so we // shadow the one that comes from ui-sref if (isObject(uiSrefActive) && states.length > 0) { return; } addState(newState, newParams, uiSrefActive); update(); }; $scope.$on('$stateChangeSuccess', update); function addState(stateName, stateParams, activeClass) { var state = $state.get(stateName, stateContext($element)); var stateHash = createStateHash(stateName, stateParams); states.push({ state: state || { name: stateName }, params: stateParams, hash: stateHash }); activeClasses[stateHash] = activeClass; } /** * @param {string} state * @param {Object|string} [params] * @return {string} */ function createStateHash(state, params) { if (!isString(state)) { throw new Error('state should be a string'); } if (isObject(params)) { return state + toJson(params); } params = $scope.$eval(params); if (isObject(params)) { return state + toJson(params); } return state; } // Update route state function update() { for (var i = 0; i < states.length; i++) { if (anyMatch(states[i].state, states[i].params)) { addClass($element, activeClasses[states[i].hash]); } else { removeClass($element, activeClasses[states[i].hash]); } if (exactMatch(states[i].state, states[i].params)) { addClass($element, activeEqClass); } else { removeClass($element, activeEqClass); } } } function addClass(el, className) { $timeout(function () { el.addClass(className); }); } function removeClass(el, className) { el.removeClass(className); } function anyMatch(state, params) { return $state.includes(state.name, params); } function exactMatch(state, params) { return $state.is(state.name, params); } update(); }] }; } angular.module('ui.router.state') .directive('uiSref', $StateRefDirective) .directive('uiSrefActive', $StateRefActiveDirective) .directive('uiSrefActiveEq', $StateRefActiveDirective) .directive('uiState', $StateRefDynamicDirective);