@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
323 lines (322 loc) • 11.1 kB
JavaScript
import { useHydrated } from "./ClientOnly.js";
import { useRouter } from "./useRouter.js";
import { nearestMatchContext } from "./matchContext.js";
import { useIntersectionObserver } from "./utils.js";
import { _isRouteDeparting, deepEqual, functionalUpdate, getUrlScheme, hasKeys, 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 { isServer } from "@tanstack/router-core/isServer";
//#region src/link.tsx
var _tmpl$ = /*#__PURE__*/ template(`<svg><a>`);
var _tmpl$2 = /*#__PURE__*/ template(`<a>`);
function useLinkProps(options) {
return createLinkProps(options, LINK_OPTION_KEYS);
}
function createLinkProps(options, ownKeys) {
const router = useRouter();
const [, propsSafeToSpread] = Solid.splitProps(options, ownKeys);
const routeId = Solid.useContext(nearestMatchContext)[0]();
const currentLocation = isServer ?? router.isServer ? () => router.stores.location.get() : Solid.createMemo((prev) => {
const location = router.stores.location.get();
return prev && _isRouteDeparting(router, routeId, location) ? prev : location;
});
const getDest = () => {
const dest = {};
for (const key of NAVIGATION_KEYS) {
const value = dest[key] = options[key];
if (isStore(value) || key === "mask" && value && Object.values(value).some(isStore)) currentLocation();
}
return dest;
};
const dest = isServer ?? router.isServer ? getDest : Solid.createMemo(getDest);
const hasHydrated = !(isServer ?? router.isServer) && router.options.ssr ? useHydrated() : void 0;
const getLinkState = (prev) => {
const location = currentLocation();
const to = options.to;
const disabled = options.disabled;
const scheme = typeof to === "string" && getUrlScheme(to);
let href;
let external;
let active = false;
if (scheme) {
if (router.protocolAllowlist.has(scheme)) external = to;
else {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`);
external = null;
}
} else {
const options_ = dest();
options_._fromLocation = location;
const next = Solid.untrack(() => router.buildLocation(options_));
if (!disabled) {
const shown = next.maskedLocation ?? next;
const publicHref = shown.publicHref;
const shownHref = shown.external ? publicHref : router.history.createHref(publicHref) || "/";
if ((shown.external || shownHref !== publicHref) && isDangerousProtocol(shownHref, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${shownHref}`);
} else href = shownHref;
}
external = !href && !disabled ? null : href && getUrlScheme(href) ? href : void 0;
if (external === void 0) {
const activeOptions = options.activeOptions;
const currentPath = removeTrailingSlash(location.pathname, router.basepath);
const nextPath = removeTrailingSlash(next.pathname, router.basepath);
active = (activeOptions?.exact ? currentPath === nextPath : currentPath.startsWith(nextPath) && (currentPath.length === nextPath.length || currentPath[nextPath.length] === "/")) && (!(activeOptions?.includeSearch ?? true) || deepEqual(location.search, next.search, !activeOptions?.exact, activeOptions?.explicitUndefined)) && (!activeOptions?.includeHash || (hasHydrated && !hasHydrated() ? "" : location.hash) === next.hash);
}
}
href = external === null ? void 0 : external || href;
return prev && prev[0] === href && prev[1] === external && prev[2] === active ? prev : [
href,
external,
active
];
};
const linkState = isServer ?? router.isServer ? getLinkState : Solid.createMemo(getLinkState);
const resolveLinkStateProps = ([href, external, active], base) => {
const disabled = options.disabled || external === null;
base.href = href;
base.disabled = disabled;
base.target = options.target;
if (disabled) Object.assign(base, STATIC_DISABLED_PROPS);
if (options.activeProps === void 0 && options.inactiveProps === void 0 && options.class === void 0 && options.style === void 0) return active ? Object.assign(base, STATIC_DEFAULT_ACTIVE_ATTRIBUTES) : base;
const stateProps = functionalUpdate((active ? options.activeProps : options.inactiveProps) ?? (active ? STATIC_ACTIVE_PROPS : EMPTY_OBJECT), {}) ?? EMPTY_OBJECT;
const baseStyle = options.style;
const stateStyle = stateProps.style;
const style = baseStyle || stateStyle ? {
...baseStyle,
...stateStyle
} : void 0;
const baseClass = options.class;
const stateClass = stateProps.class;
const className = baseClass ? stateClass ? `${baseClass} ${stateClass}` : baseClass : stateClass;
return {
...base,
...stateProps,
href,
disabled,
target: base.target,
...style && hasKeys(style) && { style },
...className && { class: className },
...active && STATIC_ACTIVE_ATTRIBUTES
};
};
if (isServer ?? router.isServer) {
const props = resolveLinkStateProps(linkState(), {
onClick: options.onClick,
onBlur: options.onBlur,
onFocus: options.onFocus,
onMouseEnter: options.onMouseEnter,
onMouseLeave: options.onMouseLeave,
onMouseOut: options.onMouseOut,
onMouseOver: options.onMouseOver,
onTouchStart: options.onTouchStart,
ref: options.ref
});
for (const key of STATIC_EVENT_PROPS) if (props[key] === void 0) delete props[key];
return Solid.mergeProps(propsSafeToSpread, props);
}
let hasRenderFetched = false;
const preload = () => !options.reloadDocument && linkState()[1] === void 0 && !options.disabled && (options.preload ?? router.options.defaultPreload);
const preloadDelay = () => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0;
const doPreload = () => router.preloadRoute(dest()).catch((err) => {
console.warn(err);
console.warn(preloadWarning);
});
const [ref, setRef] = Solid.createSignal(null);
let preloadTimeout;
const cancelPreload = () => {
clearTimeout(preloadTimeout);
preloadTimeout = void 0;
};
const enqueuePreload = (e) => {
if (!e) {
cancelPreload();
return;
}
if (!(e.isIntersecting ?? preload() === "intent")) {
if (e.isIntersecting === false) cancelPreload();
return;
}
if (!preloadDelay()) {
doPreload();
return;
}
preloadTimeout ??= setTimeout(() => {
preloadTimeout = void 0;
doPreload();
}, preloadDelay());
};
useIntersectionObserver(ref, enqueuePreload, preload);
Solid.createEffect(() => {
if (hasRenderFetched) return;
if (preload() === "render") {
doPreload();
hasRenderFetched = true;
}
});
const handleClick = (e) => {
const elementTarget = e.currentTarget.getAttribute("target");
const effectiveTarget = options.target !== void 0 ? options.target : elementTarget;
if (!options.disabled && linkState()[1] === void 0 && !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) {
e.preventDefault();
cancelPreload();
router.navigate(dest());
}
};
const handleTouchStart = () => {
if (preload() !== "intent") return;
doPreload();
};
const handleLeave = () => {
if (preload() === "intent") cancelPreload();
};
const onClick = createComposedHandler(() => options.onClick, handleClick);
const onBlur = createComposedHandler(() => options.onBlur, handleLeave);
const onFocus = createComposedHandler(() => options.onFocus, enqueuePreload);
const onMouseEnter = createComposedHandler(() => options.onMouseEnter, enqueuePreload);
const onMouseOver = createComposedHandler(() => options.onMouseOver, enqueuePreload);
const onMouseLeave = createComposedHandler(() => options.onMouseLeave, handleLeave);
const onMouseOut = createComposedHandler(() => options.onMouseOut, handleLeave);
const onTouchStart = createComposedHandler(() => options.onTouchStart, handleTouchStart);
const linkRef = (el) => {
setRef(el);
const ref = resolved.ref ?? options.ref;
if (typeof ref === "function") ref(el);
};
let resolved;
const resolvedProps = () => resolved = resolveLinkStateProps(linkState(), {
onClick,
onBlur,
onFocus,
onMouseEnter,
onMouseOver,
onMouseLeave,
onMouseOut,
onTouchStart
});
return Solid.mergeProps(propsSafeToSpread, resolvedProps, { ref: linkRef });
}
var isStore = (value) => !!value?.[Solid.$PROXY];
var NAVIGATION_KEYS = [
"to",
"reloadDocument",
"replace",
"resetScroll",
"hashScrollIntoView",
"startTransition",
"viewTransition",
"ignoreBlocker",
"params",
"search",
"hash",
"state",
"mask",
"unsafeRelative",
"from",
"href"
];
var LINK_OPTION_KEYS = [
...NAVIGATION_KEYS,
"activeProps",
"inactiveProps",
"activeOptions",
"preload",
"preloadDelay",
"preloadIntentProximity",
"target",
"disabled",
"style",
"class",
"onClick",
"onBlur",
"onFocus",
"onMouseEnter",
"onMouseLeave",
"onMouseOver",
"onMouseOut",
"onTouchStart"
];
var LINK_ELEMENT_KEYS = [
...LINK_OPTION_KEYS,
"_asChild",
"children",
"type"
];
var STATIC_EVENT_PROPS = [
"onClick",
"onBlur",
"onFocus",
"onMouseEnter",
"onMouseLeave",
"onMouseOut",
"onMouseOver",
"onTouchStart"
];
var STATIC_ACTIVE_PROPS = { class: "active" };
var 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"
};
/** 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 = props;
const linkProps = createLinkProps(props, LINK_ELEMENT_KEYS);
const children = () => {
const ch = local.children;
if (typeof ch === "function") return ch({ get isActive() {
return linkProps["data-status"] === "active";
} });
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;
})();
const memo = Solid.createMemo(children);
return createComponent(Dynamic, mergeProps({ get component() {
return local._asChild;
} }, linkProps, { get children() {
return memo();
} }));
};
var linkOptions = (options) => {
return options;
};
//#endregion
export { Link, createLink, linkOptions, useLinkProps };
//# sourceMappingURL=link.js.map