UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

390 lines (389 loc) 14.9 kB
import { useRouter } from "./useRouter.js"; import { useIntersectionObserver } from "./utils.js"; import { deepEqual, exactPathTest, hasKeys, isDangerousProtocol, preloadWarning, removeTrailingSlash } from "@tanstack/router-core"; import * as Vue from "vue"; import { isServer } from "@tanstack/router-core/isServer"; import { useStore } from "@tanstack/vue-store"; //#region src/link.tsx var timeoutMap = /* @__PURE__ */ new WeakMap(); function useLinkProps(options) { return useLinkPropsImpl(() => options); } function useLinkPropsImpl(getOptions) { const router = useRouter(); let renderFetchedHref; if (!router) { console.warn("useRouter must be used inside a <RouterProvider> component!"); return Vue.computed(() => ({})); } const type = Vue.computed(() => { const options = getOptions(); try { new URL(`${options.to}`); return "external"; } catch { return "internal"; } }); const ref = Vue.ref(null); if (isServer ?? router.isServer) { const options = getOptions(); if (type.value === "external") return Vue.ref(getExternalLinkProps(options, router, ref)); const next = router.buildLocation(options); const href = getHref(options, router, next); const isActive = getIsActive(router.stores.location.get(), next, options.activeOptions, router); const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle } = resolveStyleProps(options, isActive); const result = combineResultProps({ href, options, isActive, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle }); return Vue.ref(result); } const currentLocation = type.value === "external" ? Vue.shallowRef(router.stores.location.get()) : useStore(router.stores.location, (l) => l, { equal: (prev, next) => prev.href === next.href }); if (type.value === "external") Vue.watchEffect((onCleanup) => { if (type.value === "external") return; const subscription = router.stores.location.subscribe((location) => { if (currentLocation.value.href !== location.href) currentLocation.value = location; }); onCleanup(() => subscription.unsubscribe()); }); const next = Vue.computed(() => { const options = getOptions(); const opts = { _fromLocation: currentLocation.value, ...options }; return router.buildLocation(opts); }); const preload = Vue.computed(() => { const options = getOptions(); if (type.value === "external" || options.reloadDocument || options.disabled) return false; return options.preload ?? router.options.defaultPreload; }); const preloadDelay = Vue.computed(() => getOptions().preloadDelay ?? router.options.defaultPreloadDelay ?? 0); const isActive = Vue.computed(() => { const options = getOptions(); return getIsActive(currentLocation.value, next.value, options.activeOptions, router); }); const doPreload = () => { const options = getOptions(); return router.preloadRoute({ ...options, _builtLocation: next.value }).catch((err) => { console.warn(err); console.warn(preloadWarning); }); }; let pendingPreload; const enqueuePreload = (e) => { if (!e) { clearTimeout(timeoutMap.get(ref)); timeoutMap.delete(ref); pendingPreload = void 0; return; } const isIntersecting = e.isIntersecting; const preloadMode = isIntersecting === void 0 ? "intent" : "viewport"; if (preload.value !== preloadMode || isIntersecting === false) { if (isIntersecting === false && pendingPreload === "viewport") { clearTimeout(timeoutMap.get(ref)); timeoutMap.delete(ref); pendingPreload = void 0; } return; } if (!preloadDelay.value) { doPreload(); return; } if (!timeoutMap.has(ref)) { const scheduledHref = next.value.href; pendingPreload = preloadMode; timeoutMap.set(ref, setTimeout(() => { timeoutMap.delete(ref); pendingPreload = void 0; if (preload.value === preloadMode && next.value.href === scheduledHref) doPreload(); }, preloadDelay.value)); } }; useIntersectionObserver(ref, enqueuePreload, () => preload.value !== "viewport"); Vue.watchEffect(() => { if (preload.value !== "render") return; const nextHref = next.value.href; if (nextHref && renderFetchedHref !== nextHref) { renderFetchedHref = nextHref; doPreload(); } }); const handleClick = (e) => { if (type.value === "external") return; const options = getOptions(); const elementTarget = e.currentTarget?.getAttribute("target"); const effectiveTarget = options.target !== void 0 ? options.target : elementTarget; if (!options.disabled && !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) { if (options.reloadDocument) return; e.preventDefault(); router.navigate({ ...options, replace: options.replace, resetScroll: options.resetScroll, hashScrollIntoView: options.hashScrollIntoView, startTransition: options.startTransition, viewTransition: options.viewTransition, ignoreBlocker: options.ignoreBlocker }); } }; const handleTouchStart = () => { if (preload.value === "intent") doPreload(); }; const handleLeave = () => { if (pendingPreload === "intent") { clearTimeout(timeoutMap.get(ref)); timeoutMap.delete(ref); pendingPreload = void 0; } }; function composeEventHandlers(getUserHandler, handler) { return (event) => { getUserHandler()?.(event); handler(event); }; } const resolvedStyleProps = Vue.computed(() => { return resolveStyleProps(getOptions(), isActive.value); }); const href = Vue.computed(() => { return getHref(getOptions(), router, next.value); }); const staticEventHandlers = { onClick: composeEventHandlers(() => getOptions().onClick, handleClick), onBlur: composeEventHandlers(() => getOptions().onBlur, handleLeave), onFocus: composeEventHandlers(() => getOptions().onFocus, enqueuePreload), onMouseenter: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseenter, enqueuePreload), onMouseover: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseover, enqueuePreload), onMouseleave: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseleave, handleLeave), onMouseout: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onMouseout, handleLeave), onTouchstart: composeEventHandlers(() => getLinkEventHandlers(getOptions()).onTouchstart, handleTouchStart) }; return Vue.computed(() => { const options = getOptions(); if (type.value === "external") return getExternalLinkProps(options, router, ref, staticEventHandlers); const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle } = resolvedStyleProps.value; return combineResultProps({ href: href.value, options, ref, staticEventHandlers, isActive: isActive.value, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle }); }); } function resolveStyleProps(options, isActive) { const activeProps = options.activeProps || (() => ({ class: "active" })); const resolvedActiveProps = (isActive ? typeof activeProps === "function" ? activeProps() : activeProps : {}) || { class: void 0, style: void 0 }; const inactiveProps = options.inactiveProps || (() => ({})); const resolvedInactiveProps = (isActive ? {} : typeof inactiveProps === "function" ? inactiveProps() : inactiveProps) || { class: void 0, style: void 0 }; const classes = [ options.class, resolvedActiveProps?.class, resolvedInactiveProps?.class ].filter(Boolean); const resolvedClassName = classes.length ? classes.join(" ") : void 0; const result = {}; if (options.style) Object.assign(result, options.style); if (resolvedActiveProps?.style) Object.assign(result, resolvedActiveProps.style); if (resolvedInactiveProps?.style) Object.assign(result, resolvedInactiveProps.style); return { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle: hasKeys(result) ? result : void 0 }; } function combineResultProps({ href, options, isActive, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, ref, staticEventHandlers }) { const result = { ...getPropsSafeToSpread(options), ref, ...staticEventHandlers, href, disabled: options._asChild ? !!options.disabled : void 0, target: options.target }; if (resolvedStyle) result.style = resolvedStyle; if (resolvedClassName) result.class = resolvedClassName; if (options.disabled) { result.role = "link"; result["aria-disabled"] = true; } if (isActive) { result["data-status"] = "active"; result["aria-current"] = "page"; } for (const key of Object.keys(resolvedActiveProps)) if (key !== "class" && key !== "style") result[key] = resolvedActiveProps[key]; for (const key of Object.keys(resolvedInactiveProps)) if (key !== "class" && key !== "style") result[key] = resolvedInactiveProps[key]; return result; } function getExternalLinkProps(options, router, ref, staticEventHandlers) { const dangerous = isDangerousProtocol(options.to, router.protocolAllowlist); if (dangerous && process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${options.to}`); const eventHandlers = getLinkEventHandlers(options); const result = { ...getPropsSafeToSpread(options), ref, href: dangerous || options.disabled ? void 0 : options.to, target: options.target, disabled: options._asChild ? !!options.disabled : void 0, style: options.style, class: options.class, onClick: staticEventHandlers?.onClick ?? options.onClick, onBlur: staticEventHandlers?.onBlur ?? options.onBlur, onFocus: staticEventHandlers?.onFocus ?? options.onFocus, onMouseenter: staticEventHandlers?.onMouseenter ?? eventHandlers.onMouseenter, onMouseleave: staticEventHandlers?.onMouseleave ?? eventHandlers.onMouseleave, onMouseover: staticEventHandlers?.onMouseover ?? eventHandlers.onMouseover, onMouseout: staticEventHandlers?.onMouseout ?? eventHandlers.onMouseout, onTouchstart: staticEventHandlers?.onTouchstart ?? eventHandlers.onTouchstart }; if (options.disabled) { result.role = "link"; result["aria-disabled"] = true; } for (const key of Object.keys(result)) if (result[key] === void 0) delete result[key]; return result; } function getLinkEventHandlers(options) { return { onMouseenter: options.onMouseEnter ?? options.onMouseenter, onMouseleave: options.onMouseLeave ?? options.onMouseleave, onMouseover: options.onMouseOver ?? options.onMouseover, onMouseout: options.onMouseOut ?? options.onMouseout, onTouchstart: options.onTouchStart ?? options.onTouchstart }; } var getPropsSafeToSpread = (options) => { const { activeProps: _activeProps, inactiveProps: _inactiveProps, activeOptions: _activeOptions, to: _to, preload: _preload, preloadDelay: _preloadDelay, preloadIntentProximity: _preloadIntentProximity, hashScrollIntoView: _hashScrollIntoView, replace: _replace, startTransition: _startTransition, resetScroll: _resetScroll, viewTransition: _viewTransition, children: _children, target: _target, disabled: _disabled, style: _style, class: _class, onClick: _onClick, onBlur: _onBlur, onFocus: _onFocus, onMouseEnter: _onMouseEnter, onMouseenter: _onMouseenter, onMouseLeave: _onMouseLeave, onMouseleave: _onMouseleave, onMouseOver: _onMouseOver, onMouseover: _onMouseover, onMouseOut: _onMouseOut, onMouseout: _onMouseout, onTouchStart: _onTouchStart, onTouchstart: _onTouchstart, ignoreBlocker: _ignoreBlocker, params: _params, search: _search, hash: _hash, state: _state, mask: _mask, reloadDocument: _reloadDocument, unsafeRelative: _unsafeRelative, _asChild: __asChild, from: _from, additionalProps: _additionalProps, ...propsSafeToSpread } = options; return propsSafeToSpread; }; function getIsActive(loc, nextLoc, activeOptions, router) { if (activeOptions?.exact) { if (!exactPathTest(loc.pathname, nextLoc.pathname, router.basepath)) return false; } else { const currentPath = removeTrailingSlash(loc.pathname, router.basepath); const nextPath = removeTrailingSlash(nextLoc.pathname, router.basepath); if (!(currentPath.startsWith(nextPath) && (currentPath.length === nextPath.length || currentPath[nextPath.length] === "/"))) return false; } if (activeOptions?.includeSearch ?? true) { if (!deepEqual(loc.search, nextLoc.search, { partial: !activeOptions?.exact, ignoreUndefined: !activeOptions?.explicitUndefined })) return false; } if (activeOptions?.includeHash) return loc.hash === nextLoc.hash; return true; } function getHref(options, router, nextLocation) { if (options.disabled) return; const location = nextLocation?.maskedLocation ?? nextLocation; const publicHref = location?.publicHref; if (!publicHref) return void 0; if (location?.external) return publicHref; return router.history.createHref(publicHref) || "/"; } function createLink(Comp) { return Vue.defineComponent({ name: "CreatedLink", inheritAttrs: false, setup(_, { attrs, slots }) { return () => Vue.h(LinkImpl, { ...attrs, _asChild: Comp }, slots); } }); } var LinkImpl = Vue.defineComponent({ name: "Link", inheritAttrs: false, props: [ "_asChild", "to", "preload", "preloadDelay", "preloadIntentProximity", "activeProps", "inactiveProps", "activeOptions", "from", "search", "params", "hash", "state", "mask", "reloadDocument", "disabled", "additionalProps", "viewTransition", "resetScroll", "startTransition", "hashScrollIntoView", "replace", "ignoreBlocker", "target" ], setup(props, { attrs, slots }) { const attrsSnapshot = Vue.shallowRef({ ...attrs }); Vue.onBeforeUpdate(() => { const keys = Object.keys(attrs); const previous = attrsSnapshot.value; if (keys.length !== Object.keys(previous).length || keys.some((key) => !Object.is(attrs[key], previous[key]))) attrsSnapshot.value = { ...attrs }; }); const allProps = Vue.computed(() => ({ ...props, ...attrsSnapshot.value })); const linkPropsSource = useLinkPropsImpl(() => allProps.value); return () => { const Component = props._asChild || "a"; const linkProps = Vue.unref(linkPropsSource); const isActive = linkProps["data-status"] === "active"; const slotContent = slots.default ? slots.default({ isActive }) : []; if (Component === "svg") { const svgLinkProps = { ...linkProps }; delete svgLinkProps.class; return Vue.h("svg", {}, [Vue.h("a", svgLinkProps, slotContent)]); } if (typeof Component !== "string") return Vue.h(Component, { ...linkProps, children: slotContent }, slotContent); return Vue.h(Component, linkProps, slotContent); }; } }); /** * Link component with proper TypeScript generics support */ var Link = LinkImpl; var linkOptions = (options) => { return options; }; //#endregion export { Link, createLink, linkOptions, useLinkProps }; //# sourceMappingURL=link.js.map