UNPKG

16.9 kBJavaScriptView Raw
1import { pathname } from '@curi/interactions';
2import PathToRegexp from 'path-to-regexp';
3
4function isAsyncRoute(route) {
5 return typeof route.methods.resolve !== "undefined";
6}
7function isExternalRedirect(redirect) {
8 return "externalURL" in redirect;
9}
10function isRedirectLocation(redirect) {
11 return "url" in redirect;
12}
13
14function finishResponse(route, match, resolvedResults, router, external) {
15 var _a = resolvedResults || {}, _b = _a.resolved, resolved = _b === void 0 ? null : _b, _c = _a.error, error = _c === void 0 ? null : _c;
16 var response = {
17 data: undefined,
18 body: undefined,
19 meta: undefined
20 };
21 for (var key in match) {
22 response[key] = match[key];
23 }
24 if (!route.methods.respond) {
25 return response;
26 }
27 var results = route.methods.respond({
28 resolved: resolved,
29 error: error,
30 match: match,
31 external: external
32 });
33 if (!results) {
34 if (process.env.NODE_ENV !== "production") {
35 console.warn("\"" + match.name + "\"'s response function did not return anything. Did you forget to include a return statement?");
36 }
37 return response;
38 }
39 if (process.env.NODE_ENV !== "production") {
40 var validProperties_1 = {
41 meta: true,
42 body: true,
43 data: true,
44 redirect: true
45 };
46 Object.keys(results).forEach(function (property) {
47 if (!validProperties_1.hasOwnProperty(property)) {
48 console.warn("\"" + property + "\" is not a valid response property. The valid properties are:\n\n " + Object.keys(validProperties_1).join(", "));
49 }
50 });
51 }
52 response["meta"] = results["meta"];
53 response["body"] = results["body"];
54 response["data"] = results["data"];
55 if (results["redirect"]) {
56 response["redirect"] = createRedirect(results["redirect"], router);
57 }
58 return response;
59}
60function createRedirect(redirect, router) {
61 if (isExternalRedirect(redirect)) {
62 return redirect;
63 }
64 var name = redirect.name, params = redirect.params, query = redirect.query, hash = redirect.hash, state = redirect.state;
65 var url = isRedirectLocation(redirect)
66 ? redirect.url
67 : router.url({ name: name, params: params, query: query, hash: hash });
68 return {
69 name: name,
70 params: params,
71 query: query,
72 hash: hash,
73 state: state,
74 url: url
75 };
76}
77
78function createRouter(historyConstructor, routes, options) {
79 if (options === void 0) { options = {}; }
80 var latestResponse;
81 var latestNavigation;
82 var history = historyConstructor(function (pendingNav) {
83 var navigation = {
84 action: pendingNav.action,
85 previous: latestResponse
86 };
87 var matched = routes.match(pendingNav.location);
88 if (!matched) {
89 if (process.env.NODE_ENV !== "production") {
90 console.warn("The current location (" + pendingNav.location.pathname + ") has no matching route, " +
91 'so a response could not be emitted. A catch-all route ({ path: "(.*)" }) ' +
92 "can be used to match locations with no other matching route.");
93 }
94 pendingNav.finish();
95 finishAndResetNavCallbacks();
96 return;
97 }
98 var route = matched.route, match = matched.match;
99 if (!isAsyncRoute(route)) {
100 finalizeResponseAndEmit(route, match, pendingNav, navigation, null);
101 }
102 else {
103 announceAsyncNav();
104 route.methods
105 .resolve(match, options.external)
106 .then(function (resolved) { return ({ resolved: resolved, error: null }); }, function (error) { return ({ error: error, resolved: null }); })
107 .then(function (resolved) {
108 if (pendingNav.cancelled) {
109 return;
110 }
111 finalizeResponseAndEmit(route, match, pendingNav, navigation, resolved);
112 });
113 }
114 }, options.history || {});
115 function finalizeResponseAndEmit(route, match, pending, navigation, resolved) {
116 asyncNavComplete();
117 pending.finish();
118 var response = finishResponse(route, match, resolved, router, options.external);
119 finishAndResetNavCallbacks();
120 emitImmediate(response, navigation);
121 }
122 var _a = options.invisibleRedirects, invisibleRedirects = _a === void 0 ? false : _a;
123 function emitImmediate(response, navigation) {
124 if (!response.redirect ||
125 !invisibleRedirects ||
126 isExternalRedirect(response.redirect)) {
127 latestResponse = response;
128 latestNavigation = navigation;
129 var emit = { response: response, navigation: navigation, router: router };
130 callObservers(emit);
131 callOneTimersAndSideEffects(emit);
132 }
133 if (response.redirect !== undefined &&
134 !isExternalRedirect(response.redirect)) {
135 history.navigate(response.redirect, "replace");
136 }
137 }
138 function callObservers(emitted) {
139 observers.forEach(function (fn) {
140 fn(emitted);
141 });
142 }
143 function callOneTimersAndSideEffects(emitted) {
144 oneTimers.splice(0).forEach(function (fn) {
145 fn(emitted);
146 });
147 if (options.sideEffects) {
148 options.sideEffects.forEach(function (fn) {
149 fn(emitted);
150 });
151 }
152 }
153 /* router.observer & router.once */
154 var observers = [];
155 var oneTimers = [];
156 function observe(fn, options) {
157 var _a = (options || {}).initial, initial = _a === void 0 ? true : _a;
158 observers.push(fn);
159 if (latestResponse && initial) {
160 fn({
161 response: latestResponse,
162 navigation: latestNavigation,
163 router: router
164 });
165 }
166 return function () {
167 observers = observers.filter(function (obs) {
168 return obs !== fn;
169 });
170 };
171 }
172 function once(fn, options) {
173 var _a = (options || {}).initial, initial = _a === void 0 ? true : _a;
174 if (latestResponse && initial) {
175 fn({
176 response: latestResponse,
177 navigation: latestNavigation,
178 router: router
179 });
180 }
181 else {
182 oneTimers.push(fn);
183 }
184 }
185 /* router.url */
186 function url(details) {
187 var name = details.name, params = details.params, hash = details.hash, query = details.query;
188 var pathname$1;
189 if (name) {
190 var route = router.route(name);
191 if (route) {
192 pathname$1 = pathname(route, params);
193 }
194 }
195 return history.url({ pathname: pathname$1, hash: hash, query: query });
196 }
197 /* router.navigate */
198 var cancelCallback;
199 var finishCallback;
200 function navigate(details) {
201 cancelAndResetNavCallbacks();
202 var url = details.url, state = details.state, method = details.method;
203 history.navigate({ url: url, state: state }, method);
204 if (details.cancelled || details.finished) {
205 cancelCallback = details.cancelled;
206 finishCallback = details.finished;
207 return resetCallbacks;
208 }
209 }
210 function cancelAndResetNavCallbacks() {
211 if (cancelCallback) {
212 cancelCallback();
213 }
214 resetCallbacks();
215 }
216 function finishAndResetNavCallbacks() {
217 if (finishCallback) {
218 finishCallback();
219 }
220 resetCallbacks();
221 }
222 function resetCallbacks() {
223 cancelCallback = undefined;
224 finishCallback = undefined;
225 }
226 /* router.cancel */
227 var cancelWith;
228 var asyncNavNotifiers = [];
229 function cancel(fn) {
230 asyncNavNotifiers.push(fn);
231 return function () {
232 asyncNavNotifiers = asyncNavNotifiers.filter(function (can) {
233 return can !== fn;
234 });
235 };
236 }
237 // let any async navigation listeners (observers from router.cancel)
238 // know that there is an asynchronous navigation happening
239 function announceAsyncNav() {
240 if (asyncNavNotifiers.length && cancelWith === undefined) {
241 cancelWith = function () {
242 history.cancel();
243 asyncNavComplete();
244 cancelAndResetNavCallbacks();
245 };
246 asyncNavNotifiers.forEach(function (fn) {
247 fn(cancelWith);
248 });
249 }
250 }
251 function asyncNavComplete() {
252 if (cancelWith) {
253 cancelWith = undefined;
254 asyncNavNotifiers.forEach(function (fn) {
255 fn();
256 });
257 }
258 }
259 var router = {
260 route: routes.route,
261 history: history,
262 external: options.external,
263 observe: observe,
264 once: once,
265 cancel: cancel,
266 url: url,
267 navigate: navigate,
268 current: function () {
269 return {
270 response: latestResponse,
271 navigation: latestNavigation
272 };
273 },
274 destroy: function () {
275 history.destroy();
276 }
277 };
278 history.current();
279 return router;
280}
281
282var withLeadingSlash = function (path) {
283 return path.charAt(0) === "/" ? path : "/" + path;
284};
285var withTrailingSlash = function (path) {
286 return path.charAt(path.length - 1) === "/" ? path : path + "/";
287};
288var join = function (beginning, end) {
289 return withTrailingSlash(beginning) + end;
290};
291
292function createRoute(props, map, parent) {
293 if (parent === void 0) { parent = {
294 path: "",
295 keys: []
296 }; }
297 if (process.env.NODE_ENV !== "production") {
298 if (props.name in map) {
299 throw new Error("Multiple routes have the name \"" + props.name + "\". Route names must be unique.");
300 }
301 if (props.path.charAt(0) === "/") {
302 throw new Error("Route paths cannot start with a forward slash (/). (Received \"" + props.path + "\")");
303 }
304 }
305 var fullPath = withLeadingSlash(join(parent.path, props.path));
306 var _a = props.pathOptions || {}, _b = _a.match, matchOptions = _b === void 0 ? {} : _b, _c = _a.compile, compileOptions = _c === void 0 ? {} : _c;
307 // end must be false for routes with children, but we want to track its original value
308 var exact = matchOptions.end == null || matchOptions.end;
309 if (props.children && props.children.length) {
310 matchOptions.end = false;
311 }
312 var keys = [];
313 var re = PathToRegexp(withLeadingSlash(props.path), keys, matchOptions);
314 var keyNames = keys.map(function (key) { return key.name; });
315 if (parent.keys.length) {
316 keyNames = parent.keys.concat(keyNames);
317 }
318 var childRoutes = [];
319 var children = [];
320 if (props.children && props.children.length) {
321 childRoutes = props.children.map(function (child) {
322 return createRoute(child, map, {
323 path: fullPath,
324 keys: keyNames
325 });
326 });
327 children = childRoutes.map(function (child) { return child.public; });
328 }
329 var compiled = PathToRegexp.compile(fullPath);
330 var route = {
331 public: {
332 name: props.name,
333 keys: keyNames,
334 parent: undefined,
335 children: children,
336 methods: {
337 resolve: props.resolve,
338 respond: props.respond,
339 pathname: function (params) {
340 return compiled(params, compileOptions);
341 }
342 },
343 extra: props.extra
344 },
345 matching: {
346 re: re,
347 keys: keys,
348 exact: exact,
349 parsers: props.params || {},
350 children: childRoutes
351 }
352 };
353 map[props.name] = route.public;
354 if (childRoutes.length) {
355 childRoutes.forEach(function (child) {
356 child.public.parent = route.public;
357 });
358 }
359 return route;
360}
361
362function matchLocation(location, routes) {
363 for (var i = 0, len = routes.length; i < len; i++) {
364 var routeMatches = matchRoute(routes[i], location.pathname);
365 if (routeMatches.length) {
366 return createMatch(routeMatches, location);
367 }
368 }
369}
370function matchRoute(route, pathname) {
371 var _a = route.matching, re = _a.re, children = _a.children, exact = _a.exact;
372 var regExpMatch = re.exec(pathname);
373 if (!regExpMatch) {
374 return [];
375 }
376 var matchedSegment = regExpMatch[0], parsed = regExpMatch.slice(1);
377 var matches = [{ route: route, parsed: parsed }];
378 var remainder = pathname.slice(matchedSegment.length);
379 if (!children.length || remainder === "") {
380 return matches;
381 }
382 // match that ends with a strips it from the remainder
383 var fullSegments = withLeadingSlash(remainder);
384 for (var i = 0, length_1 = children.length; i < length_1; i++) {
385 var matched = matchRoute(children[i], fullSegments);
386 if (matched.length) {
387 return matches.concat(matched);
388 }
389 }
390 return exact ? [] : matches;
391}
392function createMatch(routeMatches, location) {
393 var route = routeMatches[routeMatches.length - 1].route.public;
394 return {
395 route: route,
396 match: {
397 location: location,
398 name: route.name,
399 params: routeMatches.reduce(function (params, _a) {
400 var route = _a.route, parsed = _a.parsed;
401 parsed.forEach(function (param, index) {
402 var name = route.matching.keys[index].name;
403 var fn = route.matching.parsers[name] || decodeURIComponent;
404 params[name] = fn(param);
405 });
406 return params;
407 }, {})
408 }
409 };
410}
411
412function prepareRoutes(routes) {
413 var mappedRoutes = {};
414 var prepared = routes.map(function (route) { return createRoute(route, mappedRoutes); });
415 return {
416 match: function (location) {
417 return matchLocation(location, prepared);
418 },
419 route: function (name) {
420 if (process.env.NODE_ENV !== "production" && !(name in mappedRoutes)) {
421 console.warn("Attempting to use route \"" + name + "\", but no route with that name exists.");
422 }
423 return mappedRoutes[name];
424 }
425 };
426}
427
428function announce(fmt, mode) {
429 if (mode === void 0) { mode = "assertive"; }
430 var announcer = document.createElement("div");
431 announcer.setAttribute("aria-live", mode);
432 // https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
433 announcer.setAttribute("style", [
434 "border: 0 !important;",
435 "clip: rect(1px, 1px, 1px, 1px) !important;",
436 "-webkit-clip-path: inset(50%) !important;",
437 "clip-path: inset(50%) !important;",
438 "height: 1px !important;",
439 "overflow: hidden !important;",
440 "padding: 0 !important;",
441 "position: absolute !important;",
442 "width: 1px !important;",
443 "white-space: nowrap !important;",
444 "top: 0;"
445 ].join(" "));
446 document.body.appendChild(announcer);
447 return function (emitted) {
448 announcer.textContent = fmt(emitted);
449 };
450}
451
452function scroll() {
453 return function (_a) {
454 var response = _a.response, navigation = _a.navigation;
455 if (navigation.action === "pop") {
456 return;
457 }
458 // wait until after the re-render to scroll
459 setTimeout(function () {
460 var hash = response.location.hash;
461 if (hash !== "") {
462 var element = document.getElementById(hash);
463 if (element && element.scrollIntoView) {
464 element.scrollIntoView();
465 return;
466 }
467 }
468 // if there is no hash, no element matching the hash,
469 // or the browser doesn't support, we will just scroll
470 // to the top of the page
471 window.scrollTo(0, 0);
472 }, 0);
473 };
474}
475
476function title(callback) {
477 return function (emitted) {
478 document.title = callback(emitted);
479 };
480}
481
482export { announce, createRouter, prepareRoutes, scroll, title };