UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

326 lines (325 loc) 12.3 kB
import { useHydrated } from "./ClientOnly.js"; import { useRouter } from "./useRouter.js"; import { useIntersectionObserver } from "./utils.js"; import { deepEqual, exactPathTest, functionalUpdate, hasKeys, isDangerousProtocol, preloadWarning, removeTrailingSlash } from "@tanstack/router-core"; import { Dynamic, claimElement, createComponent, insert, mergeProps, spread, template } from "@solidjs/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 mergeRefs(...refs) { const setRef = (ref, el) => { if (typeof ref === "function") ref(el); else if (Array.isArray(ref)) for (const nestedRef of ref) setRef(nestedRef, el); }; return (el) => { for (const ref of refs) setRef(ref, el); }; } function splitProps(props, keys) { return [props, Solid.omit(props, ...keys)]; } var timeoutMap = /* @__PURE__ */ new WeakMap(); var cancelPreload = (eventTarget) => { clearTimeout(timeoutMap.get(eventTarget)); timeoutMap.delete(eventTarget); }; function useLinkProps(options) { const router = useRouter(); const shouldHydrateHash = !isServer && !!router.options.ssr; const hasHydrated = useHydrated(); let hasRenderFetched = false; const local = options; const activeProps = () => local.activeProps ?? STATIC_ACTIVE_PROPS_GET; const inactiveProps = () => local.inactiveProps ?? STATIC_INACTIVE_PROPS_GET; const propsSafeToSpread = Solid.omit(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", "params", "search", "hash", "state", "mask", "reloadDocument", "unsafeRelative", "from"); const currentLocation = Solid.createMemo(() => router.stores.location.get(), { 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)); }, { lazy: true, equals: (prev, next) => prev.href === next.href && prev.external === next.external && prev.maskedLocation?.href === next.maskedLocation?.href }); 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 }; }, { lazy: true }); 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 {} }, { lazy: true }); const preload = Solid.createMemo(() => { if (_options().reloadDocument || externalLink() || local.disabled) return false; return local.preload ?? router.options.defaultPreload; }, { lazy: true }); 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; }, { lazy: true }); const doPreload = () => router.preloadRoute({ ...options, _builtLocation: next() }).catch((err) => { console.warn(err); console.warn(preloadWarning); }); const [ref, setRefSignal] = Solid.createSignal(null); const setRef = (el) => { Solid.runWithOwner(null, () => { setRefSignal(el); }); }; 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(preload, (preloadValue) => { if (hasRenderFetched) return; if (preloadValue === "render") { Solid.untrack(() => doPreload()); hasRenderFetched = true; } }); if (Solid.untrack(externalLink)) { const externalHref = Solid.untrack(externalLink); return Solid.merge(propsSafeToSpread, { ref: mergeRefs(setRef, options.ref), href: externalHref }, 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(); 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 simpleStyling = Solid.createMemo(() => activeProps() === STATIC_ACTIVE_PROPS_GET && inactiveProps() === STATIC_INACTIVE_PROPS_GET && local.class === void 0 && local.style === void 0, { lazy: true }); 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 resolvedStateProps = Solid.createMemo(() => (isActive() ? functionalUpdate(activeProps(), {}) : functionalUpdate(inactiveProps(), {})) ?? EMPTY_OBJECT, { lazy: true }); const resolvedClass = Solid.createMemo(() => { if (simpleStyling()) return isActive() ? "active" : void 0; return [local.class, resolvedStateProps().class].filter(Boolean).join(" ") || void 0; }, { lazy: true }); const resolvedStyle = Solid.createMemo(() => { if (simpleStyling()) return local.style; const style = { ...local.style, ...resolvedStateProps().style }; return hasKeys(style) ? style : void 0; }, { lazy: true }); const extraStateKeys = /* @__PURE__ */ new Set(); Solid.untrack(() => { for (const stateProps of [functionalUpdate(activeProps(), {}), functionalUpdate(inactiveProps(), {})]) if (stateProps) { for (const key of Object.keys(stateProps)) if (key !== "class" && key !== "style") extraStateKeys.add(key); } }); const composedRef = mergeRefs(setRef, (el) => { const r = _options().ref; if (typeof r === "function") r(el); }); const linkProps = {}; for (const key of Object.keys(propsSafeToSpread)) Object.defineProperty(linkProps, key, Object.getOwnPropertyDescriptor(propsSafeToSpread, key)); for (const key of extraStateKeys) Object.defineProperty(linkProps, key, { get: () => resolvedStateProps()[key], enumerable: true, configurable: true }); const defineGetters = (getters) => { for (const key of Object.keys(getters)) Object.defineProperty(linkProps, key, { get: getters[key], enumerable: true, configurable: true }); }; linkProps.ref = composedRef; linkProps.onClick = onClick; linkProps.onBlur = onBlur; linkProps.onFocus = onFocus; linkProps.onMouseEnter = onMouseEnter; linkProps.onMouseOver = onMouseOver; linkProps.onMouseLeave = onMouseLeave; linkProps.onMouseOut = onMouseOut; linkProps.onTouchStart = onTouchStart; defineGetters({ href: () => hrefOption()?.href, disabled: () => !!local.disabled, target: () => local.target, role: () => local.disabled ? "link" : void 0, "aria-disabled": () => local.disabled ? "true" : void 0, "data-status": () => isActive() ? "active" : void 0, "aria-current": () => isActive() ? "page" : void 0, class: resolvedClass, style: resolvedStyle }); return linkProps; } var STATIC_ACTIVE_PROPS = { class: "active" }; var STATIC_ACTIVE_PROPS_GET = () => STATIC_ACTIVE_PROPS; var EMPTY_OBJECT = {}; var STATIC_INACTIVE_PROPS_GET = () => EMPTY_OBJECT; /** 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] = splitProps(props, ["_asChild", "children"]); const [_, linkProps] = splitProps(useLinkProps(rest), ["type"]); const resolvedChildren = Solid.children(() => local.children); const children = () => { const ch = resolvedChildren(); if (typeof ch === "function") return ch({ get isActive() { return linkProps["data-status"] === "active"; } }); return ch; }; if (local._asChild === "svg") { const [_, svgLinkProps] = splitProps(linkProps, ["class"]); var _el$ = _tmpl$(); var _el$2 = _el$.firstChild; claimElement(_el$2); spread(_el$2, svgLinkProps, true); insert(_el$2, children); return _el$; } if (!local._asChild) { var _el$3 = _tmpl$2(); claimElement(_el$3); spread(_el$3, linkProps, 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