Skip to content

Commit

Permalink
feat(client): support relative route
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Apr 6, 2024
1 parent 950f158 commit 54542d4
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 67 deletions.
27 changes: 27 additions & 0 deletions e2e/docs/components/route-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,37 @@

- <RouteLink to="/README.md" active="">text</RouteLink>
- <RouteLink to="/README.md" active>text</RouteLink>
- <RouteLink to="/" active="">text</RouteLink>
- <RouteLink to="/" active>text</RouteLink>
- <RouteLink to="/README.md" :active="false">text</RouteLink>
- <RouteLink to="/README.md">text</RouteLink>
- <RouteLink to="/" :active="false">text</RouteLink>
- <RouteLink to="/">text</RouteLink>

### Class

- <RouteLink to="/README.md" class="custom-class">text</RouteLink>
- <RouteLink to="/README.md" active class="custom-class">text</RouteLink>
- <RouteLink to="/" class="custom-class">text</RouteLink>
- <RouteLink to="/" active class="custom-class">text</RouteLink>

### Attrs

- <RouteLink to="/README.md" title="Title">text</RouteLink>
- <RouteLink to="/README.md" target="_blank">text</RouteLink>
- <RouteLink to="/README.md" rel="noopener">text</RouteLink>
- <RouteLink to="/README.md" aria-label="test">text</RouteLink>
- <RouteLink to="/" title="Title">text</RouteLink>
- <RouteLink to="/" target="_blank">text</RouteLink>
- <RouteLink to="/" rel="noopener">text</RouteLink>
- <RouteLink to="/" aria-label="test">text</RouteLink>

### Slots

- <RouteLink to="/README.md"><span>text</span></RouteLink>
- <RouteLink to="/README.md"><span>text</span><span>text</span></RouteLink>
- <RouteLink to="/"><span>text</span></RouteLink>
- <RouteLink to="/"><span>text</span><span>text</span></RouteLink>

### Hash and query

Expand All @@ -56,9 +68,24 @@
- <RouteLink to="/README.md?query=1#hash">text</RouteLink>
- <RouteLink to="/README.md?query=1&query=2#hash">text</RouteLink>
- <RouteLink to="/README.md#hash?query=1&query=2">text</RouteLink>
- <RouteLink to="/#hash">text</RouteLink>
- <RouteLink to="/?query">text</RouteLink>
- <RouteLink to="/?query#hash">text</RouteLink>
- <RouteLink to="/?query=1#hash">text</RouteLink>
- <RouteLink to="/?query=1&query=2#hash">text</RouteLink>
- <RouteLink to="/#hash?query=1&query=2">text</RouteLink>
- <RouteLink to="#hash">text</RouteLink>
- <RouteLink to="?query">text</RouteLink>
- <RouteLink to="?query#hash">text</RouteLink>
- <RouteLink to="?query=1#hash">text</RouteLink>
- <RouteLink to="?query=1&query=2#hash">text</RouteLink>
- <RouteLink to="#hash?query=1&query=2">text</RouteLink>

### Relative

- <RouteLink to="../README.md">text</RouteLink>
- <RouteLink to="../404.md">text</RouteLink>
- <RouteLink to="not-exist.md">text</RouteLink>
- <RouteLink to="../">text</RouteLink>
- <RouteLink to="../404.html">text</RouteLink>
- <RouteLink to="not-exist.html">text</RouteLink>
24 changes: 21 additions & 3 deletions e2e/tests/components/route-link.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ it('RouteLink', () => {

cy.get(`.e2e-theme-content #active + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
if (index < 2) {
if (index < 4) {
cy.get('a').should('have.attr', 'class', 'route-link route-link-active')
} else {
cy.get('a').should('have.attr', 'class', 'route-link')
Expand All @@ -60,7 +60,7 @@ it('RouteLink', () => {
]
cy.get(`.e2e-theme-content #class + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
cy.get('a').should('have.attr', 'class', classResults[index])
cy.get('a').should('have.attr', 'class', classResults[index % 2])
cy.get('a').should('have.text', 'text')
})
})
Expand All @@ -70,7 +70,7 @@ it('RouteLink', () => {

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

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

const HASH_AND_QUERY_RESULTS = [
`${E2E_BASE}#hash`,
`${E2E_BASE}?query`,
`${E2E_BASE}?query#hash`,
`${E2E_BASE}?query=1#hash`,
`${E2E_BASE}?query=1&query=2#hash`,
`${E2E_BASE}#hash?query=1&query=2`,
`${E2E_BASE}#hash`,
`${E2E_BASE}?query`,
`${E2E_BASE}?query#hash`,
Expand All @@ -105,4 +111,16 @@ it('RouteLink', () => {
cy.get('a').should('have.attr', 'href', HASH_AND_QUERY_RESULTS[index])
})
})

const RELATIVE_RESULTS = [
E2E_BASE,
`${E2E_BASE}404.html`,
`${E2E_BASE}components/not-exist.html`,
]

cy.get(`.e2e-theme-content #relative + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
cy.get('a').should('have.attr', 'href', RELATIVE_RESULTS[index % 3])
})
})
})
125 changes: 64 additions & 61 deletions packages/client/src/components/RouteLink.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { h } from 'vue'
import type { FunctionalComponent, HTMLAttributes, VNode } from 'vue'
import { useRouter } from 'vue-router'
import { computed, defineComponent, h } from 'vue'
import type { SlotsType, VNode } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { resolveRoutePath } from '../router/index.js'
import { withBase } from '../utils/index.js'

Expand All @@ -23,72 +23,75 @@ const guardEvent = (event: MouseEvent): boolean | void => {
return true
}

export interface RouteLinkProps extends HTMLAttributes {
/**
* Whether the link is active to have an active class
*
* Notice that the active status is not automatically determined according to the current route.
*
* @default false
*/
active?: boolean

/**
* The class to add when the link is active
*
* @default 'route-link-active'
*/
activeClass?: string

/**
* The route path to link to
*/
to: string
}

/**
* Component to render a link to another route.
*
* It's similar to `RouterLink` in `vue-router`, but more lightweight.
*
* It's recommended to use `RouteLink` in VuePress.
*/
export const RouteLink: FunctionalComponent<
RouteLinkProps,
Record<never, never>,
{
default: () => string | VNode | (string | VNode)[]
}
> = (
{ active = false, activeClass = 'route-link-active', to, ...attrs },
{ slots },
) => {
const router = useRouter()
const resolvedPath = resolveRoutePath(to)
export const RouteLink = defineComponent({
name: 'RouteLink',

props: {
/**
* The route path to link to
*/
to: {
type: String,
required: true,
},

const path =
// only anchor or query
resolvedPath.startsWith('#') || resolvedPath.startsWith('?')
? resolvedPath
: withBase(resolvedPath)
/**
* Whether the link is active to have an active class
*
* Notice that the active status is not automatically determined according to the current route.
*/
active: Boolean,

return h(
'a',
{
...attrs,
class: ['route-link', { [activeClass]: active }],
href: path,
onClick: (event: MouseEvent = {} as MouseEvent) => {
guardEvent(event) ? router.push(to).catch() : Promise.resolve()
},
/**
* The class to add when the link is active
*/
activeClass: {
type: String,
default: 'route-link-active',
},
slots.default?.(),
)
}
},

RouteLink.displayName = 'RouteLink'
RouteLink.props = {
active: Boolean,
activeClass: String,
to: String,
}
slots: Object as SlotsType<{
default: () => string | VNode | (string | VNode)[]
}>,

setup(props, { slots }) {
const router = useRouter()
const route = useRoute()

const path = computed(() => {
if (
// as is
['#', '?', 'http://', 'https://', '//'].some((prefix) =>
props.to.startsWith(prefix),
)
) {
return props.to
}

return withBase(resolveRoutePath(props.to, route.path))
})

return () =>
h(
'a',
{
class: ['route-link', { [props.activeClass]: props.active }],
href: path.value,
onClick: (event: MouseEvent = {} as MouseEvent) => {
if (guardEvent(event)) {
router.push(path.value).catch()
}
},
},
slots.default?.(),
)
},
})
3 changes: 2 additions & 1 deletion packages/client/src/router/resolveRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export interface ResolvedRoute<T extends RouteMeta = RouteMeta>
*/
export const resolveRoute = <T extends RouteMeta = RouteMeta>(
path: string,
current?: string,
): ResolvedRoute<T> => {
const routePath = resolveRoutePath(path)
const routePath = resolveRoutePath(path, current)
const route = routes.value[routePath] ?? {
...routes.value['/404.html'],
notFound: true,
Expand Down
14 changes: 12 additions & 2 deletions packages/client/src/router/resolveRoutePath.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { normalizeRoutePath } from '@vuepress/shared'
import { redirects, routes } from '../internal/routes.js'

const FAKE_HOST = 'http://.'

/**
* Resolve route path with given raw path
*/
export const resolveRoutePath = (path: string): string => {
export const resolveRoutePath = (path: string, current?: string): string => {
let routePath = path

if (current) {
const loc = current.slice(0, current.lastIndexOf('/'))

routePath = new URL(`${loc}/${path}`, FAKE_HOST).pathname
}

// normalized path
const normalizedPath = normalizeRoutePath(path)
const normalizedPath = normalizeRoutePath(routePath)
if (routes.value[normalizedPath]) return normalizedPath

// encoded path
Expand Down

0 comments on commit 54542d4

Please sign in to comment.