350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
/** @license Copyright 2014-2015 Google, Inc. http://github.com/angular/angular/LICENSE */
|
||
(function () {
|
||
|
||
'use strict';
|
||
|
||
// keep a reference to compileProvider so we can register new component-directives
|
||
// on-the-fly based on $routeProvider configuration
|
||
// TODO: remove this– right now you can only bootstrap one Angular app with this hack
|
||
var $compileProvider, $q, $injector;
|
||
|
||
/**
|
||
* This module loads services that mimic ngRoute's configuration, and includes
|
||
* an anchor link directive that intercepts clicks to routing.
|
||
*
|
||
* This module is intended to be used as a stop-gap solution for projects upgrading from ngRoute.
|
||
* It intentionally does not implement all features of ngRoute.
|
||
*/
|
||
angular.module('ngRouteShim', [])
|
||
.provider('$route', $RouteProvider)
|
||
.config(['$compileProvider', function (compileProvider) {
|
||
$compileProvider = compileProvider;
|
||
}])
|
||
.factory('$routeParams', $routeParamsFactory)
|
||
.directive('a', anchorLinkDirective)
|
||
|
||
// Connects the legacy $routeProvider config shim to Component Router's config.
|
||
.run(['$route', '$router', function ($route, $router) {
|
||
$route.$$subscribe(function (routeDefinition) {
|
||
if (!angular.isArray(routeDefinition)) {
|
||
routeDefinition = [routeDefinition];
|
||
}
|
||
$router.config(routeDefinition);
|
||
});
|
||
}]);
|
||
|
||
|
||
/**
|
||
* A shimmed out provider that provides the same API as ngRoute's $routeProvider, but uses these calls
|
||
* to configure Component Router.
|
||
*/
|
||
function $RouteProvider() {
|
||
|
||
var routes = [];
|
||
var subscriptionFn = null;
|
||
|
||
var routeMap = {};
|
||
|
||
// Stats for which routes are skipped
|
||
var skipCount = 0;
|
||
var successCount = 0;
|
||
var allCount = 0;
|
||
|
||
function consoleMetrics() {
|
||
return '(' + skipCount + ' skipped / ' + successCount + ' success / ' + allCount + ' total)';
|
||
}
|
||
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $routeProvider#when
|
||
*
|
||
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
|
||
* contains redundant trailing slash or is missing one, the route will still match and the
|
||
* `$location.path` will be updated to add or drop the trailing slash to exactly match the
|
||
* route definition.
|
||
*
|
||
* @param {Object} route Mapping information to be assigned to `$route.current` on route
|
||
* match.
|
||
*/
|
||
this.when = function(path, route) {
|
||
//copy original route object to preserve params inherited from proto chain
|
||
var routeCopy = angular.copy(route);
|
||
|
||
allCount++;
|
||
|
||
if (angular.isDefined(routeCopy.reloadOnSearch)) {
|
||
console.warn('Route for "' + path + '" uses "reloadOnSearch" which is not implemented.');
|
||
}
|
||
if (angular.isDefined(routeCopy.caseInsensitiveMatch)) {
|
||
console.warn('Route for "' + path + '" uses "caseInsensitiveMatch" which is not implemented.');
|
||
}
|
||
|
||
// use new wildcard format
|
||
path = reformatWildcardParams(path);
|
||
|
||
if (path[path.length - 1] == '*') {
|
||
skipCount++;
|
||
console.warn('Route for "' + path + '" ignored because it ends with *. Skipping.', consoleMetrics());
|
||
return this;
|
||
}
|
||
|
||
if (path.indexOf('?') > -1) {
|
||
skipCount++;
|
||
console.warn('Route for "' + path + '" ignored because it has optional parameters. Skipping.', consoleMetrics());
|
||
return this;
|
||
}
|
||
|
||
if (typeof route.redirectTo == 'function') {
|
||
skipCount++;
|
||
console.warn('Route for "' + path + '" ignored because lazy redirecting to a function is not yet implemented. Skipping.', consoleMetrics());
|
||
return this;
|
||
}
|
||
|
||
|
||
var routeDefinition = {
|
||
path: path,
|
||
data: routeCopy
|
||
};
|
||
|
||
routeMap[path] = routeCopy;
|
||
|
||
if (route.redirectTo) {
|
||
routeDefinition.redirectTo = [routeMap[route.redirectTo].name];
|
||
} else {
|
||
if (routeCopy.controller && !routeCopy.controllerAs) {
|
||
console.warn('Route for "' + path + '" should use "controllerAs".');
|
||
}
|
||
|
||
var directiveName = routeObjToRouteName(routeCopy, path);
|
||
|
||
if (!directiveName) {
|
||
throw new Error('Could not determine a name for route "' + path + '".');
|
||
}
|
||
|
||
routeDefinition.component = directiveName;
|
||
routeDefinition.name = route.name || upperCase(directiveName);
|
||
|
||
var directiveController = routeCopy.controller;
|
||
|
||
var directiveDefinition = {
|
||
scope: false,
|
||
controller: directiveController,
|
||
controllerAs: routeCopy.controllerAs,
|
||
templateUrl: routeCopy.templateUrl,
|
||
template: routeCopy.template
|
||
};
|
||
|
||
var directiveFactory = function () {
|
||
return directiveDefinition;
|
||
};
|
||
|
||
// if we have route resolve options, prepare a wrapper controller
|
||
if (directiveController && routeCopy.resolve) {
|
||
var originalController = directiveController;
|
||
var resolvedLocals = {};
|
||
|
||
directiveDefinition.controller = ['$injector', '$scope', function ($injector, $scope) {
|
||
var locals = angular.extend({
|
||
$scope: $scope
|
||
}, resolvedLocals);
|
||
|
||
var ctrl = $injector.instantiate(originalController, locals);
|
||
|
||
if (routeCopy.controllerAs) {
|
||
locals.$scope[routeCopy.controllerAs] = ctrl;
|
||
}
|
||
|
||
return ctrl;
|
||
}];
|
||
|
||
// we take care of controllerAs in the directive controller wrapper
|
||
delete directiveDefinition.controllerAs;
|
||
|
||
// we resolve the locals in a canActivate block
|
||
directiveFactory.$canActivate = function() {
|
||
var locals = angular.extend({}, routeCopy.resolve);
|
||
|
||
angular.forEach(locals, function(value, key) {
|
||
locals[key] = angular.isString(value) ?
|
||
$injector.get(value) : $injector.invoke(value, null, null, key);
|
||
});
|
||
|
||
return $q.all(locals).then(function (locals) {
|
||
resolvedLocals = locals;
|
||
}).then(function () {
|
||
return true;
|
||
});
|
||
};
|
||
}
|
||
|
||
// register the dynamically created directive
|
||
$compileProvider.directive(directiveName, directiveFactory);
|
||
}
|
||
if (subscriptionFn) {
|
||
subscriptionFn(routeDefinition);
|
||
} else {
|
||
routes.push(routeDefinition);
|
||
}
|
||
successCount++;
|
||
|
||
return this;
|
||
};
|
||
|
||
this.otherwise = function(params) {
|
||
if (typeof params === 'string') {
|
||
params = {redirectTo: params};
|
||
}
|
||
this.when('/*rest', params);
|
||
return this;
|
||
};
|
||
|
||
|
||
this.$get = ['$q', '$injector', function (q, injector) {
|
||
$q = q;
|
||
$injector = injector;
|
||
|
||
var $route = {
|
||
routes: routeMap,
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $route#reload
|
||
*
|
||
* @description
|
||
* Causes `$route` service to reload the current route even if
|
||
* {@link ng.$location $location} hasn't changed.
|
||
*/
|
||
reload: function() {
|
||
throw new Error('Not implemented: $route.reload');
|
||
},
|
||
|
||
/**
|
||
* @ngdoc method
|
||
* @name $route#updateParams
|
||
*/
|
||
updateParams: function(newParams) {
|
||
throw new Error('Not implemented: $route.updateParams');
|
||
},
|
||
|
||
/**
|
||
* Runs the given `fn` whenever new configs are added.
|
||
* Only one subscription is allowed.
|
||
* Passed `fn` is called synchronously.
|
||
*/
|
||
$$subscribe: function(fn) {
|
||
if (subscriptionFn) {
|
||
throw new Error('only one subscription allowed');
|
||
}
|
||
subscriptionFn = fn;
|
||
subscriptionFn(routes);
|
||
routes = [];
|
||
},
|
||
|
||
/**
|
||
* Runs a string with stats about many route configs were adapted, and how many were
|
||
* dropped because they are incompatible.
|
||
*/
|
||
$$getStats: consoleMetrics
|
||
};
|
||
|
||
return $route;
|
||
}];
|
||
|
||
}
|
||
|
||
function $routeParamsFactory($router, $rootScope) {
|
||
// the identity of this object cannot change
|
||
var paramsObj = {};
|
||
|
||
$rootScope.$on('$routeChangeSuccess', function () {
|
||
var newParams = $router._currentInstruction && $router._currentInstruction.component.params;
|
||
|
||
angular.forEach(paramsObj, function (val, name) {
|
||
delete paramsObj[name];
|
||
});
|
||
angular.forEach(newParams, function (val, name) {
|
||
paramsObj[name] = val;
|
||
});
|
||
});
|
||
|
||
return paramsObj;
|
||
}
|
||
|
||
/**
|
||
* Allows normal anchor links to kick off routing.
|
||
*/
|
||
function anchorLinkDirective($router) {
|
||
return {
|
||
restrict: 'E',
|
||
link: function (scope, element) {
|
||
// If the linked element is not an anchor tag anymore, do nothing
|
||
if (element[0].nodeName.toLowerCase() !== 'a') {
|
||
return;
|
||
}
|
||
|
||
// SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
|
||
var hrefAttrName = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
|
||
'xlink:href' : 'href';
|
||
|
||
element.on('click', function (event) {
|
||
if (event.which !== 1) {
|
||
return;
|
||
}
|
||
|
||
var href = element.attr(hrefAttrName);
|
||
if (href && $router.recognize(href)) {
|
||
$router.navigateByUrl(href);
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Given a route object, attempts to find a unique directive name.
|
||
*
|
||
* @param route – route config object passed to $routeProvider.when
|
||
* @param path – route configuration path
|
||
* @returns {string|name} – a normalized (camelCase) directive name
|
||
*/
|
||
function routeObjToRouteName(route, path) {
|
||
var name = route.controllerAs;
|
||
|
||
var controller = route.controller;
|
||
if (!name && controller) {
|
||
if (angular.isArray(controller)) {
|
||
controller = controller[controller.length - 1];
|
||
}
|
||
name = controller.name;
|
||
}
|
||
|
||
if (!name) {
|
||
var segments = path.split('/');
|
||
name = segments[segments.length - 1];
|
||
}
|
||
|
||
if (name) {
|
||
name = name + 'AutoCmp';
|
||
}
|
||
|
||
return name;
|
||
}
|
||
|
||
function upperCase(str) {
|
||
return str.charAt(0).toUpperCase() + str.substr(1);
|
||
}
|
||
|
||
/*
|
||
* Changes "/:foo*" to "/*foo"
|
||
*/
|
||
var WILDCARD_PARAM_RE = new RegExp('\\/:([a-z0-9]+)\\*', 'gi');
|
||
function reformatWildcardParams(path) {
|
||
return path.replace(WILDCARD_PARAM_RE, function (m, m1) {
|
||
return '/*' + m1;
|
||
});
|
||
}
|
||
|
||
}());
|