@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
311 lines (310 loc) • 10.2 kB
JavaScript
import { useHydrated } from "./ClientOnly.js";
import { useRouter } from "./useRouter.js";
import { useRouterState } from "./useRouterState.js";
import { useIntersectionObserver } from "./utils.js";
import { deepEqual, exactPathTest, functionalUpdate, isDangerousProtocol, preloadWarning, removeTrailingSlash } from "@tanstack/router-core";
import { Dynamic, createComponent, insert, memo, mergeProps, spread, template } from "solid-js/web";
import * as Solid from "solid-js";
import { mergeRefs } from "@solid-primitives/refs";
import { isServer } from "@tanstack/router-core/isServer";
//#region src/link.tsx
var _tmpl$ = /* @__PURE__ */ template(`<svg><a>`);
var timeoutMap = /* @__PURE__ */ new WeakMap();
function useLinkProps(options) {
const router = useRouter();
const [isTransitioning, setIsTransitioning] = Solid.createSignal(false);
const shouldHydrateHash = !isServer && !!router.options.ssr;
const hasHydrated = useHydrated();
let hasRenderFetched = false;
const [local, rest] = Solid.splitProps(Solid.mergeProps({
activeProps: () => ({ class: "active" }),
inactiveProps: () => ({})
}, options), [
"activeProps",
"inactiveProps",
"activeOptions",
"to",
"preload",
"preloadDelay",
"hashScrollIntoView",
"replace",
"startTransition",
"resetScroll",
"viewTransition",
"target",
"disabled",
"style",
"class",
"onClick",
"onBlur",
"onFocus",
"onMouseEnter",
"onMouseLeave",
"onMouseOver",
"onMouseOut",
"onTouchStart",
"ignoreBlocker"
]);
const [_, propsSafeToSpread] = Solid.splitProps(rest, [
"params",
"search",
"hash",
"state",
"mask",
"reloadDocument",
"unsafeRelative"
]);
const currentLocation = useRouterState({ select: (s) => s.location });
const buildLocationKey = useRouterState({ select: (s) => {
const leaf = s.matches[s.matches.length - 1];
return {
search: leaf?.search,
hash: s.location.hash,
path: leaf?.pathname
};
} });
const from = options.from;
const _options = () => {
return {
...options,
from
};
};
const next = Solid.createMemo(() => {
buildLocationKey();
return router.buildLocation(_options());
});
const hrefOption = Solid.createMemo(() => {
if (_options().disabled) return void 0;
const location = next().maskedLocation ?? next();
const publicHref = location.publicHref;
if (location.external) return {
href: publicHref,
external: true
};
return {
href: router.history.createHref(publicHref) || "/",
external: false
};
});
const externalLink = Solid.createMemo(() => {
const _href = hrefOption();
if (_href?.external) {
if (isDangerousProtocol(_href.href, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${_href.href}`);
return;
}
return _href.href;
}
const to = _options().to;
if (typeof to === "string" && to.charCodeAt(0) === 47 && to.charCodeAt(1) !== 47) return void 0;
try {
new URL(to);
if (isDangerousProtocol(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`);
return;
}
return to;
} catch {}
});
const preload = Solid.createMemo(() => {
if (_options().reloadDocument || externalLink()) return false;
return local.preload ?? router.options.defaultPreload;
});
const preloadDelay = () => local.preloadDelay ?? router.options.defaultPreloadDelay ?? 0;
const isActive = Solid.createMemo(() => {
if (externalLink()) return false;
if (local.activeOptions?.exact) {
if (!exactPathTest(currentLocation().pathname, next().pathname, router.basepath)) return false;
} else {
const currentPathSplit = removeTrailingSlash(currentLocation().pathname, router.basepath).split("/");
if (!(removeTrailingSlash(next()?.pathname, router.basepath)?.split("/"))?.every((d, i) => d === currentPathSplit[i])) return false;
}
if (local.activeOptions?.includeSearch ?? true) {
if (!deepEqual(currentLocation().search, next().search, {
partial: !local.activeOptions?.exact,
ignoreUndefined: !local.activeOptions?.explicitUndefined
})) return false;
}
if (local.activeOptions?.includeHash) return (shouldHydrateHash && !hasHydrated() ? "" : currentLocation().hash) === next().hash;
return true;
});
const doPreload = () => router.preloadRoute(_options()).catch((err) => {
console.warn(err);
console.warn(preloadWarning);
});
const preloadViewportIoCallback = (entry) => {
if (entry?.isIntersecting) doPreload();
};
const [ref, setRef] = Solid.createSignal(null);
useIntersectionObserver(ref, preloadViewportIoCallback, { rootMargin: "100px" }, { disabled: !!local.disabled || !(preload() === "viewport") });
Solid.createEffect(() => {
if (hasRenderFetched) return;
if (!local.disabled && preload() === "render") {
doPreload();
hasRenderFetched = true;
}
});
if (externalLink()) return Solid.mergeProps(propsSafeToSpread, {
ref: mergeRefs(setRef, _options().ref),
href: externalLink()
}, Solid.splitProps(local, [
"target",
"disabled",
"style",
"class",
"onClick",
"onBlur",
"onFocus",
"onMouseEnter",
"onMouseLeave",
"onMouseOut",
"onMouseOver",
"onTouchStart"
])[0]);
const handleClick = (e) => {
const elementTarget = e.currentTarget.getAttribute("target");
const effectiveTarget = local.target !== void 0 ? local.target : elementTarget;
if (!local.disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) {
e.preventDefault();
setIsTransitioning(true);
const unsub = router.subscribe("onResolved", () => {
unsub();
setIsTransitioning(false);
});
router.navigate({
..._options(),
replace: local.replace,
resetScroll: local.resetScroll,
hashScrollIntoView: local.hashScrollIntoView,
startTransition: local.startTransition,
viewTransition: local.viewTransition,
ignoreBlocker: local.ignoreBlocker
});
}
};
const enqueueIntentPreload = (e) => {
if (local.disabled || preload() !== "intent") return;
if (!preloadDelay()) {
doPreload();
return;
}
const eventTarget = e.currentTarget || e.target;
if (!eventTarget || timeoutMap.has(eventTarget)) return;
timeoutMap.set(eventTarget, setTimeout(() => {
timeoutMap.delete(eventTarget);
doPreload();
}, preloadDelay()));
};
const handleTouchStart = (_) => {
if (local.disabled || preload() !== "intent") return;
doPreload();
};
const handleLeave = (e) => {
if (local.disabled) return;
const eventTarget = e.currentTarget || e.target;
if (eventTarget) {
const id = timeoutMap.get(eventTarget);
clearTimeout(id);
timeoutMap.delete(eventTarget);
}
};
/** Call a JSX.EventHandlerUnion with the event. */
function callHandler(event, handler) {
if (handler) if (typeof handler === "function") handler(event);
else handler[0](handler[1], event);
return event.defaultPrevented;
}
function composeEventHandlers(handlers) {
return (event) => {
for (const handler of handlers) callHandler(event, handler);
};
}
const resolvedActiveProps = () => isActive() ? functionalUpdate(local.activeProps, {}) ?? {} : {};
const resolvedInactiveProps = () => isActive() ? {} : functionalUpdate(local.inactiveProps, {});
const resolvedClassName = () => [
local.class,
resolvedActiveProps().class,
resolvedInactiveProps().class
].filter(Boolean).join(" ");
const resolvedStyle = () => ({
...local.style,
...resolvedActiveProps().style,
...resolvedInactiveProps().style
});
return Solid.mergeProps(propsSafeToSpread, resolvedActiveProps, resolvedInactiveProps, () => {
return {
href: hrefOption()?.href,
ref: mergeRefs(setRef, _options().ref),
onClick: composeEventHandlers([local.onClick, handleClick]),
onBlur: composeEventHandlers([local.onBlur, handleLeave]),
onFocus: composeEventHandlers([local.onFocus, enqueueIntentPreload]),
onMouseEnter: composeEventHandlers([local.onMouseEnter, enqueueIntentPreload]),
onMouseOver: composeEventHandlers([local.onMouseOver, enqueueIntentPreload]),
onMouseLeave: composeEventHandlers([local.onMouseLeave, handleLeave]),
onMouseOut: composeEventHandlers([local.onMouseOut, handleLeave]),
onTouchStart: composeEventHandlers([local.onTouchStart, handleTouchStart]),
disabled: !!local.disabled,
target: local.target,
...(() => {
const s = resolvedStyle();
return Object.keys(s).length ? { style: s } : {};
})(),
...(() => {
const c = resolvedClassName();
return c ? { class: c } : {};
})(),
...local.disabled && {
role: "link",
"aria-disabled": true
},
...isActive() && {
"data-status": "active",
"aria-current": "page"
},
...isTransitioning() && { "data-transitioning": "transitioning" }
};
});
}
function createLink(Comp) {
return (props) => createComponent(Link, mergeProps(props, { _asChild: Comp }));
}
var Link = (props) => {
const [local, rest] = Solid.splitProps(props, ["_asChild", "children"]);
const [_, linkProps] = Solid.splitProps(useLinkProps(rest), ["type"]);
const children = Solid.createMemo(() => {
const ch = local.children;
if (typeof ch === "function") return ch({
get isActive() {
return linkProps["data-status"] === "active";
},
get isTransitioning() {
return linkProps["data-transitioning"] === "transitioning";
}
});
return ch;
});
if (local._asChild === "svg") {
const [_, svgLinkProps] = Solid.splitProps(linkProps, ["class"]);
return (() => {
var _el$ = _tmpl$(), _el$2 = _el$.firstChild;
spread(_el$2, svgLinkProps, false, true);
insert(_el$2, children);
return _el$;
})();
}
return createComponent(Dynamic, mergeProps({ get component() {
return memo(() => !!local._asChild)() ? local._asChild : "a";
} }, linkProps, { get children() {
return children();
} }));
};
function isCtrlEvent(e) {
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
}
var linkOptions = (options) => {
return options;
};
//#endregion
export { Link, createLink, linkOptions, useLinkProps };
//# sourceMappingURL=link.js.map