From 54542d4c854e2bf2ac3ffe97158b916bc021a95f Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Sun, 7 Apr 2024 00:58:03 +0800 Subject: [PATCH] feat(client): support relative route --- e2e/docs/components/route-link.md | 27 ++++ e2e/tests/components/route-link.cy.ts | 24 +++- packages/client/src/components/RouteLink.ts | 125 +++++++++--------- packages/client/src/router/resolveRoute.ts | 3 +- .../client/src/router/resolveRoutePath.ts | 14 +- 5 files changed, 126 insertions(+), 67 deletions(-) diff --git a/e2e/docs/components/route-link.md b/e2e/docs/components/route-link.md index f271438c6e..5861096cf3 100644 --- a/e2e/docs/components/route-link.md +++ b/e2e/docs/components/route-link.md @@ -28,13 +28,19 @@ - text - text +- text +- text - text - text +- text +- text ### Class - text - text +- text +- text ### Attrs @@ -42,11 +48,17 @@ - text - text - text +- text +- text +- text +- text ### Slots - text - texttext +- text +- texttext ### Hash and query @@ -56,9 +68,24 @@ - text - text - text +- text +- text +- text +- text +- text +- text - text - text - text - text - text - text + +### Relative + +- text +- text +- text +- text +- text +- text diff --git a/e2e/tests/components/route-link.cy.ts b/e2e/tests/components/route-link.cy.ts index f11b7df99a..f3a48741af 100644 --- a/e2e/tests/components/route-link.cy.ts +++ b/e2e/tests/components/route-link.cy.ts @@ -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') @@ -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') }) }) @@ -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]) }) }) @@ -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`, @@ -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]) + }) + }) }) diff --git a/packages/client/src/components/RouteLink.ts b/packages/client/src/components/RouteLink.ts index a246bba061..d400eb6282 100644 --- a/packages/client/src/components/RouteLink.ts +++ b/packages/client/src/components/RouteLink.ts @@ -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' @@ -23,29 +23,6 @@ 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. * @@ -53,42 +30,68 @@ export interface RouteLinkProps extends HTMLAttributes { * * It's recommended to use `RouteLink` in VuePress. */ -export const RouteLink: FunctionalComponent< - RouteLinkProps, - Record, - { - 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?.(), + ) + }, +}) diff --git a/packages/client/src/router/resolveRoute.ts b/packages/client/src/router/resolveRoute.ts index ebfe486855..3f4a3ca541 100644 --- a/packages/client/src/router/resolveRoute.ts +++ b/packages/client/src/router/resolveRoute.ts @@ -13,8 +13,9 @@ export interface ResolvedRoute */ export const resolveRoute = ( path: string, + current?: string, ): ResolvedRoute => { - const routePath = resolveRoutePath(path) + const routePath = resolveRoutePath(path, current) const route = routes.value[routePath] ?? { ...routes.value['/404.html'], notFound: true, diff --git a/packages/client/src/router/resolveRoutePath.ts b/packages/client/src/router/resolveRoutePath.ts index b820df8778..b1ced1f41a 100644 --- a/packages/client/src/router/resolveRoutePath.ts +++ b/packages/client/src/router/resolveRoutePath.ts @@ -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