305 lines
8.8 KiB
Plaintext
305 lines
8.8 KiB
Plaintext
import { jsx } from "react/jsx-runtime";
|
|
import * as React from "react";
|
|
import { flushSync } from "react-dom";
|
|
import { preloadWarning, functionalUpdate, exactPathTest, removeTrailingSlash, deepEqual } from "@tanstack/router-core";
|
|
import { useRouterState } from "./useRouterState.js";
|
|
import { useRouter } from "./useRouter.js";
|
|
import { useForwardedRef, useIntersectionObserver, useLayoutEffect } from "./utils.js";
|
|
import { useMatches } from "./Matches.js";
|
|
function useLinkProps(options, forwardedRef) {
|
|
const router = useRouter();
|
|
const [isTransitioning, setIsTransitioning] = React.useState(false);
|
|
const hasRenderFetched = React.useRef(false);
|
|
const innerRef = useForwardedRef(forwardedRef);
|
|
const {
|
|
// custom props
|
|
activeProps = () => ({ className: "active" }),
|
|
inactiveProps = () => ({}),
|
|
activeOptions,
|
|
to,
|
|
preload: userPreload,
|
|
preloadDelay: userPreloadDelay,
|
|
hashScrollIntoView,
|
|
replace,
|
|
startTransition,
|
|
resetScroll,
|
|
viewTransition,
|
|
// element props
|
|
children,
|
|
target,
|
|
disabled,
|
|
style,
|
|
className,
|
|
onClick,
|
|
onFocus,
|
|
onMouseEnter,
|
|
onMouseLeave,
|
|
onTouchStart,
|
|
ignoreBlocker,
|
|
...rest
|
|
} = options;
|
|
const {
|
|
// prevent these from being returned
|
|
params: _params,
|
|
search: _search,
|
|
hash: _hash,
|
|
state: _state,
|
|
mask: _mask,
|
|
reloadDocument: _reloadDocument,
|
|
...propsSafeToSpread
|
|
} = rest;
|
|
const type = React.useMemo(() => {
|
|
try {
|
|
new URL(`${to}`);
|
|
return "external";
|
|
} catch {
|
|
}
|
|
return "internal";
|
|
}, [to]);
|
|
const currentSearch = useRouterState({
|
|
select: (s) => s.location.search,
|
|
structuralSharing: true
|
|
});
|
|
const from = useMatches({
|
|
select: (matches) => {
|
|
var _a;
|
|
return options.from ?? ((_a = matches[matches.length - 1]) == null ? void 0 : _a.fullPath);
|
|
}
|
|
});
|
|
const _options = React.useMemo(() => ({ ...options, from }), [options, from]);
|
|
const next = React.useMemo(
|
|
() => router.buildLocation(_options),
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[router, _options, currentSearch]
|
|
);
|
|
const preload = React.useMemo(() => {
|
|
if (_options.reloadDocument) {
|
|
return false;
|
|
}
|
|
return userPreload ?? router.options.defaultPreload;
|
|
}, [router.options.defaultPreload, userPreload, _options.reloadDocument]);
|
|
const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
|
|
const isActive = useRouterState({
|
|
select: (s) => {
|
|
if (activeOptions == null ? void 0 : activeOptions.exact) {
|
|
const testExact = exactPathTest(
|
|
s.location.pathname,
|
|
next.pathname,
|
|
router.basepath
|
|
);
|
|
if (!testExact) {
|
|
return false;
|
|
}
|
|
} else {
|
|
const currentPathSplit = removeTrailingSlash(
|
|
s.location.pathname,
|
|
router.basepath
|
|
).split("/");
|
|
const nextPathSplit = removeTrailingSlash(
|
|
next.pathname,
|
|
router.basepath
|
|
).split("/");
|
|
const pathIsFuzzyEqual = nextPathSplit.every(
|
|
(d, i) => d === currentPathSplit[i]
|
|
);
|
|
if (!pathIsFuzzyEqual) {
|
|
return false;
|
|
}
|
|
}
|
|
if ((activeOptions == null ? void 0 : activeOptions.includeSearch) ?? true) {
|
|
const searchTest = deepEqual(s.location.search, next.search, {
|
|
partial: !(activeOptions == null ? void 0 : activeOptions.exact),
|
|
ignoreUndefined: !(activeOptions == null ? void 0 : activeOptions.explicitUndefined)
|
|
});
|
|
if (!searchTest) {
|
|
return false;
|
|
}
|
|
}
|
|
if (activeOptions == null ? void 0 : activeOptions.includeHash) {
|
|
return s.location.hash === next.hash;
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
const doPreload = React.useCallback(() => {
|
|
router.preloadRoute(_options).catch((err) => {
|
|
console.warn(err);
|
|
console.warn(preloadWarning);
|
|
});
|
|
}, [_options, router]);
|
|
const preloadViewportIoCallback = React.useCallback(
|
|
(entry) => {
|
|
if (entry == null ? void 0 : entry.isIntersecting) {
|
|
doPreload();
|
|
}
|
|
},
|
|
[doPreload]
|
|
);
|
|
useIntersectionObserver(
|
|
innerRef,
|
|
preloadViewportIoCallback,
|
|
{ rootMargin: "100px" },
|
|
{ disabled: !!disabled || !(preload === "viewport") }
|
|
);
|
|
useLayoutEffect(() => {
|
|
if (hasRenderFetched.current) {
|
|
return;
|
|
}
|
|
if (!disabled && preload === "render") {
|
|
doPreload();
|
|
hasRenderFetched.current = true;
|
|
}
|
|
}, [disabled, doPreload, preload]);
|
|
if (type === "external") {
|
|
return {
|
|
...propsSafeToSpread,
|
|
ref: innerRef,
|
|
type,
|
|
href: to,
|
|
...children && { children },
|
|
...target && { target },
|
|
...disabled && { disabled },
|
|
...style && { style },
|
|
...className && { className },
|
|
...onClick && { onClick },
|
|
...onFocus && { onFocus },
|
|
...onMouseEnter && { onMouseEnter },
|
|
...onMouseLeave && { onMouseLeave },
|
|
...onTouchStart && { onTouchStart }
|
|
};
|
|
}
|
|
const handleClick = (e) => {
|
|
if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!target || target === "_self") && e.button === 0) {
|
|
e.preventDefault();
|
|
flushSync(() => {
|
|
setIsTransitioning(true);
|
|
});
|
|
const unsub = router.subscribe("onResolved", () => {
|
|
unsub();
|
|
setIsTransitioning(false);
|
|
});
|
|
return router.navigate({
|
|
..._options,
|
|
replace,
|
|
resetScroll,
|
|
hashScrollIntoView,
|
|
startTransition,
|
|
viewTransition,
|
|
ignoreBlocker
|
|
});
|
|
}
|
|
};
|
|
const handleFocus = (_) => {
|
|
if (disabled) return;
|
|
if (preload) {
|
|
doPreload();
|
|
}
|
|
};
|
|
const handleTouchStart = handleFocus;
|
|
const handleEnter = (e) => {
|
|
if (disabled) return;
|
|
const eventTarget = e.target || {};
|
|
if (preload) {
|
|
if (eventTarget.preloadTimeout) {
|
|
return;
|
|
}
|
|
eventTarget.preloadTimeout = setTimeout(() => {
|
|
eventTarget.preloadTimeout = null;
|
|
doPreload();
|
|
}, preloadDelay);
|
|
}
|
|
};
|
|
const handleLeave = (e) => {
|
|
if (disabled) return;
|
|
const eventTarget = e.target || {};
|
|
if (eventTarget.preloadTimeout) {
|
|
clearTimeout(eventTarget.preloadTimeout);
|
|
eventTarget.preloadTimeout = null;
|
|
}
|
|
};
|
|
const composeHandlers = (handlers) => (e) => {
|
|
var _a;
|
|
(_a = e.persist) == null ? void 0 : _a.call(e);
|
|
handlers.filter(Boolean).forEach((handler) => {
|
|
if (e.defaultPrevented) return;
|
|
handler(e);
|
|
});
|
|
};
|
|
const resolvedActiveProps = isActive ? functionalUpdate(activeProps, {}) ?? {} : {};
|
|
const resolvedInactiveProps = isActive ? {} : functionalUpdate(inactiveProps, {});
|
|
const resolvedClassName = [
|
|
className,
|
|
resolvedActiveProps.className,
|
|
resolvedInactiveProps.className
|
|
].filter(Boolean).join(" ");
|
|
const resolvedStyle = {
|
|
...style,
|
|
...resolvedActiveProps.style,
|
|
...resolvedInactiveProps.style
|
|
};
|
|
return {
|
|
...propsSafeToSpread,
|
|
...resolvedActiveProps,
|
|
...resolvedInactiveProps,
|
|
href: disabled ? void 0 : next.maskedLocation ? router.history.createHref(next.maskedLocation.href) : router.history.createHref(next.href),
|
|
ref: innerRef,
|
|
onClick: composeHandlers([onClick, handleClick]),
|
|
onFocus: composeHandlers([onFocus, handleFocus]),
|
|
onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
|
|
onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
|
|
onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
|
|
disabled: !!disabled,
|
|
target,
|
|
...Object.keys(resolvedStyle).length && { style: resolvedStyle },
|
|
...resolvedClassName && { className: resolvedClassName },
|
|
...disabled && {
|
|
role: "link",
|
|
"aria-disabled": true
|
|
},
|
|
...isActive && { "data-status": "active", "aria-current": "page" },
|
|
...isTransitioning && { "data-transitioning": "transitioning" }
|
|
};
|
|
}
|
|
function createLink(Comp) {
|
|
return React.forwardRef(function CreatedLink(props, ref) {
|
|
return /* @__PURE__ */ jsx(Link, { ...props, _asChild: Comp, ref });
|
|
});
|
|
}
|
|
const Link = React.forwardRef(
|
|
(props, ref) => {
|
|
const { _asChild, ...rest } = props;
|
|
const {
|
|
type: _type,
|
|
ref: innerRef,
|
|
...linkProps
|
|
} = useLinkProps(rest, ref);
|
|
const children = typeof rest.children === "function" ? rest.children({
|
|
isActive: linkProps["data-status"] === "active"
|
|
}) : rest.children;
|
|
if (typeof _asChild === "undefined") {
|
|
delete linkProps.disabled;
|
|
}
|
|
return React.createElement(
|
|
_asChild ? _asChild : "a",
|
|
{
|
|
...linkProps,
|
|
ref: innerRef
|
|
},
|
|
children
|
|
);
|
|
}
|
|
);
|
|
function isCtrlEvent(e) {
|
|
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
|
|
}
|
|
const linkOptions = (options) => {
|
|
return options;
|
|
};
|
|
export {
|
|
Link,
|
|
createLink,
|
|
linkOptions,
|
|
useLinkProps
|
|
};
|
|
//# sourceMappingURL=link.js.map
|