UNPKG

6.25 kBJavaScriptView Raw
1/* @flow */
2
3import UrlPattern from 'url-pattern';
4
5var window: any = global.window;
6var history: History = global.history;
7
8var hasHistoryApi: boolean = (
9 window !== undefined &&
10 history !== undefined &&
11 typeof history.pushState === 'function'
12);
13
14var locationChangeCallbacks: Array<NavigationCallback> = [];
15
16var routes: Array<Route> = [];
17
18export function addRoutes(newRoutes: ?Array<RouteSpec>): void {
19 if (!(newRoutes instanceof Array)) throw typeError(routes, 'lucid-router expects to be passed a routing array as its first parameter');
20 for (var route of newRoutes) {
21 if (route === null || !(route instanceof Object)) throw typeError(routes, 'lucid-router expects each route definition to be an object');
22 route.path = route.path || null;
23 route.name = route.name || null;
24 route.external = typeof route.external === 'function'
25 ? route.external
26 : !!route.external;
27 try {
28 route.pattern = new UrlPattern(route.path);
29 } catch (err) {
30 throw typeError(route.path, 'lucid-router expects route paths to be a string or regex expression');
31 }
32 routes.push(route);
33 }
34}
35
36export function removeRoute(name: string): void {
37 var idx = -1;
38 for (var i = 0, l = routes.length; i < l; i++) {
39 if (routes[i].name === name) {
40 idx = i;
41 break;
42 }
43 }
44 ~idx && routes.splice(idx, 1);
45}
46
47function parseQuery(query) {
48 var queryArgs = {};
49 if (query) {
50 query.split('&')
51 .filter(keyValStr => !!keyValStr)
52 .map(keyValStr => keyValStr.split('=')
53 .map(encoded => decodeURIComponent(encoded)))
54 .forEach(([key,val]) => key && (queryArgs[key] = val));
55 }
56 return queryArgs;
57}
58
59export function match(path: string): ?RouterMatch {
60 var [pathnameAndQuery,hashAndHashQuery] = path.split('#');
61 var [pathname,search] = pathnameAndQuery.split('?');
62 var [hash,hashSearch] = hashAndHashQuery
63 ? hashAndHashQuery.split('?')
64 : [];
65 var queryState = parseQuery([search,hashSearch].join('&'));
66 for (var route of routes) {
67 var matchState = route.pattern.match(pathname);
68 if (!matchState) continue;
69 return {
70 route,
71 pathname,
72 search: search ? '?'.concat(search) : '',
73 hash: hash ? '#'.concat(hash) : '',
74 hashSearch: hashSearch ? '?'.concat(hashSearch) : '',
75 state: {...queryState, ...matchState}
76 };
77 }
78 return null;
79}
80
81export function navigate(path: ?string, e?: Event, replace?: boolean): void {
82 path = getFullPath(path || '');
83 if (hasHistoryApi) {
84 if (typeof path !== 'string' || !path) throw typeError(path, 'lucid-router.navigate expected a non empty string as its first parameter');
85 var m = match(path);
86 if (m && notExternal(m)) {
87 var location: ?RouterLocation = matchAndPathToLocation(m, path);
88 if (replace) {
89 history.replaceState(null, '', path);
90 } else {
91 history.pushState(null, '', path);
92 }
93
94 if (e && e.preventDefault) {
95 e.preventDefault();
96 }
97
98 onLocationChange(location);
99 return;
100 }
101 }
102
103 if (window) {
104 if (!e) window.location = path;
105 else {
106 const target = ((e.target : any) : ?Element);
107 if (!target || target.tagName !== 'A') {
108 window.location = path;
109 }
110 }
111 }
112}
113
114export function navigatorFor(path: string, replace?: bool): NavigationCallback {
115 return e => navigate(path, e, replace);
116}
117
118export function pathFor(routeName: string, params?: Object): string {
119 for (var route of routes) {
120 if (route.name === routeName) {
121 return route.pattern.stringify(params);
122 }
123 }
124 throw new Error(`lucid-router.pathFor failed to find a route with the name '${routeName}'`);
125}
126
127export function navigateToRoute(routeName: string, params?: Object, e?: Event): void {
128 navigate(pathFor(routeName, params), e);
129}
130
131export function navigatorForRoute(routeName: string, params?: Object): NavigationCallback {
132 return e => navigateToRoute(routeName, params, e);
133}
134
135export function register(callback: RouteMatchCallback): UnregisterLocationChangeCallback {
136 if (typeof callback !== 'function') throw typeError(callback, 'lucid-router.register expects to be passed a callback function');
137 locationChangeCallbacks.push(callback);
138 return function unregister() {
139 var idx = locationChangeCallbacks.indexOf(callback);
140 ~idx && locationChangeCallbacks.splice(idx, 1);
141 };
142}
143
144function onLocationChange(location: ?RouterLocation): void {
145 locationChangeCallbacks.forEach(cb => cb(location));
146}
147
148function getFullPath(path: string): string {
149 if (window) {
150 var a: HTMLAnchorElement = window.document.createElement('a');
151 a.href = path;
152 if (!a.host) a.href = a.href; /* IE hack */
153 if (a.hostname === window.location.hostname) {
154 path = a.pathname + a.search + a.hash;
155 if (path[0] !== '/') { /* more IE hacks */
156 path = '/' + path;
157 }
158 } else {
159 path = a.href;
160 }
161 }
162 return path;
163}
164
165function getWindowPathAndQuery(): ?string {
166 var {location} = window;
167 if (!location) return null;
168 return location.pathname + location.search + location.hash;
169}
170
171export function getLocation(path: ?string): ?RouterLocation {
172 path = path || getWindowPathAndQuery() || '';
173 var m: ?RouterMatch = match(path);
174 var location = matchAndPathToLocation(m, path);
175 onLocationChange(location);
176 return location;
177}
178
179function matchAndPathToLocation(m: ?RouterMatch, p: string): ?RouterLocation {
180 return !m
181 ? null
182 : {
183 path: p,
184 name: m.route.name,
185 pathname: m.pathname,
186 search: m.search,
187 hash: m.hash,
188 hashSearch: m.hashSearch,
189 state: m.state,
190 route: m.route
191 };
192}
193
194function notExternal(m: RouterMatch): boolean {
195 var {external} = m.route;
196 if (typeof external === 'function') {
197 return !external(m);
198 } else return !external;
199}
200
201if (hasHistoryApi && window) {
202 window.addEventListener('popstate', function(e: Event) {
203 var path = getWindowPathAndQuery() || '';
204 var m: ?RouterMatch = match(path);
205 if (m && notExternal(m)) {
206 var location = matchAndPathToLocation(m, path);
207 onLocationChange(location);
208 }
209 }, false);
210}
211
212function typeError(type: any, msg: string): TypeError {
213 return new TypeError(msg + ' but got type `' + typeof type + '`!');
214}