UNPKG

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