UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

375 lines (374 loc) 11.8 kB
import { useRouter } from "./useRouter.js"; import { useRouterState } from "./useRouterState.js"; import { useIntersectionObserver } from "./utils.js"; import { useMatches } from "./Matches.js"; import { deepEqual, exactPathTest, isDangerousProtocol, preloadWarning, removeTrailingSlash } from "@tanstack/router-core"; import * as Vue from "vue"; //#region src/link.tsx var timeoutMap = /* @__PURE__ */ new WeakMap(); function useLinkProps(options) { const router = useRouter(); const isTransitioning = Vue.ref(false); let hasRenderFetched = false; if (!router) { console.warn("useRouter must be used inside a <RouterProvider> component!"); return Vue.computed(() => ({})); } const type = Vue.computed(() => { try { new URL(`${options.to}`); return "external"; } catch { return "internal"; } }); const buildLocationKey = useRouterState({ select: (s) => { const leaf = s.matches[s.matches.length - 1]; return { search: leaf?.search, hash: s.location.hash, path: leaf?.pathname }; } }); const from = useMatches({ select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath }); const _options = Vue.computed(() => ({ ...options, from: from.value })); const next = Vue.computed(() => { buildLocationKey.value; return router.buildLocation(_options.value); }); const preload = Vue.computed(() => { if (_options.value.reloadDocument) return false; return options.preload ?? router.options.defaultPreload; }); const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0); const isActive = useRouterState({ select: (s) => { const activeOptions = options.activeOptions; if (activeOptions?.exact) { if (!exactPathTest(s.location.pathname, next.value.pathname, router.basepath)) return false; } else { const currentPathSplit = removeTrailingSlash(s.location.pathname, router.basepath).split("/"); if (!(removeTrailingSlash(next.value?.pathname, router.basepath)?.split("/"))?.every((d, i) => d === currentPathSplit[i])) return false; } if (activeOptions?.includeSearch ?? true) { if (!deepEqual(s.location.search, next.value.search, { partial: !activeOptions?.exact, ignoreUndefined: !activeOptions?.explicitUndefined })) return false; } if (activeOptions?.includeHash) return s.location.hash === next.value.hash; return true; } }); const doPreload = () => router.preloadRoute(_options.value).catch((err) => { console.warn(err); console.warn(preloadWarning); }); const preloadViewportIoCallback = (entry) => { if (entry?.isIntersecting) doPreload(); }; const ref = Vue.ref(null); useIntersectionObserver(ref, preloadViewportIoCallback, { rootMargin: "100px" }, { disabled: () => !!options.disabled || !(preload.value === "viewport") }); Vue.effect(() => { if (hasRenderFetched) return; if (!options.disabled && preload.value === "render") { doPreload(); hasRenderFetched = true; } }); const getPropsSafeToSpread = () => { const result = {}; const optionRecord = options; for (const key in options) if (![ "activeProps", "inactiveProps", "activeOptions", "to", "preload", "preloadDelay", "hashScrollIntoView", "replace", "startTransition", "resetScroll", "viewTransition", "children", "target", "disabled", "style", "class", "onClick", "onBlur", "onFocus", "onMouseEnter", "onMouseLeave", "onMouseOver", "onMouseOut", "onTouchStart", "ignoreBlocker", "params", "search", "hash", "state", "mask", "reloadDocument", "_asChild", "from", "additionalProps" ].includes(key)) result[key] = optionRecord[key]; return result; }; if (type.value === "external") { if (isDangerousProtocol(options.to, router.protocolAllowlist)) { if (process.env.NODE_ENV !== "production") console.warn(`Blocked Link with dangerous protocol: ${options.to}`); const safeProps = { ...getPropsSafeToSpread(), ref, target: options.target, disabled: options.disabled, style: options.style, class: options.class, onClick: options.onClick, onBlur: options.onBlur, onFocus: options.onFocus, onMouseEnter: options.onMouseEnter, onMouseLeave: options.onMouseLeave, onMouseOver: options.onMouseOver, onMouseOut: options.onMouseOut, onTouchStart: options.onTouchStart }; Object.keys(safeProps).forEach((key) => { if (safeProps[key] === void 0) delete safeProps[key]; }); return Vue.computed(() => safeProps); } const externalProps = { ...getPropsSafeToSpread(), ref, href: options.to, target: options.target, disabled: options.disabled, style: options.style, class: options.class, onClick: options.onClick, onBlur: options.onBlur, onFocus: options.onFocus, onMouseEnter: options.onMouseEnter, onMouseLeave: options.onMouseLeave, onMouseOver: options.onMouseOver, onMouseOut: options.onMouseOut, onTouchStart: options.onTouchStart }; Object.keys(externalProps).forEach((key) => { if (externalProps[key] === void 0) delete externalProps[key]; }); return Vue.computed(() => externalProps); } const handleClick = (e) => { const elementTarget = e.currentTarget?.getAttribute("target"); const effectiveTarget = options.target !== void 0 ? options.target : elementTarget; if (!options.disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!effectiveTarget || effectiveTarget === "_self") && e.button === 0) { if (_options.value.reloadDocument) return; e.preventDefault(); isTransitioning.value = true; const unsub = router.subscribe("onResolved", () => { unsub(); isTransitioning.value = false; }); router.navigate({ ..._options.value, replace: options.replace, resetScroll: options.resetScroll, hashScrollIntoView: options.hashScrollIntoView, startTransition: options.startTransition, viewTransition: options.viewTransition, ignoreBlocker: options.ignoreBlocker }); } }; const enqueueIntentPreload = (e) => { if (options.disabled || preload.value !== "intent") return; if (!preloadDelay.value) { doPreload(); return; } const eventTarget = e.currentTarget || e.target; if (!eventTarget || timeoutMap.has(eventTarget)) return; timeoutMap.set(eventTarget, setTimeout(() => { timeoutMap.delete(eventTarget); doPreload(); }, preloadDelay.value)); }; const handleTouchStart = (_) => { if (options.disabled || preload.value !== "intent") return; doPreload(); }; const handleLeave = (e) => { if (options.disabled) return; const eventTarget = e.currentTarget || e.target; if (eventTarget) { const id = timeoutMap.get(eventTarget); clearTimeout(id); timeoutMap.delete(eventTarget); } }; function composeEventHandlers(handlers) { return (event) => { for (const handler of handlers) if (handler) handler(event); }; } const resolvedActiveProps = Vue.computed(() => { const activeProps = options.activeProps || (() => ({ class: "active" })); return (isActive.value ? typeof activeProps === "function" ? activeProps() : activeProps : {}) || { class: void 0, style: void 0 }; }); const resolvedInactiveProps = Vue.computed(() => { const inactiveProps = options.inactiveProps || (() => ({})); return (isActive.value ? {} : typeof inactiveProps === "function" ? inactiveProps() : inactiveProps) || { class: void 0, style: void 0 }; }); const resolvedClassName = Vue.computed(() => { const classes = [ options.class, resolvedActiveProps.value?.class, resolvedInactiveProps.value?.class ].filter(Boolean); return classes.length ? classes.join(" ") : void 0; }); const resolvedStyle = Vue.computed(() => { const result = {}; if (options.style) Object.assign(result, options.style); if (resolvedActiveProps.value?.style) Object.assign(result, resolvedActiveProps.value.style); if (resolvedInactiveProps.value?.style) Object.assign(result, resolvedInactiveProps.value.style); return Object.keys(result).length > 0 ? result : void 0; }); const href = Vue.computed(() => { if (options.disabled) return; const nextLocation = next.value; const location = nextLocation?.maskedLocation ?? nextLocation; const publicHref = location?.publicHref; if (!publicHref) return void 0; if (location?.external) return publicHref; return router.history.createHref(publicHref) || "/"; }); const staticEventHandlers = { onClick: composeEventHandlers([options.onClick, handleClick]), onBlur: composeEventHandlers([options.onBlur, handleLeave]), onFocus: composeEventHandlers([options.onFocus, enqueueIntentPreload]), onMouseenter: composeEventHandlers([options.onMouseEnter, enqueueIntentPreload]), onMouseover: composeEventHandlers([options.onMouseOver, enqueueIntentPreload]), onMouseleave: composeEventHandlers([options.onMouseLeave, handleLeave]), onMouseout: composeEventHandlers([options.onMouseOut, handleLeave]), onTouchstart: composeEventHandlers([options.onTouchStart, handleTouchStart]) }; return Vue.computed(() => { const result = { ...getPropsSafeToSpread(), href: href.value, ref, ...staticEventHandlers, disabled: !!options.disabled, target: options.target }; if (resolvedStyle.value) result.style = resolvedStyle.value; if (resolvedClassName.value) result.class = resolvedClassName.value; if (options.disabled) { result.role = "link"; result["aria-disabled"] = true; } if (isActive.value) { result["data-status"] = "active"; result["aria-current"] = "page"; } if (isTransitioning.value) result["data-transitioning"] = "transitioning"; const activeP = resolvedActiveProps.value; const inactiveP = resolvedInactiveProps.value; for (const key of Object.keys(activeP)) if (key !== "class" && key !== "style") result[key] = activeP[key]; for (const key of Object.keys(inactiveP)) if (key !== "class" && key !== "style") result[key] = inactiveP[key]; return result; }); } 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", "activeProps", "inactiveProps", "activeOptions", "from", "search", "params", "hash", "state", "mask", "reloadDocument", "disabled", "additionalProps", "viewTransition", "resetScroll", "startTransition", "hashScrollIntoView", "replace", "ignoreBlocker", "target" ], setup(props, { attrs, slots }) { const linkPropsComputed = useLinkProps({ ...props, ...attrs }); return () => { const Component = props._asChild || "a"; const linkProps = linkPropsComputed.value; const isActive = linkProps["data-status"] === "active"; const isTransitioning = linkProps["data-transitioning"] === "transitioning"; const slotContent = slots.default ? slots.default({ isActive, isTransitioning }) : []; 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; function isCtrlEvent(e) { return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); } var linkOptions = (options) => { return options; }; //#endregion export { Link, createLink, linkOptions, useLinkProps }; //# sourceMappingURL=link.js.map