UNPKG

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