@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
346 lines (345 loc) • 11.4 kB
JavaScript
import { useHydrated } from "./ClientOnly.js";
import { useRouter } from "./useRouter.js";
import { useIntersectionObserver } from "./utils.js";
import { deepEqual, exactPathTest, functionalUpdate, isDangerousProtocol, preloadWarning, removeTrailingSlash } from "@tanstack/router-core";
import { Dynamic, createComponent, insert, 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>`), _tmpl$2 = /* @__PURE__ */ template(`<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: STATIC_ACTIVE_PROPS_GET,
inactiveProps: STATIC_INACTIVE_PROPS_GET
}, 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 = Solid.createMemo(() => router.stores.location.state, void 0, { equals: (prev, next) => prev.href === next.href });
const _options = () => options;
const next = Solid.createMemo(() => {
const options = {
_fromLocation: currentLocation(),
..._options()
};
return Solid.untrack(() => 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 (isSafeInternal(to)) return void 0;
if (typeof to !== "string" || to.indexOf(":") === -1) 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;
const activeOptions = local.activeOptions;
const current = currentLocation();
const nextLocation = next();
if (activeOptions?.exact) {
if (!exactPathTest(current.pathname, nextLocation.pathname, router.basepath)) return false;
} else {
const currentPath = removeTrailingSlash(current.pathname, router.basepath);
const nextPath = removeTrailingSlash(nextLocation.pathname, router.basepath);
if (!(currentPath.startsWith(nextPath) && (currentPath.length === nextPath.length || currentPath[nextPath.length] === "/"))) return false;
}
if (activeOptions?.includeSearch ?? true) {
if (!deepEqual(current.search, nextLocation.search, {
partial: !activeOptions?.exact,
ignoreUndefined: !activeOptions?.explicitUndefined
})) return false;
}
if (activeOptions?.includeHash) return (shouldHydrateHash && !hasHydrated() ? "" : current.hash) === nextLocation.hash;
return true;
});
const doPreload = () => router.preloadRoute({
..._options(),
_builtLocation: next()
}).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);
}
};
const simpleStyling = Solid.createMemo(() => local.activeProps === STATIC_ACTIVE_PROPS_GET && local.inactiveProps === STATIC_INACTIVE_PROPS_GET && local.class === void 0 && local.style === void 0);
const onClick = createComposedHandler(() => local.onClick, handleClick);
const onBlur = createComposedHandler(() => local.onBlur, handleLeave);
const onFocus = createComposedHandler(() => local.onFocus, enqueueIntentPreload);
const onMouseEnter = createComposedHandler(() => local.onMouseEnter, enqueueIntentPreload);
const onMouseOver = createComposedHandler(() => local.onMouseOver, enqueueIntentPreload);
const onMouseLeave = createComposedHandler(() => local.onMouseLeave, handleLeave);
const onMouseOut = createComposedHandler(() => local.onMouseOut, handleLeave);
const onTouchStart = createComposedHandler(() => local.onTouchStart, handleTouchStart);
const resolvedProps = Solid.createMemo(() => {
const active = isActive();
const base = {
href: hrefOption()?.href,
ref: mergeRefs(setRef, _options().ref),
onClick,
onBlur,
onFocus,
onMouseEnter,
onMouseOver,
onMouseLeave,
onMouseOut,
onTouchStart,
disabled: !!local.disabled,
target: local.target,
...local.disabled && STATIC_DISABLED_PROPS,
...isTransitioning() && STATIC_TRANSITIONING_ATTRIBUTES
};
if (simpleStyling()) return {
...base,
...active && STATIC_DEFAULT_ACTIVE_ATTRIBUTES
};
const activeProps = active ? functionalUpdate(local.activeProps, {}) ?? EMPTY_OBJECT : EMPTY_OBJECT;
const inactiveProps = active ? EMPTY_OBJECT : functionalUpdate(local.inactiveProps, {});
const style = {
...local.style,
...activeProps.style,
...inactiveProps.style
};
const className = [
local.class,
activeProps.class,
inactiveProps.class
].filter(Boolean).join(" ");
return {
...activeProps,
...inactiveProps,
...base,
...Object.keys(style).length ? { style } : void 0,
...className ? { class: className } : void 0,
...active && STATIC_ACTIVE_ATTRIBUTES
};
});
return Solid.mergeProps(propsSafeToSpread, resolvedProps);
}
var STATIC_ACTIVE_PROPS = { class: "active" };
var STATIC_ACTIVE_PROPS_GET = () => STATIC_ACTIVE_PROPS;
var EMPTY_OBJECT = {};
var STATIC_INACTIVE_PROPS_GET = () => EMPTY_OBJECT;
var STATIC_DEFAULT_ACTIVE_ATTRIBUTES = {
class: "active",
"data-status": "active",
"aria-current": "page"
};
var STATIC_DISABLED_PROPS = {
role: "link",
"aria-disabled": true
};
var STATIC_ACTIVE_ATTRIBUTES = {
"data-status": "active",
"aria-current": "page"
};
var STATIC_TRANSITIONING_ATTRIBUTES = { "data-transitioning": "transitioning" };
/** Call a JSX.EventHandlerUnion with the event. */
function callHandler(event, handler) {
if (typeof handler === "function") handler(event);
else handler[0](handler[1], event);
return event.defaultPrevented;
}
function createComposedHandler(getHandler, fallback) {
return (event) => {
const handler = getHandler();
if (!handler || !callHandler(event, handler)) fallback(event);
};
}
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$;
})();
}
if (!local._asChild) return (() => {
var _el$3 = _tmpl$2();
spread(_el$3, linkProps, false, true);
insert(_el$3, children);
return _el$3;
})();
return createComponent(Dynamic, mergeProps({ get component() {
return local._asChild;
} }, linkProps, { get children() {
return children();
} }));
};
function isCtrlEvent(e) {
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
}
function isSafeInternal(to) {
if (typeof to !== "string") return false;
const zero = to.charCodeAt(0);
if (zero === 47) return to.charCodeAt(1) !== 47;
return zero === 46;
}
var linkOptions = (options) => {
return options;
};
//#endregion
export { Link, createLink, linkOptions, useLinkProps };
//# sourceMappingURL=link.js.map