forked from alephjs/aleph.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
link.ts
132 lines (122 loc) · 4.35 KB
/
link.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import React, { Children, cloneElement, ComponentType, CSSProperties, isValidElement, MouseEvent, PropsWithChildren, ReactElement, useCallback, useEffect, useMemo, useState } from 'https://esm.sh/react'
import { redirect } from './aleph.ts'
import events from './events.ts'
import { useRouter } from './hooks.ts'
import util, { reModuleExt } from './util.ts'
const prefetchedPageModules = new Set<string>()
interface LinkProps {
to: string
replace?: boolean
prefetch?: boolean
className?: string
style?: CSSProperties
}
export default function Link(props: PropsWithChildren<LinkProps>) {
const { to, replace = false, prefetch: prefetchNow = false, className, style, children } = props
const { pathname: currentPathname, query: currentQuery } = useRouter()
const currentHref = useMemo(() => {
return [currentPathname, currentQuery.toString()].filter(Boolean).join('?')
}, [currentPathname, currentQuery])
const href = useMemo(() => {
if (util.isHttpUrl(to)) {
return to
}
let [pathname, search] = util.splitBy(to, '?')
if (pathname.startsWith('/')) {
pathname = util.cleanPath(pathname)
} else {
pathname = util.cleanPath(currentPathname + '/' + pathname)
}
return [pathname, search].filter(Boolean).join('?')
}, [currentPathname, to])
const prefetch = useCallback(() => {
if (!util.isHttpUrl(href) && href !== currentHref && !prefetchedPageModules.has(href)) {
events.emit('fetch-page-module', { href })
prefetchedPageModules.add(href)
}
}, [href, currentHref])
const onClick = useCallback((e: MouseEvent) => {
e.preventDefault()
if (href !== currentHref) {
redirect(href, replace)
}
}, [href, currentHref, replace])
useEffect(() => {
if (prefetchNow) {
prefetch()
}
}, [prefetchNow, prefetch])
if (Children.count(children) === 1) {
const child = Children.toArray(children)[0]
if (isValidElement(child) && child.type === 'a') {
const { props } = child
return cloneElement(child, {
...props,
className: [className, props.className].filter(util.isNEString).join(' ') || undefined,
style: Object.assign({}, style, props.style),
href,
'aria-current': props['aria-current'] || 'page',
onClick: (e: MouseEvent) => {
if (util.isFunction(props.onClick)) {
props.onClick(e)
}
if (!e.defaultPrevented) {
onClick(e)
}
},
onMouseEnter: (e: MouseEvent) => {
if (util.isFunction(props.onMouseEnter)) {
props.onMouseEnter(e)
}
if (!e.defaultPrevented) {
prefetch()
}
}
})
}
}
return React.createElement(
'a',
{
className,
style,
href,
onClick,
onMouseEnter: prefetch,
'aria-current': 'page'
},
children
)
}
interface NavLinkProps extends LinkProps {
activeClassName?: string
activeStyle?: CSSProperties
}
export function NavLink(props: PropsWithChildren<NavLinkProps>) {
const { activeClassName = 'active', activeStyle, to, ...rest } = props
const { pathname: currentPathname } = useRouter()
const pathname = useMemo(() => {
if (util.isHttpUrl(to)) {
return to
}
let [pathname] = util.splitBy(to, '?')
if (pathname.startsWith('/')) {
pathname = util.cleanPath(pathname)
} else {
pathname = util.cleanPath(currentPathname + '/' + pathname)
}
return pathname
}, [currentPathname, to])
if (currentPathname === pathname) {
return React.createElement(
Link,
{
...rest,
to,
className: [rest.className?.trim(), activeClassName.trim()].filter(Boolean).join(' '),
style: Object.assign({}, rest.style, activeStyle)
}
)
}
return React.createElement(Link, { ...rest, to })
}