2016-06-08 14:13:41 -04:00
|
|
|
import {ActivatedRoute} from './router_state';
|
|
|
|
import {PRIMARY_OUTLET, Params} from './shared';
|
|
|
|
import {UrlSegment, UrlTree} from './url_tree';
|
|
|
|
import {forEach, shallowEqual} from './utils/collection';
|
|
|
|
import {TreeNode} from './utils/tree';
|
|
|
|
|
|
|
|
export function createUrlTree(
|
|
|
|
route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params | undefined,
|
|
|
|
fragment: string | undefined): UrlTree {
|
2016-05-26 19:51:19 -04:00
|
|
|
if (commands.length === 0) {
|
2016-06-06 19:34:48 -04:00
|
|
|
return tree(urlTree._root, urlTree, queryParams, fragment);
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const normalizedCommands = normalizeCommands(commands);
|
|
|
|
if (navigateToRoot(normalizedCommands)) {
|
2016-06-06 19:34:48 -04:00
|
|
|
return tree(new TreeNode<UrlSegment>(urlTree.root, []), urlTree, queryParams, fragment);
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const startingNode = findStartingNode(normalizedCommands, urlTree, route);
|
|
|
|
const updated = normalizedCommands.commands.length > 0 ?
|
|
|
|
updateMany(startingNode.children.slice(0), normalizedCommands.commands) :
|
|
|
|
[];
|
2016-06-01 16:35:01 -04:00
|
|
|
const newRoot = constructNewTree(urlTree._root, startingNode, updated);
|
2016-05-26 19:51:19 -04:00
|
|
|
|
2016-06-06 19:34:48 -04:00
|
|
|
return tree(newRoot, urlTree, queryParams, fragment);
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
function tree(
|
|
|
|
root: TreeNode<UrlSegment>, urlTree: UrlTree, queryParams: Params | undefined,
|
|
|
|
fragment: string | undefined): UrlTree {
|
2016-06-06 19:34:48 -04:00
|
|
|
const q = queryParams ? stringify(queryParams) : urlTree.queryParams;
|
2016-05-26 19:51:19 -04:00
|
|
|
const f = fragment ? fragment : urlTree.fragment;
|
|
|
|
return new UrlTree(root, q, f);
|
|
|
|
}
|
|
|
|
|
|
|
|
function navigateToRoot(normalizedChange: NormalizedNavigationCommands): boolean {
|
|
|
|
return normalizedChange.isAbsolute && normalizedChange.commands.length === 1 &&
|
2016-06-08 14:13:41 -04:00
|
|
|
normalizedChange.commands[0] == '/';
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
class NormalizedNavigationCommands {
|
2016-06-08 14:13:41 -04:00
|
|
|
constructor(
|
|
|
|
public isAbsolute: boolean, public numberOfDoubleDots: number, public commands: any[]) {}
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function normalizeCommands(commands: any[]): NormalizedNavigationCommands {
|
2016-06-08 14:13:41 -04:00
|
|
|
if ((typeof commands[0] === 'string') && commands.length === 1 && commands[0] == '/') {
|
2016-05-26 19:51:19 -04:00
|
|
|
return new NormalizedNavigationCommands(true, 0, commands);
|
|
|
|
}
|
|
|
|
|
|
|
|
let numberOfDoubleDots = 0;
|
|
|
|
let isAbsolute = false;
|
|
|
|
const res = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < commands.length; ++i) {
|
|
|
|
const c = commands[i];
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
if (!(typeof c === 'string')) {
|
2016-05-26 19:51:19 -04:00
|
|
|
res.push(c);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parts = c.split('/');
|
|
|
|
for (let j = 0; j < parts.length; ++j) {
|
|
|
|
let cc = parts[j];
|
|
|
|
|
|
|
|
// first exp is treated in a special way
|
|
|
|
if (i == 0) {
|
2016-06-08 14:13:41 -04:00
|
|
|
if (j == 0 && cc == '.') { // './a'
|
2016-05-26 19:51:19 -04:00
|
|
|
// skip it
|
2016-06-08 14:13:41 -04:00
|
|
|
} else if (j == 0 && cc == '') { // '/a'
|
2016-05-26 19:51:19 -04:00
|
|
|
isAbsolute = true;
|
2016-06-08 14:13:41 -04:00
|
|
|
} else if (cc == '..') { // '../a'
|
2016-05-26 19:51:19 -04:00
|
|
|
numberOfDoubleDots++;
|
|
|
|
} else if (cc != '') {
|
|
|
|
res.push(cc);
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
if (cc != '') {
|
|
|
|
res.push(cc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new NormalizedNavigationCommands(isAbsolute, numberOfDoubleDots, res);
|
|
|
|
}
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
function findStartingNode(
|
|
|
|
normalizedChange: NormalizedNavigationCommands, urlTree: UrlTree,
|
|
|
|
route: ActivatedRoute): TreeNode<UrlSegment> {
|
2016-05-26 19:51:19 -04:00
|
|
|
if (normalizedChange.isAbsolute) {
|
2016-06-01 16:35:01 -04:00
|
|
|
return urlTree._root;
|
2016-05-26 19:51:19 -04:00
|
|
|
} else {
|
2016-06-08 14:13:41 -04:00
|
|
|
const urlSegment = findUrlSegment(route, urlTree, normalizedChange.numberOfDoubleDots);
|
2016-06-01 16:35:01 -04:00
|
|
|
return findMatchingNode(urlSegment, urlTree._root);
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
function findUrlSegment(
|
|
|
|
route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment {
|
2016-06-02 14:30:38 -04:00
|
|
|
const urlSegment = route.snapshot._lastUrlSegment;
|
2016-05-26 19:51:19 -04:00
|
|
|
const path = urlTree.pathFromRoot(urlSegment);
|
|
|
|
if (path.length <= numberOfDoubleDots) {
|
2016-06-08 14:13:41 -04:00
|
|
|
throw new Error('Invalid number of \'../\'');
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
return path[path.length - 1 - numberOfDoubleDots];
|
|
|
|
}
|
|
|
|
|
|
|
|
function findMatchingNode(segment: UrlSegment, node: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
|
|
|
|
if (node.value === segment) return node;
|
|
|
|
for (let c of node.children) {
|
|
|
|
const r = findMatchingNode(segment, c);
|
|
|
|
if (r) return r;
|
|
|
|
}
|
|
|
|
throw new Error(`Cannot find url segment '${segment}'`);
|
|
|
|
}
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
function constructNewTree(
|
|
|
|
node: TreeNode<UrlSegment>, original: TreeNode<UrlSegment>,
|
|
|
|
updated: TreeNode<UrlSegment>[]): TreeNode<UrlSegment> {
|
2016-05-26 19:51:19 -04:00
|
|
|
if (node === original) {
|
|
|
|
return new TreeNode<UrlSegment>(node.value, updated);
|
|
|
|
} else {
|
|
|
|
return new TreeNode<UrlSegment>(
|
2016-06-08 14:13:41 -04:00
|
|
|
node.value, node.children.map(c => constructNewTree(c, original, updated)));
|
2016-05-26 19:51:19 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateMany(nodes: TreeNode<UrlSegment>[], commands: any[]): TreeNode<UrlSegment>[] {
|
|
|
|
const outlet = getOutlet(commands);
|
|
|
|
const nodesInRightOutlet = nodes.filter(c => c.value.outlet === outlet);
|
|
|
|
if (nodesInRightOutlet.length > 0) {
|
|
|
|
const nodeRightOutlet = nodesInRightOutlet[0]; // there can be only one
|
|
|
|
nodes[nodes.indexOf(nodeRightOutlet)] = update(nodeRightOutlet, commands);
|
|
|
|
} else {
|
|
|
|
nodes.push(update(null, commands));
|
|
|
|
}
|
|
|
|
return nodes;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPath(commands: any[]): any {
|
2016-06-08 14:13:41 -04:00
|
|
|
if (!(typeof commands[0] === 'string')) return commands[0];
|
|
|
|
const parts = commands[0].toString().split(':');
|
2016-05-26 19:51:19 -04:00
|
|
|
return parts.length > 1 ? parts[1] : commands[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
function getOutlet(commands: any[]): string {
|
2016-06-08 14:13:41 -04:00
|
|
|
if (!(typeof commands[0] === 'string')) return PRIMARY_OUTLET;
|
|
|
|
const parts = commands[0].toString().split(':');
|
2016-05-26 19:51:19 -04:00
|
|
|
return parts.length > 1 ? parts[0] : PRIMARY_OUTLET;
|
|
|
|
}
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
function update(node: TreeNode<UrlSegment>| null, commands: any[]): TreeNode<UrlSegment> {
|
2016-05-26 19:51:19 -04:00
|
|
|
const rest = commands.slice(1);
|
|
|
|
const next = rest.length === 0 ? null : rest[0];
|
|
|
|
const outlet = getOutlet(commands);
|
|
|
|
const path = getPath(commands);
|
|
|
|
|
|
|
|
// reach the end of the tree => create new tree nodes.
|
|
|
|
if (!node && !(typeof next === 'object')) {
|
|
|
|
const urlSegment = new UrlSegment(path, {}, outlet);
|
|
|
|
const children = rest.length === 0 ? [] : [update(null, rest)];
|
|
|
|
return new TreeNode<UrlSegment>(urlSegment, children);
|
|
|
|
|
|
|
|
} else if (!node && typeof next === 'object') {
|
|
|
|
const urlSegment = new UrlSegment(path, stringify(next), outlet);
|
|
|
|
return recurse(urlSegment, node, rest.slice(1));
|
|
|
|
|
|
|
|
// different outlet => preserve the subtree
|
|
|
|
} else if (node && outlet !== node.value.outlet) {
|
|
|
|
return node;
|
|
|
|
|
|
|
|
// params command
|
|
|
|
} else if (node && typeof path === 'object') {
|
|
|
|
const newSegment = new UrlSegment(node.value.path, stringify(path), node.value.outlet);
|
|
|
|
return recurse(newSegment, node, rest);
|
|
|
|
|
|
|
|
// next one is a params command && can reuse the node
|
|
|
|
} else if (node && typeof next === 'object' && compare(path, stringify(next), node.value)) {
|
|
|
|
return recurse(node.value, node, rest.slice(1));
|
|
|
|
|
|
|
|
// next one is a params command && cannot reuse the node
|
|
|
|
} else if (node && typeof next === 'object') {
|
|
|
|
const urlSegment = new UrlSegment(path, stringify(next), outlet);
|
|
|
|
return recurse(urlSegment, node, rest.slice(1));
|
|
|
|
|
|
|
|
// next one is not a params command && can reuse the node
|
|
|
|
} else if (node && compare(path, {}, node.value)) {
|
|
|
|
return recurse(node.value, node, rest);
|
|
|
|
|
|
|
|
// next one is not a params command && cannot reuse the node
|
|
|
|
} else {
|
|
|
|
const urlSegment = new UrlSegment(path, {}, outlet);
|
|
|
|
return recurse(urlSegment, node, rest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
|
|
|
const res = {};
|
|
|
|
forEach(params, (v, k) => res[k] = v.toString());
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
function compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean {
|
|
|
|
return path == segment.path && shallowEqual(params, segment.parameters);
|
|
|
|
}
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
function recurse(
|
|
|
|
urlSegment: UrlSegment, node: TreeNode<UrlSegment>| null, rest: any[]): TreeNode<UrlSegment> {
|
2016-05-26 19:51:19 -04:00
|
|
|
if (rest.length === 0) {
|
|
|
|
return new TreeNode<UrlSegment>(urlSegment, []);
|
|
|
|
}
|
|
|
|
const children = node ? node.children.slice(0) : [];
|
|
|
|
return new TreeNode<UrlSegment>(urlSegment, updateMany(children, rest));
|
|
|
|
}
|