UNPKG

mcrm-svelte-navigator

Version:
1,857 lines (1,644 loc) 66.8 kB
import { SvelteComponent, init, safe_not_equal, assign, create_slot, element, space, empty, set_attributes, insert, update_slot_base, get_all_dirty_from_scope, get_slot_changes, transition_in, transition_out, detach, component_subscribe, text, append, set_data_maybe_contenteditable, group_outros, check_outros, compute_rest_props, exclude_internal_props, create_component, mount_component, destroy_component, construct_svelte_component, get_spread_update, get_spread_object, listen } from 'svelte/internal'; import { tick, getContext, onMount, setContext, onDestroy, createEventDispatcher } from 'svelte'; import { get, writable, derived } from 'svelte/store'; // generated during release, do not modify const PUBLIC_VERSION = '4'; if (typeof window !== 'undefined') // @ts-ignore (window.__svelte || (window.__svelte = { v: new Set() })).v.add(PUBLIC_VERSION); /* * Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/utils.js * * https://github.com/reach/router/blob/master/LICENSE */ const isUndefined = value => typeof value === "undefined"; const isFunction = value => typeof value === "function"; const isNumber = value => typeof value === "number"; /** * Decides whether a given `event` should result in a navigation or not. * @param {object} event */ function shouldNavigate(event) { return ( !event.defaultPrevented && event.button === 0 && !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) ); } function createCounter() { let i = 0; /** * Returns an id and increments the internal state * @returns {number} */ return () => i++; } /** * Create a globally unique id * * @returns {string} An id */ function createGlobalId() { return Math.random().toString(36).substring(2); } function findClosest(tagName, element) { while (element && element.tagName !== tagName) { // eslint-disable-next-line no-param-reassign element = element.parentNode; } return element; } const isSSR = typeof window === "undefined"; function addListener(target, type, handler) { target.addEventListener(type, handler); return () => target.removeEventListener(type, handler); } const createInlineStyle = (disableInlineStyles, style) => disableInlineStyles ? {} : { style }; const createMarkerProps = disableInlineStyles => ({ "aria-hidden": "true", ...createInlineStyle(disableInlineStyles, "display:none;"), }); /* * Adapted from https://github.com/EmilTholin/svelte-routing * * https://github.com/EmilTholin/svelte-routing/blob/master/LICENSE */ const createKey = ctxName => `@@svnav-ctx__${ctxName}`; // Use strings instead of objects, so different versions of // svelte-navigator can potentially still work together const LOCATION = createKey("LOCATION"); const ROUTER = createKey("ROUTER"); const ROUTE = createKey("ROUTE"); const ROUTE_PARAMS = createKey("ROUTE_PARAMS"); const FOCUS_ELEM = createKey("FOCUS_ELEM"); const paramRegex = /^:(.+)/; const substr = (str, start, end) => str.substr(start, end); /** * Check if `string` starts with `search` * @param {string} string * @param {string} search * @return {boolean} */ const startsWith = (string, search) => substr(string, 0, search.length) === search; /** * Check if `segment` is a root segment * @param {string} segment * @return {boolean} */ const isRootSegment = segment => segment === ""; /** * Check if `segment` is a dynamic segment * @param {string} segment * @return {boolean} */ const isDynamic = segment => paramRegex.test(segment); /** * Check if `segment` is a splat * @param {string} segment * @return {boolean} */ const isSplat = segment => segment[0] === "*"; /** * Strip potention splat and splatname of the end of a path * @param {string} str * @return {string} */ const stripSplat = str => str.replace(/\*.*$/, ""); /** * Strip `str` of potential start and end `/` * @param {string} str * @return {string} */ const stripSlashes = str => str.replace(/(^\/+|\/+$)/g, ""); /** * Split up the URI into segments delimited by `/` * @param {string} uri * @return {string[]} */ function segmentize(uri, filterFalsy = false) { const segments = stripSlashes(uri).split("/"); return filterFalsy ? segments.filter(Boolean) : segments; } /** * Add the query to the pathname if a query is given * @param {string} pathname * @param {string} [query] * @return {string} */ const addQuery = (pathname, query) => pathname + (query ? `?${query}` : ""); /** * Normalizes a basepath * * @param {string} path * @returns {string} * * @example * normalizePath("base/path/") // -> "/base/path" */ const normalizePath = path => `/${stripSlashes(path)}`; /** * Joins and normalizes multiple path fragments * * @param {...string} pathFragments * @returns {string} */ function join(...pathFragments) { const joinFragment = fragment => segmentize(fragment, true).join("/"); const joinedSegments = pathFragments.map(joinFragment).join("/"); return normalizePath(joinedSegments); } // We start from 1 here, so we can check if an origin id has been passed // by using `originId || <fallback>` const LINK_ID = 1; const ROUTE_ID = 2; const ROUTER_ID = 3; const USE_FOCUS_ID = 4; const USE_LOCATION_ID = 5; const USE_MATCH_ID = 6; const USE_NAVIGATE_ID = 7; const USE_PARAMS_ID = 8; const USE_RESOLVABLE_ID = 9; const USE_RESOLVE_ID = 10; const NAVIGATE_ID = 11; const labels = { [LINK_ID]: "Link", [ROUTE_ID]: "Route", [ROUTER_ID]: "Router", [USE_FOCUS_ID]: "useFocus", [USE_LOCATION_ID]: "useLocation", [USE_MATCH_ID]: "useMatch", [USE_NAVIGATE_ID]: "useNavigate", [USE_PARAMS_ID]: "useParams", [USE_RESOLVABLE_ID]: "useResolvable", [USE_RESOLVE_ID]: "useResolve", [NAVIGATE_ID]: "navigate", }; const createLabel = labelId => labels[labelId]; function createIdentifier(labelId, props) { let attr; if (labelId === ROUTE_ID) { attr = props.path ? `path="${props.path}"` : "default"; } else if (labelId === LINK_ID) { attr = `to="${props.to}"`; } else if (labelId === ROUTER_ID) { attr = `basepath="${props.basepath || ""}"`; } return `<${createLabel(labelId)} ${attr || ""} />`; } function createMessage(labelId, message, props, originId) { const origin = props && createIdentifier(originId || labelId, props); const originMsg = origin ? `\n\nOccurred in: ${origin}` : ""; const label = createLabel(labelId); const msg = isFunction(message) ? message(label) : message; return `<${label}> ${msg}${originMsg}`; } const createMessageHandler = handler => (...args) => handler(createMessage(...args)); const fail = createMessageHandler(message => { throw new Error(message); }); // eslint-disable-next-line no-console const warn = createMessageHandler(console.warn); const SEGMENT_POINTS = 4; const STATIC_POINTS = 3; const DYNAMIC_POINTS = 2; const SPLAT_PENALTY = 1; const ROOT_POINTS = 1; /** * Score a route depending on how its individual segments look * @param {object} route * @param {number} index * @return {object} */ function rankRoute(route, index) { const score = route.default ? 0 : segmentize(route.fullPath).reduce((acc, segment) => { let nextScore = acc; nextScore += SEGMENT_POINTS; if (isRootSegment(segment)) { nextScore += ROOT_POINTS; } else if (isDynamic(segment)) { nextScore += DYNAMIC_POINTS; } else if (isSplat(segment)) { nextScore -= SEGMENT_POINTS + SPLAT_PENALTY; } else { nextScore += STATIC_POINTS; } return nextScore; }, 0); return { route, score, index }; } /** * Give a score to all routes and sort them on that * @param {object[]} routes * @return {object[]} */ function rankRoutes(routes) { return ( routes .map(rankRoute) // If two routes have the exact same score, we go by index instead .sort((a, b) => { if (a.score < b.score) { return 1; } if (a.score > b.score) { return -1; } return a.index - b.index; }) ); } /** * Ranks and picks the best route to match. Each segment gets the highest * amount of points, then the type of segment gets an additional amount of * points where * * static > dynamic > splat > root * * This way we don't have to worry about the order of our routes, let the * computers do it. * * A route looks like this * * { fullPath, default, value } * * And a returned match looks like: * * { route, params, uri } * * @param {object[]} routes * @param {string} uri * @return {?object} */ function pick(routes, uri) { let bestMatch; let defaultMatch; const [uriPathname] = uri.split("?"); const uriSegments = segmentize(uriPathname); const isRootUri = uriSegments[0] === ""; const ranked = rankRoutes(routes); for (let i = 0, l = ranked.length; i < l; i++) { const { route } = ranked[i]; let missed = false; const params = {}; // eslint-disable-next-line no-shadow const createMatch = uri => ({ ...route, params, uri }); if (route.default) { defaultMatch = createMatch(uri); continue; } const routeSegments = segmentize(route.fullPath); const max = Math.max(uriSegments.length, routeSegments.length); let index = 0; for (; index < max; index++) { const routeSegment = routeSegments[index]; const uriSegment = uriSegments[index]; if (!isUndefined(routeSegment) && isSplat(routeSegment)) { // Hit a splat, just grab the rest, and return a match // uri: /files/documents/work // route: /files/* or /files/*splatname const splatName = routeSegment === "*" ? "*" : routeSegment.slice(1); params[splatName] = uriSegments .slice(index) .map(decodeURIComponent) .join("/"); break; } if (isUndefined(uriSegment)) { // URI is shorter than the route, no match // uri: /users // route: /users/:userId missed = true; break; } const dynamicMatch = paramRegex.exec(routeSegment); if (dynamicMatch && !isRootUri) { const value = decodeURIComponent(uriSegment); params[dynamicMatch[1]] = value; } else if (routeSegment !== uriSegment) { // Current segments don't match, not dynamic, not splat, so no match // uri: /users/123/settings // route: /users/:id/profile missed = true; break; } } if (!missed) { bestMatch = createMatch(join(...uriSegments.slice(0, index))); break; } } return bestMatch || defaultMatch || null; } /** * Check if the `route.fullPath` matches the `uri`. * @param {Object} route * @param {string} uri * @return {?object} */ function match(route, uri) { return pick([route], uri); } /** * Resolve URIs as though every path is a directory, no files. Relative URIs * in the browser can feel awkward because not only can you be "in a directory", * you can be "at a file", too. For example: * * browserSpecResolve('foo', '/bar/') => /bar/foo * browserSpecResolve('foo', '/bar') => /foo * * But on the command line of a file system, it's not as complicated. You can't * `cd` from a file, only directories. This way, links have to know less about * their current path. To go deeper you can do this: * * <Link to="deeper"/> * // instead of * <Link to=`{${props.uri}/deeper}`/> * * Just like `cd`, if you want to go deeper from the command line, you do this: * * cd deeper * # not * cd $(pwd)/deeper * * By treating every path as a directory, linking to relative paths should * require less contextual information and (fingers crossed) be more intuitive. * @param {string} to * @param {string} base * @return {string} */ function resolve(to, base) { // /foo/bar, /baz/qux => /foo/bar if (startsWith(to, "/")) { return to; } const [toPathname, toQuery] = to.split("?"); const [basePathname] = base.split("?"); const toSegments = segmentize(toPathname); const baseSegments = segmentize(basePathname); // ?a=b, /users?b=c => /users?a=b if (toSegments[0] === "") { return addQuery(basePathname, toQuery); } // profile, /users/789 => /users/789/profile if (!startsWith(toSegments[0], ".")) { const pathname = baseSegments.concat(toSegments).join("/"); return addQuery((basePathname === "/" ? "" : "/") + pathname, toQuery); } // ./ , /users/123 => /users/123 // ../ , /users/123 => /users // ../.. , /users/123 => / // ../../one, /a/b/c/d => /a/b/one // .././one , /a/b/c/d => /a/b/c/one const allSegments = baseSegments.concat(toSegments); const segments = []; allSegments.forEach(segment => { if (segment === "..") { segments.pop(); } else if (segment !== ".") { segments.push(segment); } }); return addQuery(`/${segments.join("/")}`, toQuery); } /** * Normalizes a location for consumption by `Route` children and the `Router`. * It removes the apps basepath from the pathname * and sets default values for `search` and `hash` properties. * * @param {Object} location The current global location supplied by the history component * @param {string} basepath The applications basepath (i.e. when serving from a subdirectory) * * @returns The normalized location */ function normalizeLocation(location, basepath) { const { pathname, hash = "", search = "", state } = location; const baseSegments = segmentize(basepath, true); const pathSegments = segmentize(pathname, true); while (baseSegments.length) { if (baseSegments[0] !== pathSegments[0]) { fail( ROUTER_ID, `Invalid state: All locations must begin with the basepath "${basepath}", found "${pathname}"`, ); } baseSegments.shift(); pathSegments.shift(); } return { pathname: join(...pathSegments), hash, search, state, }; } const normalizeUrlFragment = frag => (frag.length === 1 ? "" : frag); /** * Creates a location object from an url. * It is used to create a location from the url prop used in SSR * * @param {string} url The url string (e.g. "/path/to/somewhere") * @returns {{ pathname: string; search: string; hash: string }} The location * * @example * ```js * const path = "/search?q=falafel#result-3"; * const location = parsePath(path); * // -> { * // pathname: "/search", * // search: "?q=falafel", * // hash: "#result-3", * // }; * ``` */ const parsePath = path => { const searchIndex = path.indexOf("?"); const hashIndex = path.indexOf("#"); const hasSearchIndex = searchIndex !== -1; const hasHashIndex = hashIndex !== -1; const hash = hasHashIndex ? normalizeUrlFragment(substr(path, hashIndex)) : ""; const pathnameAndSearch = hasHashIndex ? substr(path, 0, hashIndex) : path; const search = hasSearchIndex ? normalizeUrlFragment(substr(pathnameAndSearch, searchIndex)) : ""; const pathname = (hasSearchIndex ? substr(pathnameAndSearch, 0, searchIndex) : pathnameAndSearch) || "/"; return { pathname, search, hash }; }; /** * Joins a location object to one path string. * * @param {{ pathname: string; search: string; hash: string }} location The location object * @returns {string} A path, created from the location * * @example * ```js * const location = { * pathname: "/search", * search: "?q=falafel", * hash: "#result-3", * }; * const path = stringifyPath(location); * // -> "/search?q=falafel#result-3" * ``` */ const stringifyPath = location => { const { pathname, search, hash } = location; return pathname + search + hash; }; /** * Resolves a link relative to the parent Route and the Routers basepath. * * @param {string} path The given path, that will be resolved * @param {string} routeBase The current Routes base path * @param {string} appBase The basepath of the app. Used, when serving from a subdirectory * @returns {string} The resolved path * * @example * resolveLink("relative", "/routeBase", "/") // -> "/routeBase/relative" * resolveLink("/absolute", "/routeBase", "/") // -> "/absolute" * resolveLink("relative", "/routeBase", "/base") // -> "/base/routeBase/relative" * resolveLink("/absolute", "/routeBase", "/base") // -> "/base/absolute" */ function resolveLink(path, routeBase, appBase) { return join(appBase, resolve(path, routeBase)); } /** * Get the uri for a Route, by matching it against the current location. * * @param {string} routePath The Routes resolved path * @param {string} pathname The current locations pathname */ function extractBaseUri(routePath, pathname) { const fullPath = normalizePath(stripSplat(routePath)); const baseSegments = segmentize(fullPath, true); const pathSegments = segmentize(pathname, true).slice(0, baseSegments.length); const routeMatch = match({ fullPath }, join(...pathSegments)); return routeMatch && routeMatch.uri; } /* * Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/history.js * * https://github.com/reach/router/blob/master/LICENSE */ const POP = "POP"; const PUSH = "PUSH"; const REPLACE = "REPLACE"; function getLocation(source) { return { ...source.location, pathname: encodeURI(decodeURI(source.location.pathname)), state: source.history.state, _key: (source.history.state && source.history.state._key) || "initial", }; } function createHistory(source) { let listeners = []; let location = getLocation(source); let action = POP; const notifyListeners = (listenerFns = listeners) => listenerFns.forEach(listener => listener({ location, action })); return { get location() { return location; }, listen(listener) { listeners.push(listener); const popstateListener = () => { location = getLocation(source); action = POP; notifyListeners([listener]); }; // Call listener when it is registered notifyListeners([listener]); const unlisten = addListener(source, "popstate", popstateListener); return () => { unlisten(); listeners = listeners.filter(fn => fn !== listener); }; }, /** * Navigate to a new absolute route. * * @param {string|number} to The path to navigate to. * * If `to` is a number we will navigate to the stack entry index + `to` * (-> `navigate(-1)`, is equivalent to hitting the back button of the browser) * @param {Object} options * @param {*} [options.state] The state will be accessible through `location.state` * @param {boolean} [options.replace=false] Replace the current entry in the history * stack, instead of pushing on a new one */ navigate(to, options) { const { state = {}, replace = false } = options || {}; action = replace ? REPLACE : PUSH; if (isNumber(to)) { if (options) { warn( NAVIGATE_ID, "Navigation options (state or replace) are not supported, " + "when passing a number as the first argument to navigate. " + "They are ignored.", ); } action = POP; source.history.go(to); } else { const keyedState = { ...state, _key: createGlobalId() }; // try...catch iOS Safari limits to 100 pushState calls try { source.history[replace ? "replaceState" : "pushState"]( keyedState, "", to, ); } catch (e) { source.location[replace ? "replace" : "assign"](to); } } location = getLocation(source); notifyListeners(); }, }; } function createStackFrame(state, uri) { return { ...parsePath(uri), state }; } // Stores history entries in memory for testing or other platforms like Native function createMemorySource(initialPathname = "/") { let index = 0; let stack = [createStackFrame(null, initialPathname)]; return { // This is just for testing... get entries() { return stack; }, get location() { return stack[index]; }, addEventListener() {}, removeEventListener() {}, history: { get state() { return stack[index].state; }, pushState(state, title, uri) { index++; // Throw away anything in the stack with an index greater than the current index. // This happens, when we go back using `go(-n)`. The index is now less than `stack.length`. // If we call `go(+n)` the stack entries with an index greater than the current index can // be reused. // However, if we navigate to a path, instead of a number, we want to create a new branch // of navigation. stack = stack.slice(0, index); stack.push(createStackFrame(state, uri)); }, replaceState(state, title, uri) { stack[index] = createStackFrame(state, uri); }, go(to) { const newIndex = index + to; if (newIndex < 0 || newIndex > stack.length - 1) { return; } index = newIndex; }, }, }; } // Global history uses window.history as the source if available, // otherwise a memory history const canUseDOM = !!( !isSSR && window.document && window.document.createElement ); // Use memory history in iframes (for example in Svelte REPL) const isEmbeddedPage = !isSSR && window.location.origin === "null"; const globalHistory = createHistory( canUseDOM && !isEmbeddedPage ? window : createMemorySource(), ); const { navigate } = globalHistory; // We need to keep the focus candidate in a separate file, so svelte does // not update, when we mutate it. // Also, we need a single global reference, because taking focus needs to // work globally, even if we have multiple top level routers // eslint-disable-next-line import/no-mutable-exports let focusCandidate = null; // eslint-disable-next-line import/no-mutable-exports let initialNavigation = true; /** * Check if RouterA is above RouterB in the document * @param {number} routerIdA The first Routers id * @param {number} routerIdB The second Routers id */ function isAbove(routerIdA, routerIdB) { const routerMarkers = document.querySelectorAll("[data-svnav-router]"); for (let i = 0; i < routerMarkers.length; i++) { const node = routerMarkers[i]; const currentId = Number(node.dataset.svnavRouter); if (currentId === routerIdA) return true; if (currentId === routerIdB) return false; } return false; } /** * Check if a Route candidate is the best choice to move focus to, * and store the best match. * @param {{ level: number; routerId: number; route: { id: number; focusElement: import("svelte/store").Readable<Promise<Element>|null>; } }} item A Route candidate, that updated and is visible after a navigation */ function pushFocusCandidate(item) { if ( // Best candidate if it's the only candidate... !focusCandidate || // Route is nested deeper, than previous candidate // -> Route change was triggered in the deepest affected // Route, so that's were focus should move to item.level > focusCandidate.level || // If the level is identical, we want to focus the first Route in the document, // so we pick the first Router lookin from page top to page bottom. (item.level === focusCandidate.level && isAbove(item.routerId, focusCandidate.routerId)) ) { focusCandidate = item; } } /** * Reset the focus candidate. */ function clearFocusCandidate() { focusCandidate = null; } function initialNavigationOccurred() { initialNavigation = false; } /* * `focus` Adapted from https://github.com/oaf-project/oaf-side-effects/blob/master/src/index.ts * * https://github.com/oaf-project/oaf-side-effects/blob/master/LICENSE */ function focus(elem) { if (!elem) return false; const TABINDEX = "tabindex"; try { if (!elem.hasAttribute(TABINDEX)) { elem.setAttribute(TABINDEX, "-1"); let unlisten; // We remove tabindex after blur to avoid weird browser behavior // where a mouse click can activate elements with tabindex="-1". const blurListener = () => { elem.removeAttribute(TABINDEX); unlisten(); }; unlisten = addListener(elem, "blur", blurListener); } elem.focus(); return document.activeElement === elem; } catch (e) { // Apparently trying to focus a disabled element in IE can throw. // See https://stackoverflow.com/a/1600194/2476884 return false; } } function isEndMarker(elem, id) { return Number(elem.dataset.svnavRouteEnd) === id; } function isHeading(elem) { return /^H[1-6]$/i.test(elem.tagName); } function query(selector, parent = document) { return parent.querySelector(selector); } function queryHeading(id) { const marker = query(`[data-svnav-route-start="${id}"]`); let current = marker.nextElementSibling; while (!isEndMarker(current, id)) { if (isHeading(current)) { return current; } const heading = query("h1,h2,h3,h4,h5,h6", current); if (heading) { return heading; } current = current.nextElementSibling; } return null; } function handleFocus(route) { Promise.resolve(get(route.focusElement)).then(elem => { const focusElement = elem || queryHeading(route.id); if (!focusElement) { warn( ROUTER_ID, "Could not find an element to focus. " + "You should always render a header for accessibility reasons, " + 'or set a custom focus element via the "useFocus" hook. ' + "If you don't want this Route or Router to manage focus, " + 'pass "primary={false}" to it.', route, ROUTE_ID, ); } const headingFocused = focus(focusElement); if (headingFocused) return; focus(document.documentElement); }); } const createTriggerFocus = (a11yConfig, announcementText, location) => (manageFocus, announceNavigation) => // Wait until the dom is updated, so we can look for headings tick().then(() => { if (!focusCandidate || initialNavigation) { initialNavigationOccurred(); return; } if (manageFocus) { handleFocus(focusCandidate.route); } if (a11yConfig.announcements && announceNavigation) { const { path, fullPath, meta, params, uri } = focusCandidate.route; const announcementMessage = a11yConfig.createAnnouncement( { path, fullPath, meta, params, uri }, get(location), ); Promise.resolve(announcementMessage).then(message => { announcementText.set(message); }); } clearFocusCandidate(); }); const visuallyHiddenStyle = "position:fixed;" + "top:-1px;" + "left:0;" + "width:1px;" + "height:1px;" + "padding:0;" + "overflow:hidden;" + "clip:rect(0,0,0,0);" + "white-space:nowrap;" + "border:0;"; /* src\Router.svelte generated by Svelte v4.2.1 */ function create_if_block$1(ctx) { let div; let t; let div_levels = [ { role: "status" }, { "aria-atomic": "true" }, { "aria-live": "polite" }, { "data-svnav-announcer": "" }, createInlineStyle(/*shouldDisableInlineStyles*/ ctx[6], visuallyHiddenStyle) ]; let div_data = {}; for (let i = 0; i < div_levels.length; i += 1) { div_data = assign(div_data, div_levels[i]); } return { c() { div = element("div"); t = text(/*$announcementText*/ ctx[0]); set_attributes(div, div_data); }, m(target, anchor) { insert(target, div, anchor); append(div, t); }, p(ctx, dirty) { if (dirty[0] & /*$announcementText*/ 1) set_data_maybe_contenteditable(t, /*$announcementText*/ ctx[0], div_data['contenteditable']); }, d(detaching) { if (detaching) { detach(div); } } }; } function create_fragment$2(ctx) { let div; let t0; let t1; let if_block_anchor; let current; let div_levels = [ createMarkerProps(/*shouldDisableInlineStyles*/ ctx[6]), { "data-svnav-router": /*routerId*/ ctx[3] } ]; let div_data = {}; for (let i = 0; i < div_levels.length; i += 1) { div_data = assign(div_data, div_levels[i]); } const default_slot_template = /*#slots*/ ctx[22].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[21], null); let if_block = /*isTopLevelRouter*/ ctx[2] && /*manageFocus*/ ctx[4] && /*a11yConfig*/ ctx[1].announcements && create_if_block$1(ctx); return { c() { div = element("div"); t0 = space(); if (default_slot) default_slot.c(); t1 = space(); if (if_block) if_block.c(); if_block_anchor = empty(); set_attributes(div, div_data); }, m(target, anchor) { insert(target, div, anchor); insert(target, t0, anchor); if (default_slot) { default_slot.m(target, anchor); } insert(target, t1, anchor); if (if_block) if_block.m(target, anchor); insert(target, if_block_anchor, anchor); current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && (!current || dirty[0] & /*$$scope*/ 2097152)) { update_slot_base( default_slot, default_slot_template, ctx, /*$$scope*/ ctx[21], !current ? get_all_dirty_from_scope(/*$$scope*/ ctx[21]) : get_slot_changes(default_slot_template, /*$$scope*/ ctx[21], dirty, null), null ); } } if (/*isTopLevelRouter*/ ctx[2] && /*manageFocus*/ ctx[4] && /*a11yConfig*/ ctx[1].announcements) if_block.p(ctx, dirty); }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) { detach(div); detach(t0); detach(t1); detach(if_block_anchor); } if (default_slot) default_slot.d(detaching); if (if_block) if_block.d(detaching); } }; } const createId$1 = createCounter(); const defaultBasepath = "/"; function instance$2($$self, $$props, $$invalidate) { let $location; let $activeRoute; let $prevLocation; let $routes; let $announcementText; let { $$slots: slots = {}, $$scope } = $$props; let { basepath = defaultBasepath } = $$props; let { url = null } = $$props; let { history = globalHistory } = $$props; let { primary = true } = $$props; let { a11y = {} } = $$props; let { disableInlineStyles = false } = $$props; const a11yConfig = { createAnnouncement: route => `Navigated to ${route.uri}`, announcements: true, ...a11y }; // Remember the initial `basepath`, so we can fire a warning // when the user changes it later const initialBasepath = basepath; const normalizedBasepath = normalizePath(basepath); const locationContext = getContext(LOCATION); const routerContext = getContext(ROUTER); const isTopLevelRouter = !locationContext; const routerId = createId$1(); const manageFocus = primary && !(routerContext && !routerContext.manageFocus); const announcementText = writable(""); component_subscribe($$self, announcementText, value => $$invalidate(0, $announcementText = value)); const shouldDisableInlineStyles = routerContext ? routerContext.disableInlineStyles : disableInlineStyles; const routes = writable([]); component_subscribe($$self, routes, value => $$invalidate(20, $routes = value)); const activeRoute = writable(null); component_subscribe($$self, activeRoute, value => $$invalidate(18, $activeRoute = value)); // Used in SSR to synchronously set that a Route is active. let hasActiveRoute = false; // Nesting level of router. // We will need this to identify sibling routers, when moving // focus on navigation, so we can focus the first possible router const level = isTopLevelRouter ? 0 : routerContext.level + 1; // If we're running an SSR we force the location to the `url` prop const getInitialLocation = () => normalizeLocation(isSSR ? parsePath(url) : history.location, normalizedBasepath); const location = isTopLevelRouter ? writable(getInitialLocation()) : locationContext; component_subscribe($$self, location, value => $$invalidate(17, $location = value)); const prevLocation = writable($location); component_subscribe($$self, prevLocation, value => $$invalidate(19, $prevLocation = value)); const triggerFocus = createTriggerFocus(a11yConfig, announcementText, location); const createRouteFilter = routeId => routeList => routeList.filter(routeItem => routeItem.id !== routeId); function registerRoute(route) { if (isSSR) { // In SSR we should set the activeRoute immediately if it is a match. // If there are more Routes being registered after a match is found, // we just skip them. if (hasActiveRoute) { return; } const matchingRoute = match(route, $location.pathname); if (matchingRoute) { hasActiveRoute = true; // Return the match in SSR mode, so the matched Route can use it immediatly. // Waiting for activeRoute to update does not work, because it updates // after the Route is initialized return matchingRoute; // eslint-disable-line consistent-return } } else { routes.update(prevRoutes => { // Remove an old version of the updated route, // before pushing the new version const nextRoutes = createRouteFilter(route.id)(prevRoutes); nextRoutes.push(route); return nextRoutes; }); } } function unregisterRoute(routeId) { routes.update(createRouteFilter(routeId)); } if (!isTopLevelRouter && basepath !== defaultBasepath) { warn(ROUTER_ID, 'Only top-level Routers can have a "basepath" prop. It is ignored.', { basepath }); } if (isTopLevelRouter) { // The topmost Router in the tree is responsible for updating // the location store and supplying it through context. onMount(() => { const unlisten = history.listen(changedHistory => { const normalizedLocation = normalizeLocation(changedHistory.location, normalizedBasepath); prevLocation.set($location); location.set(normalizedLocation); }); return unlisten; }); setContext(LOCATION, location); } setContext(ROUTER, { activeRoute, registerRoute, unregisterRoute, manageFocus, level, id: routerId, history: isTopLevelRouter ? history : routerContext.history, basepath: isTopLevelRouter ? normalizedBasepath : routerContext.basepath, disableInlineStyles: shouldDisableInlineStyles }); $$self.$$set = $$props => { if ('basepath' in $$props) $$invalidate(11, basepath = $$props.basepath); if ('url' in $$props) $$invalidate(12, url = $$props.url); if ('history' in $$props) $$invalidate(13, history = $$props.history); if ('primary' in $$props) $$invalidate(14, primary = $$props.primary); if ('a11y' in $$props) $$invalidate(15, a11y = $$props.a11y); if ('disableInlineStyles' in $$props) $$invalidate(16, disableInlineStyles = $$props.disableInlineStyles); if ('$$scope' in $$props) $$invalidate(21, $$scope = $$props.$$scope); }; $$self.$$.update = () => { if ($$self.$$.dirty[0] & /*basepath*/ 2048) { if (basepath !== initialBasepath) { warn(ROUTER_ID, 'You cannot change the "basepath" prop. It is ignored.'); } } if ($$self.$$.dirty[0] & /*$routes, $location*/ 1179648) { // This reactive statement will be run when the Router is created // when there are no Routes and then again the following tick, so it // will not find an active Route in SSR and in the browser it will only // pick an active Route after all Routes have been registered. { const bestMatch = pick($routes, $location.pathname); activeRoute.set(bestMatch); } } if ($$self.$$.dirty[0] & /*$location, $prevLocation*/ 655360) { // Manage focus and announce navigation to screen reader users { if (isTopLevelRouter) { const hasHash = !!$location.hash; // When a hash is present in the url, we skip focus management, because // focusing a different element will prevent in-page jumps (See #3) const shouldManageFocus = !hasHash && manageFocus; // We don't want to make an announcement, when the hash changes, // but the active route stays the same const announceNavigation = !hasHash || $location.pathname !== $prevLocation.pathname; triggerFocus(shouldManageFocus, announceNavigation); } } } if ($$self.$$.dirty[0] & /*$activeRoute*/ 262144) { // Queue matched Route, so top level Router can decide which Route to focus. // Non primary Routers should just be ignored if (manageFocus && $activeRoute && $activeRoute.primary) { pushFocusCandidate({ level, routerId, route: $activeRoute }); } } }; return [ $announcementText, a11yConfig, isTopLevelRouter, routerId, manageFocus, announcementText, shouldDisableInlineStyles, routes, activeRoute, location, prevLocation, basepath, url, history, primary, a11y, disableInlineStyles, $location, $activeRoute, $prevLocation, $routes, $$scope, slots ]; } class Router extends SvelteComponent { constructor(options) { super(); init( this, options, instance$2, create_fragment$2, safe_not_equal, { basepath: 11, url: 12, history: 13, primary: 14, a11y: 15, disableInlineStyles: 16 }, null, [-1, -1] ); } } var Router$1 = Router; /** * Check if a component or hook have been created outside of a * context providing component * @param {number} componentId * @param {*} props * @param {string?} ctxKey * @param {number?} ctxProviderId */ function usePreflightCheck( componentId, props, ctxKey = ROUTER, ctxProviderId = ROUTER_ID, ) { const ctx = getContext(ctxKey); if (!ctx) { fail( componentId, label => `You cannot use ${label} outside of a ${createLabel(ctxProviderId)}.`, props, ); } } const toReadonly = ctx => { const { subscribe } = getContext(ctx); return { subscribe }; }; /** * Access the current location via a readable store. * @returns {import("svelte/store").Readable<{ pathname: string; search: string; hash: string; state: {}; }>} * * @example ```html <script> import { useLocation } from "svelte-navigator"; const location = useLocation(); $: console.log($location); // { // pathname: "/blog", // search: "?id=123", // hash: "#comments", // state: {} // } </script> ``` */ function useLocation() { usePreflightCheck(USE_LOCATION_ID); return toReadonly(LOCATION); } /** * @typedef {{ path: string; fullPath: string; uri: string; params: {}; }} RouteMatch */ /** * @typedef {import("svelte/store").Readable<RouteMatch|null>} RouteMatchStore */ /** * Access the history of top level Router. */ function useHistory() { const { history } = getContext(ROUTER); return history; } /** * Access the base of the parent Route. */ function useRouteBase() { const route = getContext(ROUTE); return route ? derived(route, _route => _route.base) : writable("/"); } /** * Resolve a given link relative to the current `Route` and the `Router`s `basepath`. * It is used under the hood in `Link` and `useNavigate`. * You can use it to manually resolve links, when using the `link` or `links` actions. * * @returns {(path: string) => string} * * @example ```html <script> import { link, useResolve } from "svelte-navigator"; const resolve = useResolve(); // `resolvedLink` will be resolved relative to its parent Route // and the Routers `basepath` const resolvedLink = resolve("relativePath"); </script> <a href={resolvedLink} use:link>Relative link</a> ``` */ function useResolve() { usePreflightCheck(USE_RESOLVE_ID); const routeBase = useRouteBase(); const { basepath: appBase } = getContext(ROUTER); /** * Resolves the path relative to the current route and basepath. * * @param {string} path The path to resolve * @returns {string} The resolved path */ const resolve = path => resolveLink(path, get(routeBase), appBase); return resolve; } /** * Resolve a given link relative to the current `Route` and the `Router`s `basepath`. * It is used under the hood in `Link` and `useNavigate`. * You can use it to manually resolve links, when using the `link` or `links` actions. * * @returns {import("svelte/store").Readable<string>} * * @example ```html <script> import { link, useResolvable } from "svelte-navigator"; // `resolvedLink` will be resolved relative to its parent Route // and the Routers `basepath`. const resolvedLink = useResolvable("relativePath"); </script> <a href={$resolvedLink} use:link>Relative link</a> ``` */ function useResolvable(path) { usePreflightCheck(USE_RESOLVABLE_ID); const routeBase = useRouteBase(); const { basepath: appBase } = getContext(ROUTER); return derived(routeBase, _routeBase => resolveLink(path, _routeBase, appBase), ); } /** * A hook, that returns a context-aware version of `navigate`. * It will automatically resolve the given link relative to the current Route. * It will also resolve a link against the `basepath` of the Router. * * @example ```html <!-- App.svelte --> <script> import { link, Route } from "svelte-navigator"; import RouteComponent from "./RouteComponent.svelte"; </script> <Router> <Route path="route1"> <RouteComponent /> </Route> <!-- ... --> </Router> <!-- RouteComponent.svelte --> <script> import { useNavigate } from "svelte-navigator"; const navigate = useNavigate(); </script> <button on:click="{() => navigate('relativePath')}"> go to /route1/relativePath </button> <button on:click="{() => navigate('/absolutePath')}"> go to /absolutePath </button> ``` * * @example ```html <!-- App.svelte --> <script> import { link, Route } from "svelte-navigator"; import RouteComponent from "./RouteComponent.svelte"; </script> <Router basepath="/base"> <Route path="route1"> <RouteComponent /> </Route> <!-- ... --> </Router> <!-- RouteComponent.svelte --> <script> import { useNavigate } from "svelte-navigator"; const navigate = useNavigate(); </script> <button on:click="{() => navigate('relativePath')}"> go to /base/route1/relativePath </button> <button on:click="{() => navigate('/absolutePath')}"> go to /base/absolutePath </button> ``` */ function useNavigate() { usePreflightCheck(USE_NAVIGATE_ID); const resolve = useResolve(); const { navigate } = useHistory(); /** * Navigate to a new route. * Resolves the link relative to the current route and basepath. * * @param {string|number} to The path to navigate to. * * If `to` is a number we will navigate to the stack entry index + `to` * (-> `navigate(-1)`, is equivalent to hitting the back button of the browser) * @param {Object} options * @param {*} [options.state] * @param {boolean} [options.replace=false] */ const navigateRelative = (to, options) => { // If to is a number, we navigate to the target stack entry via `history.go`. // Otherwise resolve the link const target = isNumber(to) ? to : resolve(to); return navigate(target, options); }; return navigateRelative; } /** * Use Svelte Navigators matching without needing to use a Route. * Returns a readable store with the potential match, * that changes, when the location changes. * * The provided path will be resolved relatively, * as you're used to with all paths in Svelte Navigator * * @param {string} path The path, to match against. * It works just like a Route path * @returns {RouteMatchStore} The matched route. * Returns `null`, when nothing could be matched * * @example ```html <script> import { useMatch } from "svelte-navigator"; const relativeMatch = useMatch("relative/path/:to/*somewhere"); const absoluteMatch = useMatch("/absolute/path/:to/*somewhere"); $: console.log($relativeMatch.params.to); $: console.log($absoluteMatch.params.somewhere); </script> ``` */ function useMatch(path) { usePreflightCheck(USE_MATCH_ID); const location = useLocation(); const resolve = useResolve(); const { basepath: appBase } = getContext(ROUTER); const resolvedPath = resolve(path); const { pathname: fullPath } = normalizeLocation( { pathname: resolvedPath }, appBase, ); return derived(location, loc => match({ fullPath, path }, loc.pathname)); } /** * Access the parent Routes matched params and wildcards * @returns {import("svelte/store").Readable<{ [param: string]: any; }>} A readable store containing the matched parameters and wildcards * * @example ```html <!-- Somewhere inside <Route path="user/:id/*splat" /> with a current url of "/myApp/user/123/pauls-profile" --> <script> import { useParams } from "svelte-navigator"; const params = useParams(); $: console.log($params); // -> { id: "123", splat: "pauls-profile" } </script> <h3>Welcome user {$params.id}! bleep bloop...</h3> ``` */ function useParams() { usePreflightCheck(USE_PARAMS_ID, null, ROUTE, ROUTE_ID); return toReadonly(ROUTE_PARAMS); } /** * Provide a custom element to focus, when the parent route is visited. * It returns the `registerFocus` function you can call manually with an * Element or use as a Svelte action via the `use` directive. * * @example ```html <!-- Using `registerFocus` as a Svelte action: --> <!-- Somewhere inside a Route --> <script> import { useFocus } from "svelte-navigator"; const registerFocus = useFocus(); </script> <h1>Don't worry about me...</h1> <p use:registerFocus>Here, look at me!</p> ``` * @example ```html <!-- Calling `registerFocus` manually: --> <!-- Somewhere inside a Route --> <script> import { onMount } from "svelte"; import { useFocus } from "svelte-navigator"; const registerFocus = useFocus(); let focusElement; onMount(() => registerFocus(focusElement)) </script> <h1>Don't worry about me...</h1> <p bind:this={focusElement}>Here, look at me!</p> ``` * @example ```html <!-- Using `registerFocus` asyncronously: --> <!-- Somewhere inside a Route --> <script> import { onMount } from "svelte"; import { useFocus } from "svelte-navigator"; const registerFocus = useFocus(); const lazyImport = import("./MyComponent.svelte").then(module => module.default); </script> {#await lazyImport then MyComponent} <MyComponent {registerFocus} /> {/await} <!-- MyComponent.svelte --> <script> export let registerFocus; </script> <h1 use:registerFocus>Hi there!</h1> ``` */ function useFocus() { usePreflightCheck(USE_FOCUS_ID, null, ROUTE, ROUTE_ID); const location = useLocation(); const focusElement = getContext(FOCUS_ELEM); let resolve; const unsubscribe = location.subscribe(() => { const lazyElement = new Promise(_resolve => { resolve = _resolve; }); focusElement.set(lazyElement); }); onDestroy(unsubscribe); return node => { let unmounted = false; const innerUnsubscribe = location.subscribe(() => { tick().then(() => { if (!unmounted) { resolve(node); } }); }); return { destroy() { unmounted = true; innerUnsubscribe(); }, }; }; } /* src\Route.svelte generated by Svelte v4.2.1 */ const get_default_slot_changes = dirty => ({ params: dirty & /*$params*/ 16, location: dirty & /*$location*/ 8 }); const get_default_slot_context = ctx => ({ params: isSSR ? get(/*params*/ ctx[10]) : /*$params*/ ctx[4], location: /*$location*/ ctx[3], navigate: /*navigate*/ ctx[11] }); // (98:0) {#if isActive} function create_if_block(ctx) { let router; let current; router = new Router$1({ props: { primary: /*primary*/ ctx[1], $$slots: { default: [create_default_slot] }, $$scope: { ctx } } }); return { c() { create_component(router.$$.fragment); }, m(target, anchor) { mount_component(router, target, anchor); current = true; }, p(ctx, dirty) { const router_changes = {}; if (dirty & /*primary*/ 2) router_changes.primary = /*primary*/ ctx[1]; if (dirty & /*$$scope, component, $location, $params, $$restProps*/ 528409) { router_changes.$$scope = { dirty, ctx }; } router.$set(router_changes); }, i(local) { if (current) return; transition_in(router.$$.fragment, local); current = true; }, o(local) { transition_out(router.$$.fragment, local); current = false; }, d(detaching) { destroy_component(router, detaching); } }; } // (114:2) {:else} function create_else_block(ctx) { let current; const default_slot_template = /*#slots*/ ctx[18].default; const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[19], get_default_slot_context); return { c() { if (default_slot) default_slot.c(); }, m(target, anchor) { if (default_slot) { default_slot.m(target, anchor); } current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && (!current || dirty & /*$$scope, $params, $location*/ 524312)) { update_slot_base( default_slot, default_slot_template, ctx, /*$$scope*/ ctx[19], !current ? get_all_dirty_from_scope(/*$$scope*/ ctx[19]) : get_slot_changes(default_slot_template, /*$$scope*/ ctx[19], dirty, get_default_slot_changes), get_default_slot_context ); } } }, i(local) { if (current) return; transition_in(default_slot, local); current = true; }, o(local) { transition_out(default_slot, local); current = false; }, d(detaching) { if (default_slot) default_slot.d(detaching); } }; } // (106:2) {#if component !== null} function create_if_block_1(ctx) { let switch_instance; let switch_instance_anchor; let current; const switch_instance_spread_levels = [ { location: /*$location*/ ctx[3] }, { navigate: /*navigate*/ ctx[11] }, isSSR ? get(/*params*/ ctx[10]) : /*$params*/ ctx[4], /*$$restProps*/ ctx[12] ]; var switch_value = /*component*/ ctx[0]; function switch_props(ctx, dirty) { let switch_instance_props = {}; if (dirty !== undefined && dirty & /*$location, navigate, params, $params, $$restProps*/ 7192) { switch_instance_props = get_spread_update(switch_instance_spread_levels, [ dirty & /*$location*/ 8 && { location: /*$location*/ ctx[3] }, dirty & /*navigate*/ 2048 && { navigate: /*navigate*/ ctx[11] }, dirty & /*params, $params*/ 1040 && get_spread_object(isSSR ? get(/*params*/ ctx[10]) : /*$params*/ ctx[4]), dirty & /*$$restProps*/ 4096 && get_spread_object(/*$$restProps*/ ctx[12]) ]); } else { for (let i = 0; i < switch_instance_spread_levels.length; i += 1) { switch_instance_props = assign(switch_instance_props, switch_instance_spread_levels[i]); } } return { props: switch_instance_props }; } if (switch_value) { switch_instance = c