@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
322 lines (321 loc) • 11 kB
JavaScript
import { useHydrated } from "./ClientOnly.js";
import { useRouter } from "./useRouter.js";
import { useIntersectionObserver } from "./utils.js";
import { 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 { 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();
var cancelPreload = (eventTarget) => {
clearTimeout(timeoutMap.get(eventTarget));
timeoutMap.delete(eventTarget);
};
function useLinkProps(options) {
const router = useRouter();
const [local, rest] = Solid.splitProps(Solid.mergeProps({
activeProps: STATIC_ACTIVE_PROPS_GET,
inactiveProps: STATIC_INACTIVE_PROPS_GET
}, options), [
"activeProps",
"inactiveProps",
"activeOptions",
"to",
"preload",
"preloadDelay",
"preloadIntentProximity",
"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",
"from",
"href"
]);
const currentLocation = Solid.createMemo(() => router.stores.location.get(), void 0, { equals: (prev, next) => prev.href === next.href });
const next = Solid.createMemo(() => {
const nextOptions = {
_fromLocation: currentLocation(),
...options
};
return Solid.untrack(() => router.buildLocation(nextOptions));
});
const hrefOption = Solid.createMemo(() => {
if (options.disabled) return void 0;
const location = next().maskedLocation ?? next();
const publicHref = location.publicHref;
const external = location.external;
const href = external ? publicHref : router.history.createHref(publicHref) || "/";
if ((external || href !== publicHref) && isDangerousProtocol(href, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${href}`);
return;
}
return href;
});
const externalLink = Solid.createMemo(() => {
const to = options.to;
const scheme = typeof to === "string" && getUrlScheme(to);
if (scheme) {
if (!router.protocolAllowlist.has(scheme)) {
if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${to}`);
return null;
}
return to;
}
const _href = hrefOption();
if (!_href && !options.disabled) return null;
return _href && getUrlScheme(_href) ? _href : void 0;
});
const shouldHydrateHash = !isServer && !!router.options.ssr;
const hasHydrated = isServer ?? router.isServer ? void 0 : useHydrated();
const isActive = Solid.createMemo(() => {
if (externalLink() !== void 0) return false;
const activeOptions = local.activeOptions;
const current = currentLocation();
const nextLocation = next();
const currentPath = removeTrailingSlash(current.pathname, router.basepath);
const nextPath = removeTrailingSlash(nextLocation.pathname, router.basepath);
if (activeOptions?.exact ? currentPath !== nextPath : !(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 simpleStyling = Solid.createMemo(() => local.activeProps === STATIC_ACTIVE_PROPS_GET && local.inactiveProps === STATIC_INACTIVE_PROPS_GET && local.class === void 0 && local.style === void 0);
const resolveLinkStateProps = (base) => {
const active = isActive();
if (simpleStyling()) return {
...base,
...active && STATIC_DEFAULT_ACTIVE_ATTRIBUTES
};
const stateProps = active ? functionalUpdate(local.activeProps, {}) ?? EMPTY_OBJECT : functionalUpdate(local.inactiveProps, {});
const style = {
...local.style,
...stateProps.style
};
const className = [local.class, stateProps.class].filter(Boolean).join(" ");
return {
...stateProps,
...base,
...hasKeys(style) ? { style } : void 0,
...className ? { class: className } : void 0,
...active && STATIC_ACTIVE_ATTRIBUTES
};
};
if (isServer ?? router.isServer) {
const external = externalLink();
const disabled = local.disabled || external === null;
const props = resolveLinkStateProps({
onClick: local.onClick,
onBlur: local.onBlur,
onFocus: local.onFocus,
onMouseEnter: local.onMouseEnter,
onMouseLeave: local.onMouseLeave,
onMouseOut: local.onMouseOut,
onMouseOver: local.onMouseOver,
onTouchStart: local.onTouchStart,
href: external === null ? void 0 : external || hrefOption(),
ref: options.ref,
disabled,
target: local.target,
...disabled && STATIC_DISABLED_PROPS
});
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 = Solid.createMemo(() => {
if (options.reloadDocument || externalLink() !== void 0 || local.disabled) return false;
return local.preload ?? router.options.defaultPreload;
});
const preloadDelay = () => local.preloadDelay ?? router.options.defaultPreloadDelay ?? 0;
const doPreload = () => router.preloadRoute(options).catch((err) => {
console.warn(err);
console.warn(preloadWarning);
});
const [ref, setRef] = Solid.createSignal(null);
const enqueuePreload = (e) => {
if (!e) {
cancelPreload(ref);
return;
}
if (!(e.isIntersecting ?? preload() === "intent")) {
if (e.isIntersecting === false) cancelPreload(ref);
return;
}
if (!preloadDelay()) {
doPreload();
return;
}
if (!timeoutMap.has(ref)) timeoutMap.set(ref, setTimeout(() => {
timeoutMap.delete(ref);
doPreload();
}, preloadDelay()));
};
useIntersectionObserver(ref, enqueuePreload, () => preload() !== "viewport");
Solid.createEffect(() => {
if (hasRenderFetched) return;
if (preload() === "render") {
doPreload();
hasRenderFetched = true;
}
});
const handleClick = (e) => {
const elementTarget = e.currentTarget.getAttribute("target");
const effectiveTarget = local.target !== void 0 ? local.target : elementTarget;
if (!local.disabled && externalLink() === void 0 && !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) {
e.preventDefault();
router.navigate({
...options,
replace: local.replace,
resetScroll: local.resetScroll,
hashScrollIntoView: local.hashScrollIntoView,
startTransition: local.startTransition,
viewTransition: local.viewTransition,
ignoreBlocker: local.ignoreBlocker
});
}
};
const handleTouchStart = () => {
if (preload() !== "intent") return;
doPreload();
};
const handleLeave = () => {
if (preload() === "intent") cancelPreload(ref);
};
const onClick = createComposedHandler(() => local.onClick, handleClick);
const onBlur = createComposedHandler(() => local.onBlur, handleLeave);
const onFocus = createComposedHandler(() => local.onFocus, enqueuePreload);
const onMouseEnter = createComposedHandler(() => local.onMouseEnter, enqueuePreload);
const onMouseOver = createComposedHandler(() => local.onMouseOver, enqueuePreload);
const onMouseLeave = createComposedHandler(() => local.onMouseLeave, handleLeave);
const onMouseOut = createComposedHandler(() => local.onMouseOut, handleLeave);
const onTouchStart = createComposedHandler(() => local.onTouchStart, handleTouchStart);
const resolvedProps = Solid.createMemo(() => {
const external = externalLink();
const disabled = local.disabled || external === null;
return resolveLinkStateProps({
href: external === null ? void 0 : external || hrefOption(),
ref: mergeRefs(setRef, options.ref),
onClick,
onBlur,
onFocus,
onMouseEnter,
onMouseOver,
onMouseLeave,
onMouseOut,
onTouchStart,
disabled,
target: local.target,
...disabled && STATIC_DISABLED_PROPS
});
});
return Solid.mergeProps(propsSafeToSpread, resolvedProps);
}
var STATIC_EVENT_PROPS = [
"onClick",
"onBlur",
"onFocus",
"onMouseEnter",
"onMouseLeave",
"onMouseOut",
"onMouseOver",
"onTouchStart"
];
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"
};
/** 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";
} });
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();
} }));
};
var linkOptions = (options) => {
return options;
};
//#endregion
export { Link, createLink, linkOptions, useLinkProps };
//# sourceMappingURL=link.js.map