main
import { type MouseEvent, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
/** 无协议但形如域名(`www.example.com/x`)的 href 按站外链接处理。 */
const HOST_LIKE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i
/**
* 富文本(Markdown / 服务端 embed HTML)渲染出的链接元素不允许直接跳转:
* 一旦放行就是整页刷新。这个处理器是它们唯一的出口——
*
* - `#fragment`、`mailto:` 等非 http(s) 与带修饰键的点击仍交给浏览器;
* - 站内应用路径走 react-router(SPA 内部切换);
* - 站外地址与 `/api/` 下载地址开新标签页。
*/
export function useSpaLinkClick() {
const navigate = useNavigate()
return useCallback(
(event: MouseEvent<HTMLElement>) => {
if (event.defaultPrevented || event.button !== 0) return
// 修饰键是「新标签打开」的浏览器约定:放行即可,当前文档不会刷新。
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
const target = event.target instanceof Element ? event.target : null
const href = target?.closest('a')?.getAttribute('href')
if (!href || href.startsWith('#')) return
const external = !href.startsWith('/') && HOST_LIKE.test(href.split(/[/?#]/, 1)[0])
let url: URL
try {
url = new URL(external ? `https://${href}` : href, window.location.href)
} catch {
return
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') return
event.preventDefault()
if (!external && url.origin === window.location.origin && !url.pathname.startsWith('/api/')) {
navigate(`${url.pathname}${url.search}${url.hash}`)
return
}
window.open(url.href, '_blank', 'noopener,noreferrer')
},
[navigate],
)
}