@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
398 lines (397 loc) • 13.9 kB
JavaScript
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