zwischenstand
This commit is contained in:
50
vendor/svelte-routing/src/Link.svelte
vendored
Normal file
50
vendor/svelte-routing/src/Link.svelte
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import { HISTORY, LOCATION, ROUTER } from "./contexts.js";
|
||||
import { resolve, shouldNavigate } from "./utils.js";
|
||||
|
||||
export let to = "#";
|
||||
export let replace = false;
|
||||
export let state = {};
|
||||
export let getProps = () => ({});
|
||||
export let preserveScroll = false;
|
||||
|
||||
const location = getContext(LOCATION);
|
||||
const { base } = getContext(ROUTER);
|
||||
const { navigate } = getContext(HISTORY);
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let href, isPartiallyCurrent, isCurrent, props;
|
||||
$: href = resolve(to, $base.uri);
|
||||
$: isPartiallyCurrent = $location.pathname.startsWith(href);
|
||||
$: isCurrent = href === $location.pathname;
|
||||
$: ariaCurrent = isCurrent ? "page" : undefined;
|
||||
$: props = getProps({
|
||||
location: $location,
|
||||
href,
|
||||
isPartiallyCurrent,
|
||||
isCurrent,
|
||||
existingProps: $$restProps,
|
||||
});
|
||||
|
||||
const onClick = (event) => {
|
||||
dispatch("click", event);
|
||||
if (shouldNavigate(event)) {
|
||||
event.preventDefault();
|
||||
// Don't push another entry to the history stack when the user
|
||||
// clicks on a Link to the page they are currently on.
|
||||
const shouldReplace = $location.pathname === href || replace;
|
||||
navigate(href, { state, replace: shouldReplace, preserveScroll });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
aria-current={ariaCurrent}
|
||||
on:click={onClick}
|
||||
{...props}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot active={!!ariaCurrent} />
|
||||
</a>
|
||||
59
vendor/svelte-routing/src/Route.svelte
vendored
Normal file
59
vendor/svelte-routing/src/Route.svelte
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import { getContext, onDestroy } from "svelte";
|
||||
import { ROUTER } from "./contexts.js";
|
||||
import { canUseDOM } from "./utils.js";
|
||||
|
||||
export let path = "";
|
||||
export let component = null;
|
||||
|
||||
let routeParams = {};
|
||||
let routeProps = {};
|
||||
|
||||
const { registerRoute, unregisterRoute, activeRoute } = getContext(ROUTER);
|
||||
|
||||
const route = {
|
||||
path,
|
||||
// If no path prop is given, this Route will act as the default Route
|
||||
// that is rendered if no other Route in the Router is a match.
|
||||
default: path === "",
|
||||
};
|
||||
|
||||
$: if ($activeRoute && $activeRoute.route === route) {
|
||||
routeParams = $activeRoute.params;
|
||||
|
||||
const { component: c, path, ...rest } = $$props;
|
||||
routeProps = rest;
|
||||
|
||||
if (c) {
|
||||
// goja gives object instead of class
|
||||
if (
|
||||
c.toString().startsWith("class ") ||
|
||||
c.toString().includes("object")
|
||||
)
|
||||
component = c;
|
||||
else component = c();
|
||||
}
|
||||
|
||||
canUseDOM() && !$activeRoute.preserveScroll && window?.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
registerRoute(route);
|
||||
|
||||
onDestroy(() => {
|
||||
unregisterRoute(route);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $activeRoute && $activeRoute.route === route}
|
||||
{#if component}
|
||||
{#await component then resolvedComponent}
|
||||
<svelte:component
|
||||
this={resolvedComponent?.default || resolvedComponent}
|
||||
{...routeParams}
|
||||
{...routeProps}
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<slot params={routeParams} />
|
||||
{/if}
|
||||
{/if}
|
||||
145
vendor/svelte-routing/src/Router.svelte
vendored
Normal file
145
vendor/svelte-routing/src/Router.svelte
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
<script>
|
||||
import { getContext, onMount, setContext } from "svelte";
|
||||
import { derived, writable } from "svelte/store";
|
||||
import { HISTORY, LOCATION, ROUTER } from "./contexts.js";
|
||||
import { globalHistory } from "./history.js";
|
||||
import { combinePaths, pick } from "./utils.js";
|
||||
|
||||
export let basepath = "/";
|
||||
export let url = null;
|
||||
export let viewtransition = null;
|
||||
export let history = globalHistory;
|
||||
|
||||
const viewtransitionFn = (node, _, direction) => {
|
||||
const vt = viewtransition(direction);
|
||||
if (typeof vt?.fn === "function") return vt.fn(node, vt);
|
||||
else return vt;
|
||||
};
|
||||
|
||||
setContext(HISTORY, history);
|
||||
|
||||
const locationContext = getContext(LOCATION);
|
||||
const routerContext = getContext(ROUTER);
|
||||
|
||||
const routes = writable([]);
|
||||
const activeRoute = writable(null);
|
||||
let hasActiveRoute = false; // Used in SSR to synchronously set that a Route is active.
|
||||
|
||||
// If locationContext is not set, this is the topmost Router in the tree.
|
||||
// If the `url` prop is given we force the location to it.
|
||||
const location =
|
||||
locationContext || writable(url ? { pathname: url } : history.location);
|
||||
|
||||
// If routerContext is set, the routerBase of the parent Router
|
||||
// will be the base for this Router's descendants.
|
||||
// If routerContext is not set, the path and resolved uri will both
|
||||
// have the value of the basepath prop.
|
||||
const base = routerContext
|
||||
? routerContext.routerBase
|
||||
: writable({
|
||||
path: basepath,
|
||||
uri: basepath,
|
||||
});
|
||||
|
||||
const routerBase = derived([base, activeRoute], ([base, activeRoute]) => {
|
||||
// If there is no activeRoute, the routerBase will be identical to the base.
|
||||
if (!activeRoute) return base;
|
||||
|
||||
const { path: basepath } = base;
|
||||
const { route, uri } = activeRoute;
|
||||
// Remove the potential /* or /*splatname from
|
||||
// the end of the child Routes relative paths.
|
||||
const path = route.default ? basepath : route.path.replace(/\*.*$/, "");
|
||||
return { path, uri };
|
||||
});
|
||||
|
||||
const registerRoute = (route) => {
|
||||
const { path: basepath } = $base;
|
||||
let { path } = route;
|
||||
|
||||
// We store the original path in the _path property so we can reuse
|
||||
// it when the basepath changes. The only thing that matters is that
|
||||
// the route reference is intact, so mutation is fine.
|
||||
route._path = path;
|
||||
route.path = combinePaths(basepath, path);
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
// 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 = pick([route], $location.pathname);
|
||||
|
||||
if (matchingRoute) {
|
||||
activeRoute.set(matchingRoute);
|
||||
hasActiveRoute = true;
|
||||
}
|
||||
} else {
|
||||
routes.update((rs) => [...rs, route]);
|
||||
}
|
||||
};
|
||||
|
||||
const unregisterRoute = (route) => {
|
||||
routes.update((rs) => rs.filter((r) => r !== route));
|
||||
};
|
||||
|
||||
let preserveScroll = false;
|
||||
|
||||
// This reactive statement will update all the Routes' path when
|
||||
// the basepath changes.
|
||||
$: {
|
||||
const { path: basepath } = $base;
|
||||
routes.update((rs) =>
|
||||
rs.map((r) =>
|
||||
Object.assign(r, { path: combinePaths(basepath, r._path) })
|
||||
)
|
||||
);
|
||||
}
|
||||
// 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 ? { ...bestMatch, preserveScroll } : bestMatch
|
||||
);
|
||||
}
|
||||
|
||||
if (!locationContext) {
|
||||
// The topmost Router in the tree is responsible for updating
|
||||
// the location store and supplying it through context.
|
||||
onMount(() => {
|
||||
const unlisten = history.listen((event) => {
|
||||
preserveScroll = event.preserveScroll || false;
|
||||
location.set(event.location);
|
||||
});
|
||||
|
||||
return unlisten;
|
||||
});
|
||||
|
||||
setContext(LOCATION, location);
|
||||
}
|
||||
|
||||
setContext(ROUTER, {
|
||||
activeRoute,
|
||||
base,
|
||||
routerBase,
|
||||
registerRoute,
|
||||
unregisterRoute,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if viewtransition}
|
||||
{#key $location.pathname}
|
||||
<div in:viewtransitionFn out:viewtransitionFn>
|
||||
<slot
|
||||
route={$activeRoute && $activeRoute.uri}
|
||||
location={$location}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{:else}
|
||||
<slot route={$activeRoute && $activeRoute.uri} location={$location} />
|
||||
{/if}
|
||||
87
vendor/svelte-routing/src/actions.js
vendored
Normal file
87
vendor/svelte-routing/src/actions.js
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
import { navigate } from "./history.js";
|
||||
import { hostMatches, shouldNavigate } from "./utils.js";
|
||||
|
||||
/**
|
||||
* A link action that can be added to <a href=""> tags rather
|
||||
* than using the <Link> component.
|
||||
*
|
||||
* Example:
|
||||
* ```html
|
||||
* <a href="/post/{postId}" use:link>{post.title}</a>
|
||||
* ```
|
||||
*/
|
||||
const link = (node) => {
|
||||
const onClick = (event) => {
|
||||
const anchor = event.currentTarget;
|
||||
|
||||
if (
|
||||
(anchor.target === "" || anchor.target === "_self") &&
|
||||
hostMatches(anchor) &&
|
||||
shouldNavigate(event)
|
||||
) {
|
||||
event.preventDefault();
|
||||
navigate(anchor.pathname + anchor.search, {
|
||||
replace: anchor.hasAttribute("replace"),
|
||||
preserveScroll: anchor.hasAttribute("preserveScroll"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
node.addEventListener("click", onClick);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("click", onClick);
|
||||
},
|
||||
};
|
||||
};
|
||||
/**
|
||||
* An action to be added at a root element of your application to
|
||||
* capture all relative links and push them onto the history stack.
|
||||
*
|
||||
* Example:
|
||||
* ```html
|
||||
* <div use:links>
|
||||
* <Router>
|
||||
* <Route path="/" component={Home} />
|
||||
* <Route path="/p/:projectId/:docId?" component={ProjectScreen} />
|
||||
* {#each projects as project}
|
||||
* <a href="/p/{project.id}">{project.title}</a>
|
||||
* {/each}
|
||||
* </Router>
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
const links = (node) => {
|
||||
const findClosest = (tagName, el) => {
|
||||
while (el && el.tagName !== tagName) el = el.parentNode;
|
||||
return el;
|
||||
};
|
||||
|
||||
const onClick = (event) => {
|
||||
const anchor = findClosest("A", event.target);
|
||||
if (
|
||||
anchor &&
|
||||
(anchor.target === "" || anchor.target === "_self") &&
|
||||
hostMatches(anchor) &&
|
||||
shouldNavigate(event) &&
|
||||
!anchor.hasAttribute("noroute")
|
||||
) {
|
||||
event.preventDefault();
|
||||
navigate(anchor.pathname + anchor.search, {
|
||||
replace: anchor.hasAttribute("replace"),
|
||||
preserveScroll: anchor.hasAttribute("preserveScroll"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
node.addEventListener("click", onClick);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener("click", onClick);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { link, links };
|
||||
9
vendor/svelte-routing/src/contexts.js
vendored
Normal file
9
vendor/svelte-routing/src/contexts.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { getContext } from "svelte";
|
||||
|
||||
export const LOCATION = {};
|
||||
export const ROUTER = {};
|
||||
export const HISTORY = {};
|
||||
|
||||
export const useLocation = () => getContext(LOCATION);
|
||||
export const useRouter = () => getContext(ROUTER);
|
||||
export const useHistory = () => getContext(HISTORY);
|
||||
109
vendor/svelte-routing/src/history.js
vendored
Normal file
109
vendor/svelte-routing/src/history.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/history.js
|
||||
* https://github.com/reach/router/blob/master/LICENSE
|
||||
*/
|
||||
import { canUseDOM } from "./utils";
|
||||
|
||||
const getLocation = (source) => {
|
||||
return {
|
||||
...source.location,
|
||||
state: source.history.state,
|
||||
key: (source.history.state && source.history.state.key) || "initial",
|
||||
};
|
||||
};
|
||||
const createHistory = (source) => {
|
||||
const listeners = [];
|
||||
let location = getLocation(source);
|
||||
|
||||
return {
|
||||
get location() {
|
||||
return location;
|
||||
},
|
||||
|
||||
listen(listener) {
|
||||
listeners.push(listener);
|
||||
|
||||
const popstateListener = () => {
|
||||
location = getLocation(source);
|
||||
listener({ location, action: "POP" });
|
||||
};
|
||||
|
||||
source.addEventListener("popstate", popstateListener);
|
||||
|
||||
return () => {
|
||||
source.removeEventListener("popstate", popstateListener);
|
||||
const index = listeners.indexOf(listener);
|
||||
listeners.splice(index, 1);
|
||||
};
|
||||
},
|
||||
|
||||
navigate(
|
||||
to,
|
||||
{
|
||||
state,
|
||||
replace = false,
|
||||
preserveScroll = false,
|
||||
blurActiveElement = true,
|
||||
} = {}
|
||||
) {
|
||||
state = { ...state, key: Date.now() + "" };
|
||||
// try...catch iOS Safari limits to 100 pushState calls
|
||||
try {
|
||||
if (replace) source.history.replaceState(state, "", to);
|
||||
else source.history.pushState(state, "", to);
|
||||
} catch (e) {
|
||||
source.location[replace ? "replace" : "assign"](to);
|
||||
}
|
||||
location = getLocation(source);
|
||||
listeners.forEach((listener) =>
|
||||
listener({ location, action: "PUSH", preserveScroll })
|
||||
);
|
||||
if (typeof document !== "undefined")
|
||||
if (blurActiveElement) document.activeElement.blur();
|
||||
},
|
||||
};
|
||||
};
|
||||
// Stores history entries in memory for testing or other platforms like Native
|
||||
const createMemorySource = (initialPathname = "/") => {
|
||||
let index = 0;
|
||||
const stack = [{ pathname: initialPathname, search: "" }];
|
||||
const states = [];
|
||||
|
||||
return {
|
||||
get location() {
|
||||
return stack[index];
|
||||
},
|
||||
addEventListener(name, fn) {},
|
||||
removeEventListener(name, fn) {},
|
||||
history: {
|
||||
get entries() {
|
||||
return stack;
|
||||
},
|
||||
get index() {
|
||||
return index;
|
||||
},
|
||||
get state() {
|
||||
return states[index];
|
||||
},
|
||||
pushState(state, _, uri) {
|
||||
const [pathname, search = ""] = uri.split("?");
|
||||
index++;
|
||||
stack.push({ pathname, search });
|
||||
states.push(state);
|
||||
},
|
||||
replaceState(state, _, uri) {
|
||||
const [pathname, search = ""] = uri.split("?");
|
||||
stack[index] = { pathname, search };
|
||||
states[index] = state;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
// Global history uses window.history as the source if available,
|
||||
// otherwise a memory history
|
||||
const globalHistory = createHistory(
|
||||
canUseDOM() ? window : createMemorySource()
|
||||
);
|
||||
const { navigate } = globalHistory;
|
||||
|
||||
export { globalHistory, navigate, createHistory, createMemorySource };
|
||||
6
vendor/svelte-routing/src/index.js
vendored
Normal file
6
vendor/svelte-routing/src/index.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as Link } from "./Link.svelte";
|
||||
export { default as Route } from "./Route.svelte";
|
||||
export { default as Router } from "./Router.svelte";
|
||||
export { link, links } from "./actions.js";
|
||||
export * from "./contexts.js";
|
||||
export { navigate } from "./history.js";
|
||||
278
vendor/svelte-routing/src/utils.js
vendored
Normal file
278
vendor/svelte-routing/src/utils.js
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/utils.js
|
||||
* https://github.com/reach/router/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
const PARAM = /^:(.+)/;
|
||||
const SEGMENT_POINTS = 4;
|
||||
const STATIC_POINTS = 3;
|
||||
const DYNAMIC_POINTS = 2;
|
||||
const SPLAT_PENALTY = 1;
|
||||
const ROOT_POINTS = 1;
|
||||
|
||||
/**
|
||||
* Split up the URI into segments delimited by `/`
|
||||
* Strip starting/ending `/`
|
||||
* @param {string} uri
|
||||
* @return {string[]}
|
||||
*/
|
||||
const segmentize = (uri) => uri.replace(/(^\/+|\/+$)/g, "").split("/");
|
||||
/**
|
||||
* Strip `str` of potential start and end `/`
|
||||
* @param {string} string
|
||||
* @return {string}
|
||||
*/
|
||||
const stripSlashes = (string) => string.replace(/(^\/+|\/+$)/g, "");
|
||||
/**
|
||||
* Score a route depending on how its individual segments look
|
||||
* @param {object} route
|
||||
* @param {number} index
|
||||
* @return {object}
|
||||
*/
|
||||
const rankRoute = (route, index) => {
|
||||
const score = route.default
|
||||
? 0
|
||||
: segmentize(route.path).reduce((score, segment) => {
|
||||
score += SEGMENT_POINTS;
|
||||
|
||||
if (segment === "") {
|
||||
score += ROOT_POINTS;
|
||||
} else if (PARAM.test(segment)) {
|
||||
score += DYNAMIC_POINTS;
|
||||
} else if (segment[0] === "*") {
|
||||
score -= SEGMENT_POINTS + SPLAT_PENALTY;
|
||||
} else {
|
||||
score += STATIC_POINTS;
|
||||
}
|
||||
|
||||
return score;
|
||||
}, 0);
|
||||
|
||||
return { route, score, index };
|
||||
};
|
||||
/**
|
||||
* Give a score to all routes and sort them on that
|
||||
* If two routes have the exact same score, we go by index instead
|
||||
* @param {object[]} routes
|
||||
* @return {object[]}
|
||||
*/
|
||||
const rankRoutes = (routes) =>
|
||||
routes
|
||||
.map(rankRoute)
|
||||
.sort((a, b) =>
|
||||
a.score < b.score ? 1 : a.score > b.score ? -1 : 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
|
||||
*
|
||||
* { path, default, value }
|
||||
*
|
||||
* And a returned match looks like:
|
||||
*
|
||||
* { route, params, uri }
|
||||
*
|
||||
* @param {object[]} routes
|
||||
* @param {string} uri
|
||||
* @return {?object}
|
||||
*/
|
||||
const pick = (routes, uri) => {
|
||||
let match;
|
||||
let default_;
|
||||
|
||||
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].route;
|
||||
let missed = false;
|
||||
|
||||
if (route.default) {
|
||||
default_ = {
|
||||
route,
|
||||
params: {},
|
||||
uri,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const routeSegments = segmentize(route.path);
|
||||
const params = {};
|
||||
const max = Math.max(uriSegments.length, routeSegments.length);
|
||||
let index = 0;
|
||||
|
||||
for (; index < max; index++) {
|
||||
const routeSegment = routeSegments[index];
|
||||
const uriSegment = uriSegments[index];
|
||||
|
||||
if (routeSegment && routeSegment[0] === "*") {
|
||||
// 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 (typeof uriSegment === "undefined") {
|
||||
// URI is shorter than the route, no match
|
||||
// uri: /users
|
||||
// route: /users/:userId
|
||||
missed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const dynamicMatch = PARAM.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) {
|
||||
match = {
|
||||
route,
|
||||
params,
|
||||
uri: "/" + uriSegments.slice(0, index).join("/"),
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return match || default_ || null;
|
||||
};
|
||||
/**
|
||||
* 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}` : "");
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
const resolve = (to, base) => {
|
||||
// /foo/bar, /baz/qux => /foo/bar
|
||||
if (to.startsWith("/")) 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 (!toSegments[0].startsWith(".")) {
|
||||
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);
|
||||
};
|
||||
/**
|
||||
* Combines the `basepath` and the `path` into one path.
|
||||
* @param {string} basepath
|
||||
* @param {string} path
|
||||
*/
|
||||
const combinePaths = (basepath, path) =>
|
||||
`${stripSlashes(
|
||||
path === "/"
|
||||
? basepath
|
||||
: `${stripSlashes(basepath)}/${stripSlashes(path)}`
|
||||
)}/`;
|
||||
/**
|
||||
* Decides whether a given `event` should result in a navigation or not.
|
||||
* @param {object} event
|
||||
*/
|
||||
const shouldNavigate = (event) =>
|
||||
!event.defaultPrevented &&
|
||||
event.button === 0 &&
|
||||
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
|
||||
|
||||
// svelte seems to kill anchor.host value in ie11, so fall back to checking href
|
||||
const hostMatches = (anchor) => {
|
||||
const host = location.host;
|
||||
return (
|
||||
anchor.host === host ||
|
||||
anchor.href.indexOf(`https://${host}`) === 0 ||
|
||||
anchor.href.indexOf(`http://${host}`) === 0
|
||||
);
|
||||
};
|
||||
|
||||
const canUseDOM = () =>
|
||||
typeof window !== "undefined" &&
|
||||
"document" in window &&
|
||||
"location" in window;
|
||||
|
||||
export {
|
||||
stripSlashes,
|
||||
pick,
|
||||
resolve,
|
||||
combinePaths,
|
||||
shouldNavigate,
|
||||
hostMatches,
|
||||
canUseDOM,
|
||||
};
|
||||
Reference in New Issue
Block a user