Skip to content

Commit 90e218a

Browse files
committed
feat(client): support relative route and add AutoLink
1 parent 950f158 commit 90e218a

File tree

12 files changed

+494
-146
lines changed

12 files changed

+494
-146
lines changed

e2e/docs/components/route-link.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,37 @@
2828

2929
- <RouteLink to="/README.md" active="">text</RouteLink>
3030
- <RouteLink to="/README.md" active>text</RouteLink>
31+
- <RouteLink to="/" active="">text</RouteLink>
32+
- <RouteLink to="/" active>text</RouteLink>
3133
- <RouteLink to="/README.md" :active="false">text</RouteLink>
3234
- <RouteLink to="/README.md">text</RouteLink>
35+
- <RouteLink to="/" :active="false">text</RouteLink>
36+
- <RouteLink to="/">text</RouteLink>
3337

3438
### Class
3539

3640
- <RouteLink to="/README.md" class="custom-class">text</RouteLink>
3741
- <RouteLink to="/README.md" active class="custom-class">text</RouteLink>
42+
- <RouteLink to="/" class="custom-class">text</RouteLink>
43+
- <RouteLink to="/" active class="custom-class">text</RouteLink>
3844

3945
### Attrs
4046

4147
- <RouteLink to="/README.md" title="Title">text</RouteLink>
4248
- <RouteLink to="/README.md" target="_blank">text</RouteLink>
4349
- <RouteLink to="/README.md" rel="noopener">text</RouteLink>
4450
- <RouteLink to="/README.md" aria-label="test">text</RouteLink>
51+
- <RouteLink to="/" title="Title">text</RouteLink>
52+
- <RouteLink to="/" target="_blank">text</RouteLink>
53+
- <RouteLink to="/" rel="noopener">text</RouteLink>
54+
- <RouteLink to="/" aria-label="test">text</RouteLink>
4555

4656
### Slots
4757

4858
- <RouteLink to="/README.md"><span>text</span></RouteLink>
4959
- <RouteLink to="/README.md"><span>text</span><span>text</span></RouteLink>
60+
- <RouteLink to="/"><span>text</span></RouteLink>
61+
- <RouteLink to="/"><span>text</span><span>text</span></RouteLink>
5062

5163
### Hash and query
5264

@@ -56,9 +68,24 @@
5668
- <RouteLink to="/README.md?query=1#hash">text</RouteLink>
5769
- <RouteLink to="/README.md?query=1&query=2#hash">text</RouteLink>
5870
- <RouteLink to="/README.md#hash?query=1&query=2">text</RouteLink>
71+
- <RouteLink to="/#hash">text</RouteLink>
72+
- <RouteLink to="/?query">text</RouteLink>
73+
- <RouteLink to="/?query#hash">text</RouteLink>
74+
- <RouteLink to="/?query=1#hash">text</RouteLink>
75+
- <RouteLink to="/?query=1&query=2#hash">text</RouteLink>
76+
- <RouteLink to="/#hash?query=1&query=2">text</RouteLink>
5977
- <RouteLink to="#hash">text</RouteLink>
6078
- <RouteLink to="?query">text</RouteLink>
6179
- <RouteLink to="?query#hash">text</RouteLink>
6280
- <RouteLink to="?query=1#hash">text</RouteLink>
6381
- <RouteLink to="?query=1&query=2#hash">text</RouteLink>
6482
- <RouteLink to="#hash?query=1&query=2">text</RouteLink>
83+
84+
### Relative
85+
86+
- <RouteLink to="../README.md">text</RouteLink>
87+
- <RouteLink to="../404.md">text</RouteLink>
88+
- <RouteLink to="not-exist.md">text</RouteLink>
89+
- <RouteLink to="../">text</RouteLink>
90+
- <RouteLink to="../404.html">text</RouteLink>
91+
- <RouteLink to="not-exist.html">text</RouteLink>

e2e/tests/components/route-link.cy.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ it('RouteLink', () => {
4545

4646
cy.get(`.e2e-theme-content #active + ul > li`).each((el, index) => {
4747
cy.wrap(el).within(() => {
48-
if (index < 2) {
48+
if (index < 4) {
4949
cy.get('a').should('have.attr', 'class', 'route-link route-link-active')
5050
} else {
5151
cy.get('a').should('have.attr', 'class', 'route-link')
@@ -60,7 +60,7 @@ it('RouteLink', () => {
6060
]
6161
cy.get(`.e2e-theme-content #class + ul > li`).each((el, index) => {
6262
cy.wrap(el).within(() => {
63-
cy.get('a').should('have.attr', 'class', classResults[index])
63+
cy.get('a').should('have.attr', 'class', classResults[index % 2])
6464
cy.get('a').should('have.text', 'text')
6565
})
6666
})
@@ -70,7 +70,7 @@ it('RouteLink', () => {
7070

7171
cy.get(`.e2e-theme-content #attrs + ul > li`).each((el, index) => {
7272
cy.wrap(el).within(() => {
73-
cy.get('a').should('have.attr', attrName[index], attrValue[index])
73+
cy.get('a').should('have.attr', attrName[index % 4], attrValue[index % 4])
7474
})
7575
})
7676

@@ -86,6 +86,12 @@ it('RouteLink', () => {
8686
})
8787

8888
const HASH_AND_QUERY_RESULTS = [
89+
`${E2E_BASE}#hash`,
90+
`${E2E_BASE}?query`,
91+
`${E2E_BASE}?query#hash`,
92+
`${E2E_BASE}?query=1#hash`,
93+
`${E2E_BASE}?query=1&query=2#hash`,
94+
`${E2E_BASE}#hash?query=1&query=2`,
8995
`${E2E_BASE}#hash`,
9096
`${E2E_BASE}?query`,
9197
`${E2E_BASE}?query#hash`,
@@ -105,4 +111,16 @@ it('RouteLink', () => {
105111
cy.get('a').should('have.attr', 'href', HASH_AND_QUERY_RESULTS[index])
106112
})
107113
})
114+
115+
const RELATIVE_RESULTS = [
116+
E2E_BASE,
117+
`${E2E_BASE}404.html`,
118+
`${E2E_BASE}components/not-exist.html`,
119+
]
120+
121+
cy.get(`.e2e-theme-content #relative + ul > li`).each((el, index) => {
122+
cy.wrap(el).within(() => {
123+
cy.get('a').should('have.attr', 'href', RELATIVE_RESULTS[index % 3])
124+
})
125+
})
108126
})
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { isLinkWithProtocol } from '@vuepress/shared'
2+
import type { PropType, SlotsType, VNode } from 'vue'
3+
import { computed, defineComponent, h, toRef } from 'vue'
4+
import { useRoute } from 'vue-router'
5+
import { useSiteData } from '../composables/index.js'
6+
import { RouteLink } from './RouteLink.js'
7+
8+
export interface AutoLinkProps {
9+
/**
10+
* Text of item
11+
*
12+
* 项目文字
13+
*/
14+
text: string
15+
16+
/**
17+
* Aria label of item
18+
*
19+
* 项目无障碍标签
20+
*/
21+
ariaLabel?: string
22+
23+
/**
24+
* Link of item
25+
*
26+
* 当前页面链接
27+
*/
28+
link: string
29+
30+
/**
31+
* Rel of `<a>` tag
32+
*
33+
* `<a>` 标签的 `rel` 属性
34+
*/
35+
rel?: string
36+
37+
/**
38+
* Target of `<a>` tag
39+
*
40+
* `<a>` 标签的 `target` 属性
41+
*/
42+
target?: string
43+
44+
/**
45+
* Regexp mode to be active
46+
*
47+
* 匹配激活的正则表达式
48+
*/
49+
activeMatch?: string
50+
}
51+
52+
export const AutoLink = defineComponent({
53+
name: 'AutoLink',
54+
55+
props: {
56+
/**
57+
* @description Autolink config
58+
*/
59+
config: {
60+
type: Object as PropType<AutoLinkProps>,
61+
required: true,
62+
},
63+
64+
/**
65+
* @description Whether it's active only when exact match
66+
*/
67+
exact: Boolean,
68+
},
69+
70+
slots: Object as SlotsType<{
71+
default?: () => VNode[] | VNode
72+
before?: () => VNode[] | VNode | null
73+
after?: () => VNode[] | VNode | null
74+
}>,
75+
76+
setup(props, { slots }) {
77+
const route = useRoute()
78+
const siteData = useSiteData()
79+
80+
const config = toRef(props, 'config')
81+
82+
// If the link has non-http protocol
83+
const withProtocol = computed(() => isLinkWithProtocol(config.value.link))
84+
85+
// Resolve the `target` attr
86+
const linkTarget = computed(
87+
() => config.value.target || (withProtocol.value ? '_blank' : undefined),
88+
)
89+
90+
// If the `target` attr is "_blank"
91+
const isBlankTarget = computed(() => linkTarget.value === '_blank')
92+
93+
// Render `<RouteLink>` or not
94+
const renderRouteLink = computed(
95+
() => !withProtocol.value && !isBlankTarget.value,
96+
)
97+
98+
// Resolve the `rel` attr
99+
const linkRel = computed(
100+
() =>
101+
config.value.rel ||
102+
(isBlankTarget.value ? 'noopener noreferrer' : null),
103+
)
104+
105+
// Resolve the `aria-label` attr
106+
const linkAriaLabel = computed(
107+
() => config.value.ariaLabel || config.value.text,
108+
)
109+
110+
// Should be active when current route is a subpath of this link
111+
const shouldBeActiveInSubpath = computed(() => {
112+
// Should not be active in `exact` mode
113+
if (props.exact) return false
114+
115+
const localeKeys = Object.keys(siteData.value.locales)
116+
117+
return localeKeys.length
118+
? // Check all the locales
119+
localeKeys.every((key) => key !== config.value.link)
120+
: // Check root
121+
config.value.link !== '/'
122+
})
123+
124+
// If this link is active
125+
const isActive = computed(() =>
126+
renderRouteLink.value
127+
? config.value.activeMatch
128+
? new RegExp(config.value.activeMatch, 'u').test(route.path)
129+
: // If this link is active in subpath
130+
shouldBeActiveInSubpath.value
131+
? route.path.startsWith(config.value.link)
132+
: route.path === config.value.link
133+
: false,
134+
)
135+
136+
return (): VNode => {
137+
const { text, link } = config.value
138+
const { before, after, default: defaultSlot } = slots
139+
140+
const content = defaultSlot?.() || [
141+
before ? before() : null,
142+
text,
143+
after?.(),
144+
]
145+
146+
return renderRouteLink.value
147+
? h(
148+
RouteLink,
149+
{
150+
'class': 'auto-link',
151+
'to': link,
152+
'active': isActive.value,
153+
'aria-label': linkAriaLabel.value,
154+
// Class needs to be merged manually
155+
},
156+
() => content,
157+
)
158+
: h(
159+
'a',
160+
{
161+
'class': 'auto-link anchor-link',
162+
'href': link,
163+
'rel': linkRel.value,
164+
'target': linkTarget.value,
165+
'aria-label': linkAriaLabel.value,
166+
},
167+
content,
168+
)
169+
}
170+
},
171+
})

0 commit comments

Comments
 (0)