UNPKG

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