UNPKG

@tanstack/vue-router

Version:

Modern and scalable routing for Vue applications

398 lines (397 loc) 13.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) { 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 ref = Vue.ref(null); const eventHandlers = getLinkEventHandlers(options); 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(options), ref, target: options.target, disabled: options.disabled, style: options.style, class: options.class, onClick: options.onClick, onBlur: options.onBlur, onFocus: options.onFocus, onMouseenter: eventHandlers.onMouseenter, onMouseleave: eventHandlers.onMouseleave, onMouseover: eventHandlers.onMouseover, onMouseout: eventHandlers.onMouseout, onTouchstart: eventHandlers.onTouchstart }; Object.keys(safeProps).forEach((key) => { if (safeProps[key] === void 0) delete safeProps[key]; }); return Vue.computed(() => safeProps); } const externalProps = { ...getPropsSafeToSpread(options), 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: eventHandlers.onMouseenter, onMouseleave: eventHandlers.onMouseleave, onMouseover: eventHandlers.onMouseover, onMouseout: eventHandlers.onMouseout, onTouchstart: eventHandlers.onTouchstart }; Object.keys(externalProps).forEach((key) => { if (externalProps[key] === void 0) delete externalProps[key]; }); return Vue.computed(() => externalProps); } if (isServer ?? router.isServer) { const next = router.buildLocation(options); const href = getHref({ options, router, nextLocation: next }); const isActive = getIsActive({ loc: router.stores.location.get(), nextLoc: next, activeOptions: options.activeOptions, router }); const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle } = resolveStyleProps({ options, isActive }); const result = combineResultProps({ href, options, isActive, isTransitioning: false, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle }); return Vue.ref(result); } const currentLocation = useStore(router.stores.location, (l) => l, { equal: (prev, next) => prev.href === next.href }); const next = Vue.computed(() => { const opts = { _fromLocation: currentLocation.value, ...options }; return router.buildLocation(opts); }); const preload = Vue.computed(() => { if (options.reloadDocument) return false; return options.preload ?? router.options.defaultPreload; }); const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0); const isActive = Vue.computed(() => getIsActive({ activeOptions: options.activeOptions, loc: currentLocation.value, nextLoc: next.value, router })); const doPreload = () => router.preloadRoute({ ...options, _builtLocation: next.value }).catch((err) => { console.warn(err); console.warn(preloadWarning); }); const preloadViewportIoCallback = (entry) => { if (entry?.isIntersecting) doPreload(); }; 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 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.reloadDocument) return; e.preventDefault(); isTransitioning.value = true; const unsub = router.subscribe("onResolved", () => { unsub(); isTransitioning.value = false; }); router.navigate({ ...options, 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 resolvedStyleProps = Vue.computed(() => resolveStyleProps({ options, isActive: isActive.value })); const href = Vue.computed(() => getHref({ options, router, nextLocation: next.value })); const staticEventHandlers = { onClick: composeEventHandlers([options.onClick, handleClick]), onBlur: composeEventHandlers([options.onBlur, handleLeave]), onFocus: composeEventHandlers([options.onFocus, enqueueIntentPreload]), onMouseenter: composeEventHandlers([eventHandlers.onMouseenter, enqueueIntentPreload]), onMouseover: composeEventHandlers([eventHandlers.onMouseover, enqueueIntentPreload]), onMouseleave: composeEventHandlers([eventHandlers.onMouseleave, handleLeave]), onMouseout: composeEventHandlers([eventHandlers.onMouseout, handleLeave]), onTouchstart: composeEventHandlers([eventHandlers.onTouchstart, handleTouchStart]) }; return Vue.computed(() => { const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle } = resolvedStyleProps.value; return combineResultProps({ href: href.value, options, ref, staticEventHandlers, isActive: isActive.value, isTransitioning: isTransitioning.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, isTransitioning, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, ref, staticEventHandlers }) { const result = { ...getPropsSafeToSpread(options), ref, ...staticEventHandlers, href, disabled: !!options.disabled, 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"; } if (isTransitioning) result["data-transitioning"] = "transitioning"; 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 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({ activeOptions, loc, nextLoc, 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 linkPropsSource = useLinkProps({ ...props, ...attrs }); return () => { const Component = props._asChild || "a"; const linkProps = Vue.unref(linkPropsSource); 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