UNPKG

mcrm-svelte-navigator

Version:
1,651 lines (1,437 loc) 78.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('svelte/internal'), require('svelte'), require('svelte/store')) : typeof define === 'function' && define.amd ? define(['exports', 'svelte/internal', 'svelte', 'svelte/store'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SvelteNavigator = {}, global.internal, global.svelte, global.store)); })(this, (function (exports, internal, svelte, store) { 'use strict'; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // 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 => _objectSpread2({ "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) { let filterFalsy = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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() { const joinFragment = fragment => segmentize(fragment, true).join("/"); for (var _len = arguments.length, pathFragments = new Array(_len), _key = 0; _key < _len; _key++) { pathFragments[_key] = arguments[_key]; } const joinedSegments = pathFragments.map(joinFragment).join("/"); return normalizePath(joinedSegments); } // 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 => function () { return handler(createMessage(...arguments)); }; 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 => _objectSpread2(_objectSpread2({}, 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; } const POP = "POP"; const PUSH = "PUSH"; const REPLACE = "REPLACE"; function getLocation(source) { return _objectSpread2(_objectSpread2({}, 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 = function () { let listenerFns = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : listeners; return 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 = _objectSpread2(_objectSpread2({}, 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 _objectSpread2(_objectSpread2({}, parsePath(uri)), {}, { state }); } // Stores history entries in memory for testing or other platforms like Native function createMemorySource() { let initialPathname = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "/"; 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; // 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) { let parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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(store.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 svelte.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 }, store.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;"; 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 = internal.assign(div_data, div_levels[i]); } return { c() { div = internal.element("div"); t = internal.text( /*$announcementText*/ ctx[0]); internal.set_attributes(div, div_data); }, m(target, anchor) { internal.insert(target, div, anchor); internal.append(div, t); }, p(ctx, dirty) { if (dirty[0] & /*$announcementText*/ 1) internal.set_data_maybe_contenteditable(t, /*$announcementText*/ ctx[0], div_data['contenteditable']); }, d(detaching) { if (detaching) { internal.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 = internal.assign(div_data, div_levels[i]); } const default_slot_template = /*#slots*/ ctx[22].default; const default_slot = internal.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 = internal.element("div"); t0 = internal.space(); if (default_slot) default_slot.c(); t1 = internal.space(); if (if_block) if_block.c(); if_block_anchor = internal.empty(); internal.set_attributes(div, div_data); }, m(target, anchor) { internal.insert(target, div, anchor); internal.insert(target, t0, anchor); if (default_slot) { default_slot.m(target, anchor); } internal.insert(target, t1, anchor); if (if_block) if_block.m(target, anchor); internal.insert(target, if_block_anchor, anchor); current = true; }, p(ctx, dirty) { if (default_slot) { if (default_slot.p && (!current || dirty[0] & /*$$scope*/ 2097152)) { internal.update_slot_base(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[21], !current ? internal.get_all_dirty_from_scope( /*$$scope*/ ctx[21]) : internal.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; internal.transition_in(default_slot, local); current = true; }, o(local) { internal.transition_out(default_slot, local); current = false; }, d(detaching) { if (detaching) { internal.detach(div); internal.detach(t0); internal.detach(t1); internal.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 = _objectSpread2({ 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 = svelte.getContext(LOCATION); const routerContext = svelte.getContext(ROUTER); const isTopLevelRouter = !locationContext; const routerId = createId$1(); const manageFocus = primary && !(routerContext && !routerContext.manageFocus); const announcementText = store.writable(""); internal.component_subscribe($$self, announcementText, value => $$invalidate(0, $announcementText = value)); const shouldDisableInlineStyles = routerContext ? routerContext.disableInlineStyles : disableInlineStyles; const routes = store.writable([]); internal.component_subscribe($$self, routes, value => $$invalidate(20, $routes = value)); const activeRoute = store.writable(null); internal.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 ? store.writable(getInitialLocation()) : locationContext; internal.component_subscribe($$self, location, value => $$invalidate(17, $location = value)); const prevLocation = store.writable($location); internal.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. svelte.onMount(() => { const unlisten = history.listen(changedHistory => { const normalizedLocation = normalizeLocation(changedHistory.location, normalizedBasepath); prevLocation.set($location); location.set(normalizedLocation); }); return unlisten; }); svelte.setContext(LOCATION, location); } svelte.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 internal.SvelteComponent { constructor(options) { super(); internal.init(this, options, instance$2, create_fragment$2, internal.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) { let ctxKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ROUTER; let ctxProviderId = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : ROUTER_ID; const ctx = svelte.getContext(ctxKey); if (!ctx) { fail(componentId, label => "You cannot use " + label + " outside of a " + createLabel(ctxProviderId) + ".", props); } } const toReadonly = ctx => { const { subscribe } = svelte.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 } = svelte.getContext(ROUTER); return history; } /** * Access the base of the parent Route. */ function useRouteBase() { const route = svelte.getContext(ROUTE); return route ? store.derived(route, _route => _route.base) : store.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 } = svelte.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, store.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 } = svelte.getContext(ROUTER); return store.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 } = svelte.getContext(ROUTER); const resolvedPath = resolve(path); const { pathname: fullPath } = normalizeLocation({ pathname: resolvedPath }, appBase); return store.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