UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

323 lines (322 loc) • 11.1 kB
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