UNPKG

@curi/router

Version:

A JavaScript router that doesn't care how you render

493 lines (478 loc) 17.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var interactions = require('@curi/interactions'); var PathToRegexp = _interopDefault(require('path-to-regexp')); function isAsyncRoute(route) { return typeof route.methods.resolve !== "undefined"; } function isExternalRedirect(redirect) { return "externalURL" in redirect; } function isRedirectLocation(redirect) { return "url" in redirect; } function finishResponse(route, match, resolvedResults, router, external) { var _a = resolvedResults || {}, _b = _a.resolved, resolved = _b === void 0 ? null : _b, _c = _a.error, error = _c === void 0 ? null : _c; var response = { data: undefined, body: undefined, meta: undefined }; for (var key in match) { response[key] = match[key]; } if (!route.methods.respond) { return response; } var results = route.methods.respond({ resolved: resolved, error: error, match: match, external: external }); if (!results) { if (process.env.NODE_ENV !== "production") { console.warn("\"" + match.name + "\"'s response function did not return anything. Did you forget to include a return statement?"); } return response; } if (process.env.NODE_ENV !== "production") { var validProperties_1 = { meta: true, body: true, data: true, redirect: true }; Object.keys(results).forEach(function (property) { if (!validProperties_1.hasOwnProperty(property)) { console.warn("\"" + property + "\" is not a valid response property. The valid properties are:\n\n " + Object.keys(validProperties_1).join(", ")); } }); } response["meta"] = results["meta"]; response["body"] = results["body"]; response["data"] = results["data"]; if (results["redirect"]) { response["redirect"] = createRedirect(results["redirect"], router); } return response; } function createRedirect(redirect, router) { if (isExternalRedirect(redirect)) { return redirect; } var name = redirect.name, params = redirect.params, query = redirect.query, hash = redirect.hash, state = redirect.state; var url = isRedirectLocation(redirect) ? redirect.url : router.url({ name: name, params: params, query: query, hash: hash }); return { name: name, params: params, query: query, hash: hash, state: state, url: url }; } function createRouter(historyConstructor, routes, options) { if (options === void 0) { options = {}; } var latestResponse; var latestNavigation; var history = historyConstructor(function (pendingNav) { var navigation = { action: pendingNav.action, previous: latestResponse }; var matched = routes.match(pendingNav.location); if (!matched) { if (process.env.NODE_ENV !== "production") { console.warn("The current location (" + pendingNav.location.pathname + ") has no matching route, " + 'so a response could not be emitted. A catch-all route ({ path: "(.*)" }) ' + "can be used to match locations with no other matching route."); } pendingNav.finish(); finishAndResetNavCallbacks(); return; } var route = matched.route, match = matched.match; if (!isAsyncRoute(route)) { finalizeResponseAndEmit(route, match, pendingNav, navigation, null); } else { announceAsyncNav(); route.methods .resolve(match, options.external) .then(function (resolved) { return ({ resolved: resolved, error: null }); }, function (error) { return ({ error: error, resolved: null }); }) .then(function (resolved) { if (pendingNav.cancelled) { return; } finalizeResponseAndEmit(route, match, pendingNav, navigation, resolved); }); } }, options.history || {}); function finalizeResponseAndEmit(route, match, pending, navigation, resolved) { asyncNavComplete(); pending.finish(); var response = finishResponse(route, match, resolved, router, options.external); finishAndResetNavCallbacks(); emitImmediate(response, navigation); } var _a = options.invisibleRedirects, invisibleRedirects = _a === void 0 ? false : _a; function emitImmediate(response, navigation) { if (!response.redirect || !invisibleRedirects || isExternalRedirect(response.redirect)) { latestResponse = response; latestNavigation = navigation; var emit = { response: response, navigation: navigation, router: router }; callObservers(emit); callOneTimersAndSideEffects(emit); } if (response.redirect !== undefined && !isExternalRedirect(response.redirect)) { history.navigate(response.redirect, "replace"); } } function callObservers(emitted) { observers.forEach(function (fn) { fn(emitted); }); } function callOneTimersAndSideEffects(emitted) { oneTimers.splice(0).forEach(function (fn) { fn(emitted); }); if (options.sideEffects) { options.sideEffects.forEach(function (fn) { fn(emitted); }); } } /* router.observer & router.once */ var observers = []; var oneTimers = []; function observe(fn, options) { var _a = (options || {}).initial, initial = _a === void 0 ? true : _a; observers.push(fn); if (latestResponse && initial) { fn({ response: latestResponse, navigation: latestNavigation, router: router }); } return function () { observers = observers.filter(function (obs) { return obs !== fn; }); }; } function once(fn, options) { var _a = (options || {}).initial, initial = _a === void 0 ? true : _a; if (latestResponse && initial) { fn({ response: latestResponse, navigation: latestNavigation, router: router }); } else { oneTimers.push(fn); } } /* router.url */ function url(details) { var name = details.name, params = details.params, hash = details.hash, query = details.query; var pathname; if (name) { var route = router.route(name); if (route) { pathname = interactions.pathname(route, params); } } return history.url({ pathname: pathname, hash: hash, query: query }); } /* router.navigate */ var cancelCallback; var finishCallback; function navigate(details) { cancelAndResetNavCallbacks(); var url = details.url, state = details.state, method = details.method; history.navigate({ url: url, state: state }, method); if (details.cancelled || details.finished) { cancelCallback = details.cancelled; finishCallback = details.finished; return resetCallbacks; } } function cancelAndResetNavCallbacks() { if (cancelCallback) { cancelCallback(); } resetCallbacks(); } function finishAndResetNavCallbacks() { if (finishCallback) { finishCallback(); } resetCallbacks(); } function resetCallbacks() { cancelCallback = undefined; finishCallback = undefined; } /* router.cancel */ var cancelWith; var asyncNavNotifiers = []; function cancel(fn) { asyncNavNotifiers.push(fn); return function () { asyncNavNotifiers = asyncNavNotifiers.filter(function (can) { return can !== fn; }); }; } // let any async navigation listeners (observers from router.cancel) // know that there is an asynchronous navigation happening function announceAsyncNav() { if (asyncNavNotifiers.length && cancelWith === undefined) { cancelWith = function () { history.cancel(); asyncNavComplete(); cancelAndResetNavCallbacks(); }; asyncNavNotifiers.forEach(function (fn) { fn(cancelWith); }); } } function asyncNavComplete() { if (cancelWith) { cancelWith = undefined; asyncNavNotifiers.forEach(function (fn) { fn(); }); } } var router = { route: routes.route, history: history, external: options.external, observe: observe, once: once, cancel: cancel, url: url, navigate: navigate, current: function () { return { response: latestResponse, navigation: latestNavigation }; }, destroy: function () { history.destroy(); } }; history.current(); return router; } var withLeadingSlash = function (path) { return path.charAt(0) === "/" ? path : "/" + path; }; var withTrailingSlash = function (path) { return path.charAt(path.length - 1) === "/" ? path : path + "/"; }; var join = function (beginning, end) { return withTrailingSlash(beginning) + end; }; function createRoute(props, map, parent) { if (parent === void 0) { parent = { path: "", keys: [] }; } if (process.env.NODE_ENV !== "production") { if (props.name in map) { throw new Error("Multiple routes have the name \"" + props.name + "\". Route names must be unique."); } if (props.path.charAt(0) === "/") { throw new Error("Route paths cannot start with a forward slash (/). (Received \"" + props.path + "\")"); } } var fullPath = withLeadingSlash(join(parent.path, props.path)); var _a = props.pathOptions || {}, _b = _a.match, matchOptions = _b === void 0 ? {} : _b, _c = _a.compile, compileOptions = _c === void 0 ? {} : _c; // end must be false for routes with children, but we want to track its original value var exact = matchOptions.end == null || matchOptions.end; if (props.children && props.children.length) { matchOptions.end = false; } var keys = []; var re = PathToRegexp(withLeadingSlash(props.path), keys, matchOptions); var keyNames = keys.map(function (key) { return key.name; }); if (parent.keys.length) { keyNames = parent.keys.concat(keyNames); } var childRoutes = []; var children = []; if (props.children && props.children.length) { childRoutes = props.children.map(function (child) { return createRoute(child, map, { path: fullPath, keys: keyNames }); }); children = childRoutes.map(function (child) { return child.public; }); } var compiled = PathToRegexp.compile(fullPath); var route = { public: { name: props.name, keys: keyNames, parent: undefined, children: children, methods: { resolve: props.resolve, respond: props.respond, pathname: function (params) { return compiled(params, compileOptions); } }, extra: props.extra }, matching: { re: re, keys: keys, exact: exact, parsers: props.params || {}, children: childRoutes } }; map[props.name] = route.public; if (childRoutes.length) { childRoutes.forEach(function (child) { child.public.parent = route.public; }); } return route; } function matchLocation(location, routes) { for (var i = 0, len = routes.length; i < len; i++) { var routeMatches = matchRoute(routes[i], location.pathname); if (routeMatches.length) { return createMatch(routeMatches, location); } } } function matchRoute(route, pathname) { var _a = route.matching, re = _a.re, children = _a.children, exact = _a.exact; var regExpMatch = re.exec(pathname); if (!regExpMatch) { return []; } var matchedSegment = regExpMatch[0], parsed = regExpMatch.slice(1); var matches = [{ route: route, parsed: parsed }]; var remainder = pathname.slice(matchedSegment.length); if (!children.length || remainder === "") { return matches; } // match that ends with a strips it from the remainder var fullSegments = withLeadingSlash(remainder); for (var i = 0, length_1 = children.length; i < length_1; i++) { var matched = matchRoute(children[i], fullSegments); if (matched.length) { return matches.concat(matched); } } return exact ? [] : matches; } function createMatch(routeMatches, location) { var route = routeMatches[routeMatches.length - 1].route.public; return { route: route, match: { location: location, name: route.name, params: routeMatches.reduce(function (params, _a) { var route = _a.route, parsed = _a.parsed; parsed.forEach(function (param, index) { var name = route.matching.keys[index].name; var fn = route.matching.parsers[name] || decodeURIComponent; params[name] = fn(param); }); return params; }, {}) } }; } function prepareRoutes(routes) { var mappedRoutes = {}; var prepared = routes.map(function (route) { return createRoute(route, mappedRoutes); }); return { match: function (location) { return matchLocation(location, prepared); }, route: function (name) { if (process.env.NODE_ENV !== "production" && !(name in mappedRoutes)) { console.warn("Attempting to use route \"" + name + "\", but no route with that name exists."); } return mappedRoutes[name]; } }; } function announce(fmt, mode) { if (mode === void 0) { mode = "assertive"; } var announcer = document.createElement("div"); announcer.setAttribute("aria-live", mode); // https://hugogiraudel.com/2016/10/13/css-hide-and-seek/ announcer.setAttribute("style", [ "border: 0 !important;", "clip: rect(1px, 1px, 1px, 1px) !important;", "-webkit-clip-path: inset(50%) !important;", "clip-path: inset(50%) !important;", "height: 1px !important;", "overflow: hidden !important;", "padding: 0 !important;", "position: absolute !important;", "width: 1px !important;", "white-space: nowrap !important;", "top: 0;" ].join(" ")); document.body.appendChild(announcer); return function (emitted) { announcer.textContent = fmt(emitted); }; } function scroll() { return function (_a) { var response = _a.response, navigation = _a.navigation; if (navigation.action === "pop") { return; } // wait until after the re-render to scroll setTimeout(function () { var hash = response.location.hash; if (hash !== "") { var element = document.getElementById(hash); if (element && element.scrollIntoView) { element.scrollIntoView(); return; } } // if there is no hash, no element matching the hash, // or the browser doesn't support, we will just scroll // to the top of the page window.scrollTo(0, 0); }, 0); }; } function title(callback) { return function (emitted) { document.title = callback(emitted); }; } exports.announce = announce; exports.createRouter = createRouter; exports.prepareRoutes = prepareRoutes; exports.scroll = scroll; exports.title = title;