-
Notifications
You must be signed in to change notification settings - Fork 921
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(client): add AutoLink component
- Loading branch information
1 parent
ab3d6e6
commit 5a4700a
Showing
4 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# AutoLink | ||
|
||
<div id="route-link"> | ||
<AutoLink v-for="item in routeLinksConfig" v-bind="item" /> | ||
</div> | ||
|
||
<div id="external-link"> | ||
<AutoLink v-for="item in externalLinksConfig" v-bind="item" /> | ||
</div> | ||
|
||
<div id="config"> | ||
<AutoLink v-bind="{ text: 'text1', link: '/', ariaLabel: 'label' }" /> | ||
<AutoLink v-bind="{ text: 'text2', link: 'https://example.com/test/' }" /> | ||
</div> | ||
|
||
<script setup lang="ts"> | ||
import { AutoLink } from 'vuepress/client' | ||
|
||
const routeLinks = [ | ||
'/', | ||
'/README.md', | ||
'/index.html', | ||
'/non-existent', | ||
'/non-existent.md', | ||
'/non-existent.html', | ||
'/routes/non-ascii-paths/中文目录名/中文文件名', | ||
'/routes/non-ascii-paths/中文目录名/中文文件名.md', | ||
'/routes/non-ascii-paths/中文目录名/中文文件名.html', | ||
'/README.md#hash', | ||
'/README.md?query', | ||
'/README.md?query#hash', | ||
'/#hash', | ||
'/?query', | ||
'/?query#hash', | ||
'#hash', | ||
'?query', | ||
'?query#hash', | ||
'route-link', | ||
'route-link.md', | ||
'route-link.html', | ||
'not-existent', | ||
'not-existent.md', | ||
'not-existent.html', | ||
'../', | ||
'../README.md', | ||
'../404.md', | ||
'../404.html', | ||
] | ||
|
||
const routeLinksConfig = routeLinks.map((link) => ({ link, text: 'text' })) | ||
|
||
const externalLinks = [ | ||
'//example.com', | ||
'http://example.com', | ||
'https://example.com', | ||
'mailto:[email protected]', | ||
'tel:+1234567890', | ||
] | ||
|
||
const externalLinksConfig = externalLinks.map((link) => ({ link, text: 'text' })) | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { expect, test } from '@playwright/test' | ||
import { BASE } from '../../utils/env' | ||
|
||
test.beforeEach(async ({ page }) => { | ||
await page.goto('components/auto-link.html') | ||
}) | ||
|
||
test('should render route-link correctly', async ({ page }) => { | ||
for (const el of await page | ||
.locator('.e2e-theme-content #route-link a') | ||
.all()) { | ||
await expect(el).toHaveAttribute('class', /route-link/) | ||
} | ||
}) | ||
|
||
test('should render external-link correctly', async ({ page }) => { | ||
for (const el of await page | ||
.locator('.e2e-theme-content #external-link a') | ||
.all()) { | ||
await expect(el).toHaveAttribute('class', /external-link/) | ||
} | ||
}) | ||
|
||
test('should render config correctly', async ({ page }) => { | ||
const locator = page.locator('.e2e-theme-content #config a') | ||
|
||
await expect(await locator.nth(0)).toHaveText('text1') | ||
await expect(await locator.nth(0)).toHaveAttribute('href', BASE) | ||
await expect(await locator.nth(0)).toHaveAttribute('aria-label', 'label') | ||
|
||
await expect(await locator.nth(1)).toHaveText('text2') | ||
await expect(await locator.nth(1)).toHaveAttribute( | ||
'href', | ||
'https://example.com/test/', | ||
) | ||
await expect(await locator.nth(1)).toHaveAttribute('target', '_blank') | ||
await expect(await locator.nth(1)).toHaveAttribute( | ||
'rel', | ||
'noopener noreferrer', | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
import { isLinkWithProtocol } from '@vuepress/shared' | ||
import type { SlotsType, VNode } from 'vue' | ||
import { computed, defineComponent, h } from 'vue' | ||
import { useRoute } from 'vue-router' | ||
import { useSiteData } from '../composables/index.js' | ||
import { RouteLink } from './RouteLink.js' | ||
|
||
export interface AutoLinkConfig { | ||
/** | ||
* Text of item | ||
* | ||
* 项目文字 | ||
*/ | ||
text: string | ||
|
||
/** | ||
* Aria label of item | ||
* | ||
* 项目无障碍标签 | ||
*/ | ||
ariaLabel?: string | ||
|
||
/** | ||
* Link of item | ||
* | ||
* 当前页面链接 | ||
*/ | ||
link: string | ||
|
||
/** | ||
* Rel of `<a>` tag | ||
* | ||
* `<a>` 标签的 `rel` 属性 | ||
*/ | ||
rel?: string | ||
|
||
/** | ||
* Target of `<a>` tag | ||
* | ||
* `<a>` 标签的 `target` 属性 | ||
*/ | ||
target?: string | ||
|
||
/** | ||
* Regexp mode to be active | ||
* | ||
* 匹配激活的正则表达式 | ||
*/ | ||
activeMatch?: string | ||
} | ||
|
||
export const AutoLink = defineComponent({ | ||
name: 'AutoLink', | ||
|
||
props: { | ||
/** | ||
* Text of item | ||
* | ||
* 项目文字 | ||
*/ | ||
text: { | ||
type: String, | ||
required: true, | ||
}, | ||
|
||
/** | ||
* Link of item | ||
* | ||
* 当前页面链接 | ||
*/ | ||
link: { | ||
type: String, | ||
required: true, | ||
}, | ||
|
||
/** | ||
* Aria label of item | ||
* | ||
* 项目无障碍标签 | ||
*/ | ||
ariaLabel: { | ||
type: String, | ||
default: '', | ||
}, | ||
|
||
/** | ||
* Rel of `<a>` tag | ||
* | ||
* `<a>` 标签的 `rel` 属性 | ||
*/ | ||
rel: { | ||
type: String, | ||
default: '', | ||
}, | ||
|
||
/** | ||
* Target of `<a>` tag | ||
* | ||
* `<a>` 标签的 `target` 属性 | ||
*/ | ||
target: { | ||
type: String, | ||
default: '', | ||
}, | ||
|
||
/** | ||
* Whether it's active only when exact match | ||
* | ||
* 是否当恰好匹配时激活 | ||
*/ | ||
exact: Boolean, | ||
|
||
/** | ||
* Regexp mode to be active | ||
* | ||
* @description has higher priority than exact | ||
* | ||
* 匹配激活的正则表达式 | ||
* | ||
* @description 比 exact 的优先级更高 | ||
*/ | ||
activeMatch: { | ||
type: [String, RegExp], | ||
default: '', | ||
}, | ||
}, | ||
|
||
slots: Object as SlotsType<{ | ||
default?: () => VNode[] | VNode | ||
before?: () => VNode[] | VNode | null | ||
after?: () => VNode[] | VNode | null | ||
}>, | ||
|
||
setup(props, { slots }) { | ||
const route = useRoute() | ||
const siteData = useSiteData() | ||
|
||
// If the link has non-http protocol | ||
const withProtocol = computed(() => isLinkWithProtocol(props.link)) | ||
|
||
// Resolve the `target` attr | ||
const linkTarget = computed( | ||
() => props.target || (withProtocol.value ? '_blank' : undefined), | ||
) | ||
|
||
// If the `target` attr is "_blank" | ||
const isBlankTarget = computed(() => linkTarget.value === '_blank') | ||
|
||
// Whether the link is internal | ||
const isInternal = computed( | ||
() => !withProtocol.value && !isBlankTarget.value, | ||
) | ||
|
||
// Resolve the `rel` attr | ||
const linkRel = computed( | ||
() => props.rel || (isBlankTarget.value ? 'noopener noreferrer' : null), | ||
) | ||
|
||
// Resolve the `aria-label` attr | ||
const linkAriaLabel = computed(() => props.ariaLabel ?? props.text) | ||
|
||
// Should be active when current route is a subpath of this link | ||
const shouldBeActiveInSubpath = computed(() => { | ||
// Should not be active in `exact` mode | ||
if (props.exact) return false | ||
|
||
const localePaths = Object.keys(siteData.value.locales) | ||
|
||
return localePaths.length | ||
? // Check all the locales | ||
localePaths.every((key) => key !== props.link) | ||
: // Check root | ||
props.link !== '/' | ||
}) | ||
|
||
// If this link is active | ||
const isActive = computed(() => { | ||
if (!isInternal.value) return false | ||
|
||
if (props.activeMatch) | ||
return ( | ||
props.activeMatch instanceof RegExp | ||
? props.activeMatch | ||
: new RegExp(props.activeMatch, 'u') | ||
).test(route.path) | ||
|
||
// If this link is active in subpath | ||
if (shouldBeActiveInSubpath.value) | ||
return route.path.startsWith(props.link) | ||
|
||
return route.path === props.link | ||
}) | ||
|
||
return (): VNode => { | ||
const { before, after, default: defaultSlot } = slots | ||
|
||
const content = defaultSlot?.() || [ | ||
before ? before() : null, | ||
props.text, | ||
after?.(), | ||
] | ||
|
||
return isInternal.value | ||
? h( | ||
RouteLink, | ||
{ | ||
'class': 'auto-link', | ||
'to': props.link, | ||
'active': isActive.value, | ||
'aria-label': linkAriaLabel.value, | ||
}, | ||
() => content, | ||
) | ||
: h( | ||
'a', | ||
{ | ||
'class': 'auto-link external-link', | ||
'href': props.link, | ||
'rel': linkRel.value, | ||
'target': linkTarget.value, | ||
'aria-label': linkAriaLabel.value, | ||
}, | ||
content, | ||
) | ||
} | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './AutoLink.js' | ||
export * from './ClientOnly.js' | ||
export * from './Content.js' | ||
export * from './RouteLink.js' |