1 |
|
2 |
|
3 | import UrlPattern from 'url-pattern';
|
4 |
|
5 | var window: any = global.window;
|
6 | var history: History = global.history;
|
7 |
|
8 | var hasHistoryApi: boolean = (
|
9 | window !== undefined &&
|
10 | history !== undefined &&
|
11 | typeof history.pushState === 'function'
|
12 | );
|
13 |
|
14 | var locationChangeCallbacks: Array<NavigationCallback> = [];
|
15 |
|
16 | var routes: Array<Route> = [];
|
17 |
|
18 | export 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 |
|
36 | export 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 |
|
47 | function 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 |
|
59 | export 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 |
|
81 | export 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 |
|
114 | export function navigatorFor(path: string, replace?: bool): NavigationCallback {
|
115 | return e => navigate(path, e, replace);
|
116 | }
|
117 |
|
118 | export 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 |
|
127 | export function navigateToRoute(routeName: string, params?: Object, e?: Event): void {
|
128 | navigate(pathFor(routeName, params), e);
|
129 | }
|
130 |
|
131 | export function navigatorForRoute(routeName: string, params?: Object): NavigationCallback {
|
132 | return e => navigateToRoute(routeName, params, e);
|
133 | }
|
134 |
|
135 | export 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 |
|
144 | function onLocationChange(location: ?RouterLocation): void {
|
145 | locationChangeCallbacks.forEach(cb => cb(location));
|
146 | }
|
147 |
|
148 | function 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;
|
153 | if (a.hostname === window.location.hostname) {
|
154 | path = a.pathname + a.search + a.hash;
|
155 | if (path[0] !== '/') {
|
156 | path = '/' + path;
|
157 | }
|
158 | } else {
|
159 | path = a.href;
|
160 | }
|
161 | }
|
162 | return path;
|
163 | }
|
164 |
|
165 | function getWindowPathAndQuery(): ?string {
|
166 | var {location} = window;
|
167 | if (!location) return null;
|
168 | return location.pathname + location.search + location.hash;
|
169 | }
|
170 |
|
171 | export 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 |
|
179 | function 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 |
|
194 | function notExternal(m: RouterMatch): boolean {
|
195 | var {external} = m.route;
|
196 | if (typeof external === 'function') {
|
197 | return !external(m);
|
198 | } else return !external;
|
199 | }
|
200 |
|
201 | if (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 |
|
212 | function typeError(type: any, msg: string): TypeError {
|
213 | return new TypeError(msg + ' but got type `' + typeof type + '`!');
|
214 | }
|