mirror of https://github.com/vuejs/vitepress
parent
5bb4730f7f
commit
077784fa34
@ -1,127 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="navbar-link">
|
|
||||||
<a
|
|
||||||
class="item"
|
|
||||||
:class="classes"
|
|
||||||
:href="href"
|
|
||||||
:target="target"
|
|
||||||
:rel="rel"
|
|
||||||
:aria-label="item.ariaLabel"
|
|
||||||
>
|
|
||||||
{{ item.text }} <OutboundLink v-if="isExternalLink" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, defineProps } from 'vue'
|
|
||||||
import { useRoute } from 'vitepress'
|
|
||||||
import { isExternal } from '../utils'
|
|
||||||
import type { DefaultTheme } from '../config'
|
|
||||||
import { useUrl } from '../composables/url'
|
|
||||||
import OutboundLink from './icons/OutboundLink.vue'
|
|
||||||
|
|
||||||
const { item } = defineProps<{
|
|
||||||
item: DefaultTheme.NavItemWithLink
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const normalizePath = (path: string): string => {
|
|
||||||
path = path
|
|
||||||
.replace(/#.*$/, '')
|
|
||||||
.replace(/\?.*$/, '')
|
|
||||||
.replace(/\.html$/, '')
|
|
||||||
if (path.endsWith('/')) {
|
|
||||||
path += 'index'
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
const { withBase } = useUrl()
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const classes = computed(() => ({
|
|
||||||
active: isActiveLink.value,
|
|
||||||
external: isExternalLink.value
|
|
||||||
}))
|
|
||||||
|
|
||||||
const isActiveLink = computed(() => {
|
|
||||||
return normalizePath(withBase(item.link)) === normalizePath(route.path)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isExternalLink = computed(() => {
|
|
||||||
return isExternal(item.link)
|
|
||||||
})
|
|
||||||
|
|
||||||
const href = computed(() => {
|
|
||||||
return isExternalLink.value ? item.link : withBase(item.link)
|
|
||||||
})
|
|
||||||
|
|
||||||
const target = computed(() => {
|
|
||||||
if (item.target) {
|
|
||||||
return item.target
|
|
||||||
}
|
|
||||||
|
|
||||||
return isExternalLink.value ? '_blank' : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const rel = computed(() => {
|
|
||||||
if (item.rel) {
|
|
||||||
return item.rel
|
|
||||||
}
|
|
||||||
|
|
||||||
return isExternalLink.value ? 'noopener noreferrer' : ''
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.navbar-link {
|
|
||||||
position: relative;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 720px) {
|
|
||||||
.navbar-link {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-link + .navbar-link,
|
|
||||||
.dropdown-wrapper + .navbar-link {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: -2px;
|
|
||||||
line-height: 40px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--c-text);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover,
|
|
||||||
.item.active {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 720px) {
|
|
||||||
.item {
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
font-size: .9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover,
|
|
||||||
.item.active {
|
|
||||||
color: var(--c-text);
|
|
||||||
border-bottom-color: var(--c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item.external:hover {
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,116 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav v-if="navData || repoInfo" class="navbar-links">
|
|
||||||
<template v-if="navData">
|
|
||||||
<template v-for="item of navData">
|
|
||||||
<NavDropdownLink v-if="item.items" :item="item" />
|
|
||||||
<NavBarLink v-else :item="item" />
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<NavDropdownLink v-if="localeCandidates" :item="localeCandidates" />
|
|
||||||
<NavBarLink v-if="repoInfo" :item="repoInfo" />
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useSiteData, useSiteDataByRoute, useRoute } from 'vitepress'
|
|
||||||
import { inBrowser } from '/@app/utils'
|
|
||||||
import NavBarLink from './NavBarLink.vue'
|
|
||||||
import NavDropdownLink from './NavDropdownLink.vue'
|
|
||||||
import type { DefaultTheme } from '../config'
|
|
||||||
|
|
||||||
const platforms = ['GitHub', 'GitLab', 'Bitbucket'].map(
|
|
||||||
(platform) => [platform, new RegExp(platform, 'i')] as const
|
|
||||||
)
|
|
||||||
|
|
||||||
const siteDataByRoute = useSiteDataByRoute()
|
|
||||||
const siteData = useSiteData()
|
|
||||||
const route = useRoute()
|
|
||||||
const repoInfo = computed(() => {
|
|
||||||
const theme = siteData.value.themeConfig as DefaultTheme.Config
|
|
||||||
const repo = theme.docsRepo || theme.repo
|
|
||||||
let text: string | undefined = theme.repoLabel
|
|
||||||
|
|
||||||
if (repo) {
|
|
||||||
const link = /^https?:/.test(repo) ? repo : `https://github.com/${repo}`
|
|
||||||
if (!text) {
|
|
||||||
// if no label is provided, deduce it from the repo url
|
|
||||||
const repoHosts = link.match(/^https?:\/\/[^/]+/)
|
|
||||||
if (repoHosts) {
|
|
||||||
const repoHost = repoHosts[0]
|
|
||||||
const foundPlatform = platforms.find(([_platform, re]) =>
|
|
||||||
re.test(repoHost)
|
|
||||||
)
|
|
||||||
text = foundPlatform && foundPlatform[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { link, text: text || 'Source' }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const localeCandidates = computed(() => {
|
|
||||||
const locales = siteData.value.themeConfig.locales
|
|
||||||
if (!locales) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const localeKeys = Object.keys(locales)
|
|
||||||
if (localeKeys.length <= 1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle site base
|
|
||||||
const siteBase = inBrowser ? siteData.value.base : '/'
|
|
||||||
const siteBaseWithoutSuffix = siteBase.endsWith('/')
|
|
||||||
? siteBase.slice(0, -1)
|
|
||||||
: siteBase
|
|
||||||
// remove site base in browser env
|
|
||||||
const routerPath = route.path.slice(siteBaseWithoutSuffix.length)
|
|
||||||
|
|
||||||
const currentLangBase = localeKeys.find((v) => {
|
|
||||||
if (v === '/') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return routerPath.startsWith(v)
|
|
||||||
})
|
|
||||||
const currentContentPath = currentLangBase
|
|
||||||
? routerPath.substring(currentLangBase.length - 1)
|
|
||||||
: routerPath
|
|
||||||
const candidates = localeKeys.map((v) => {
|
|
||||||
const localePath = v.endsWith('/') ? v.slice(0, -1) : v
|
|
||||||
return {
|
|
||||||
text: locales[v].label || locales[v].lang,
|
|
||||||
link: `${localePath}${currentContentPath}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentLangKey = currentLangBase ? currentLangBase : '/'
|
|
||||||
const selectText = locales[currentLangKey].selectText
|
|
||||||
? locales[currentLangKey].selectText
|
|
||||||
: 'Languages'
|
|
||||||
return {
|
|
||||||
text: selectText,
|
|
||||||
items: candidates
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const navData = computed(() => {
|
|
||||||
return siteDataByRoute.value.themeConfig.nav
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.navbar-links {
|
|
||||||
padding: 1rem 0;
|
|
||||||
border-bottom: 1px solid var(--c-divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 720px) {
|
|
||||||
.navbar-links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,249 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dropdown-wrapper" :class="{ open }">
|
<div class="nav-dropdown-link" :class="{ open }">
|
||||||
<button
|
<button class="button" :aria-label="item.ariaLabel" @click="toggle">
|
||||||
class="dropdown-title"
|
<span class="button-text">{{ item.text }}</span>
|
||||||
type="button"
|
<span class="button-arrow" :class="open ? 'down' : 'right'" />
|
||||||
:aria-label="item.ariaLabel"
|
|
||||||
@click="setOpen(!open)"
|
|
||||||
>
|
|
||||||
<span>{{ item.text }}</span>
|
|
||||||
<span class="arrow" :class="open ? 'down' : 'right'" />
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="nav-dropdown">
|
<ul class="dialog">
|
||||||
<li
|
<li v-for="item in item.items" :key="item.text" class="dialog-item">
|
||||||
v-for="(subItem, index) in item.items"
|
<NavDropdownLinkItem :item="item" />
|
||||||
:key="subItem.link || index"
|
|
||||||
class="dropdown-item"
|
|
||||||
>
|
|
||||||
<h4 v-if="subItem.items">{{ subItem.text }}</h4>
|
|
||||||
<ul v-if="subItem.items" class="dropdown-subitem-wrapper">
|
|
||||||
<li
|
|
||||||
v-for="childSubItem in subItem.items"
|
|
||||||
:key="childSubItem.link"
|
|
||||||
class="dropdown-subitem"
|
|
||||||
>
|
|
||||||
<NavBarLink
|
|
||||||
:item="childSubItem"
|
|
||||||
@focusout="
|
|
||||||
isLastItemOfArray(childSubItem, subItem.items) &&
|
|
||||||
isLastItemOfArray(subItem, item.items) &&
|
|
||||||
setOpen(false)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<NavBarLink
|
|
||||||
v-else
|
|
||||||
:item="subItem"
|
|
||||||
@focusout="isLastItemOfArray(subItem, item.items) && setOpen(false)"
|
|
||||||
/>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NavBarLink from './NavBarLink.vue'
|
import { watch, defineProps } from 'vue'
|
||||||
import { ref, watch, defineProps } from 'vue'
|
|
||||||
import { useRoute } from 'vitepress'
|
import { useRoute } from 'vitepress'
|
||||||
import type { DefaultTheme } from '../config'
|
import type { DefaultTheme } from '../config'
|
||||||
|
import NavDropdownLinkItem from './NavDropdownLinkItem.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
item: DefaultTheme.NavItemWithChildren
|
item: DefaultTheme.NavItemWithChildren
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const open = ref(false)
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
watch(
|
ref: open = false
|
||||||
() => route.path,
|
|
||||||
() => {
|
|
||||||
open.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const setOpen = (value: boolean) => {
|
watch(() => route.path, () => { open = false })
|
||||||
open.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLastItemOfArray = <T>(item: T, array: T[]) => {
|
function toggle() {
|
||||||
return array.length && array.indexOf(item) === array.length - 1
|
open = !open
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.dropdown-wrapper {
|
.nav-dropdown-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: 36px;
|
||||||
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .dropdown-title {
|
|
||||||
font: inherit;
|
|
||||||
color: var(--c-text);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
display: inline-block;
|
|
||||||
height: 1.75rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
padding: inherit;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .dropdown-title:hover {
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .dropdown-title .arrow {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: -1px;
|
|
||||||
margin-left: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item {
|
|
||||||
color: inherit;
|
|
||||||
line-height: 1.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item h4 {
|
|
||||||
margin: 0.45rem 0 0;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
padding: 0.45rem 1.5rem 0 1.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item .nav-item {
|
@media (min-width: 720px) {
|
||||||
margin-left: 0.5rem;
|
.nav-dropdown-link {
|
||||||
}
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem-wrapper {
|
.nav-dropdown-link:hover .dialog {
|
||||||
padding: 0;
|
display: block;
|
||||||
list-style: none;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper
|
.nav-dropdown-link.open {
|
||||||
.nav-dropdown
|
height: auto;
|
||||||
.dropdown-item
|
|
||||||
.dropdown-subitem-wrapper
|
|
||||||
.dropdown-subitem {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item a {
|
.button {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1.7rem;
|
border: 0;
|
||||||
position: relative;
|
padding: 0 1.5rem;
|
||||||
border-bottom: none;
|
width: 100%;
|
||||||
font-weight: 400;
|
text-align: left;
|
||||||
margin-bottom: 0;
|
line-height: 36px;
|
||||||
margin-left: 0;
|
font-family: var(--font-family-base);
|
||||||
padding: 0 1.5rem 0 1.25rem;
|
font-size: 1rem;
|
||||||
}
|
font-weight: 600;
|
||||||
|
color: var(--c-text);
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item a:hover {
|
white-space: nowrap;
|
||||||
color: var(--c-brand);
|
background-color: transparent;
|
||||||
}
|
cursor: pointer;
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item a.active {
|
|
||||||
color: var(--c-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item a.active::after {
|
|
||||||
content: '';
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5px solid var(--c-brand);
|
|
||||||
border-top: 3px solid transparent;
|
|
||||||
border-bottom: 3px solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
top: calc(50% - 1.5px);
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item:first-child h4 {
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper {
|
|
||||||
height: 1.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper:hover .nav-dropdown,
|
.button:focus {
|
||||||
.dropdown-wrapper.open .nav-dropdown {
|
outline: 0;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper.open:blur {
|
@media (min-width: 720px) {
|
||||||
display: none;
|
.button {
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: .9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper .dropdown-title .arrow {
|
.button-arrow {
|
||||||
border-left: 4px solid transparent;
|
display: inline-block;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: 8px;
|
||||||
|
border-top: 6px solid #ccc;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-top: 6px solid #aaa;
|
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown {
|
.button-arrow.right {
|
||||||
display: none;
|
transform: rotate(-90deg);
|
||||||
height: auto !important;
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-height: calc(100vh - 2.7rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 0.6rem 0;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-bottom-color: #ccc;
|
|
||||||
text-align: left;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 719px) {
|
@media (min-width: 720px) {
|
||||||
.dropdown-wrapper {
|
.button-arrow.right {
|
||||||
height: auto;
|
transform: rotate(0);
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .dropdown-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown {
|
.dialog {
|
||||||
position: relative;
|
margin: 0;
|
||||||
top: none;
|
padding: 0;
|
||||||
right: none;
|
list-style: none;
|
||||||
border: none;
|
}
|
||||||
padding: 4px 0;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper:hover .nav-dropdown {
|
@media (min-width: 720px) {
|
||||||
|
.dialog {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
position: absolute;
|
||||||
|
top: 26px;
|
||||||
.dropdown-wrapper.open .nav-dropdown {
|
right: -8px;
|
||||||
display: block;
|
border-radius: 6px;
|
||||||
}
|
padding: 12px 0;
|
||||||
|
min-width: 128px;
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item .nav-item {
|
background-color: var(--c-bg);
|
||||||
margin: 0;
|
box-shadow: var(--shadow-3);
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item .nav-link {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="nav-dropdown-link-item">
|
||||||
|
<a
|
||||||
|
class="item"
|
||||||
|
:class="classes"
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
:rel="rel"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
>
|
||||||
|
<span class="arrow" />
|
||||||
|
<span class="text">{{ text }}</span>
|
||||||
|
<span class="icon"><OutboundLink v-if="isExternal" /></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import type { DefaultTheme } from '../config'
|
||||||
|
import { useNavLink } from '../composables/navLink'
|
||||||
|
import OutboundLink from './icons/OutboundLink.vue'
|
||||||
|
|
||||||
|
const { item } = defineProps<{
|
||||||
|
item: DefaultTheme.NavItem
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
classes,
|
||||||
|
isActive,
|
||||||
|
isExternal,
|
||||||
|
href,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
|
ariaLabel,
|
||||||
|
text
|
||||||
|
} = useNavLink(item)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
padding: 0 1.5rem 0 2.5rem;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: .9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--c-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.item {
|
||||||
|
padding: 0 24px 0 12px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: .85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--c-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item.active .arrow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover,
|
||||||
|
.item.active {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item.external:hover {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.arrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-top: 6px solid #ccc;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px) rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="nav-link">
|
||||||
|
<a
|
||||||
|
class="item"
|
||||||
|
:class="classes"
|
||||||
|
:href="href"
|
||||||
|
:target="target"
|
||||||
|
:rel="rel"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
>
|
||||||
|
{{ text }} <OutboundLink v-if="isExternal" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import { useNavLink } from '../composables/navLink'
|
||||||
|
import OutboundLink from './icons/OutboundLink.vue'
|
||||||
|
|
||||||
|
const { item } = defineProps<{
|
||||||
|
item: DefaultTheme.NavItemWithLink
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
classes,
|
||||||
|
isActive,
|
||||||
|
isExternal,
|
||||||
|
href,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
|
ariaLabel,
|
||||||
|
text
|
||||||
|
} = useNavLink(item)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-link {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
line-height: 36px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--c-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover,
|
||||||
|
.item.active {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item.external:hover {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.item {
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: .9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover,
|
||||||
|
.item.active {
|
||||||
|
border-bottom-color: var(--c-brand);
|
||||||
|
color: var(--c-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<nav v-if="show" class="nav-links">
|
||||||
|
<template v-if="links">
|
||||||
|
<div v-for="item in links" :key="item.text" class="item">
|
||||||
|
<NavDropdownLink v-if="item.items" :item="item" />
|
||||||
|
<NavLink v-else :item="item" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="localeLinks" class="item">
|
||||||
|
<NavDropdownLink :item="localeLinks" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="repo" class="item">
|
||||||
|
<NavLink :item="repo" />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useSiteDataByRoute } from 'vitepress'
|
||||||
|
import { useLocaleLinks } from '../composables/nav'
|
||||||
|
import { useRepo } from '../composables/repo'
|
||||||
|
import NavLink from './NavLink.vue'
|
||||||
|
import NavDropdownLink from './NavDropdownLink.vue'
|
||||||
|
|
||||||
|
ref: siteByRoute = useSiteDataByRoute()
|
||||||
|
ref: localeLinks = useLocaleLinks()
|
||||||
|
ref: repo = useRepo()
|
||||||
|
|
||||||
|
ref: show = computed(() => links || repo)
|
||||||
|
|
||||||
|
ref: links = computed(() => siteByRoute.themeConfig.nav)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-links {
|
||||||
|
padding: .75rem 0;
|
||||||
|
border-bottom: 1px solid var(--c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 0 0;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item + .item {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,59 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useSiteData } from 'vitepress'
|
||||||
|
import { inBrowser } from '/@app/utils'
|
||||||
|
import type { DefaultTheme } from '../config'
|
||||||
|
|
||||||
|
export function useLocaleLinks() {
|
||||||
|
const route = useRoute()
|
||||||
|
const site = useSiteData()
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const theme = site.value.themeConfig as DefaultTheme.Config
|
||||||
|
const locales = theme.locales
|
||||||
|
|
||||||
|
if (!locales) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const localeKeys = Object.keys(locales)
|
||||||
|
|
||||||
|
if (localeKeys.length <= 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle site base
|
||||||
|
const siteBase = inBrowser ? site.value.base : '/'
|
||||||
|
|
||||||
|
const siteBaseWithoutSuffix = siteBase.endsWith('/')
|
||||||
|
? siteBase.slice(0, -1)
|
||||||
|
: siteBase
|
||||||
|
|
||||||
|
// remove site base in browser env
|
||||||
|
const routerPath = route.path.slice(siteBaseWithoutSuffix.length)
|
||||||
|
|
||||||
|
const currentLangBase = localeKeys.find((key) => {
|
||||||
|
return key === '/' ? false : routerPath.startsWith(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentContentPath = currentLangBase
|
||||||
|
? routerPath.substring(currentLangBase.length - 1)
|
||||||
|
: routerPath
|
||||||
|
|
||||||
|
const candidates = localeKeys.map((v) => {
|
||||||
|
const localePath = v.endsWith('/') ? v.slice(0, -1) : v
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: locales[v].label,
|
||||||
|
link: `${localePath}${currentContentPath}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentLangKey = currentLangBase ? currentLangBase : '/'
|
||||||
|
|
||||||
|
const selectText = locales[currentLangKey].selectText
|
||||||
|
? locales[currentLangKey].selectText
|
||||||
|
: 'Languages'
|
||||||
|
|
||||||
|
return { text: selectText, items: candidates }
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vitepress'
|
||||||
|
import type { DefaultTheme } from '../config'
|
||||||
|
import { isExternal as isExternalCheck } from '../utils'
|
||||||
|
import { useUrl } from '../composables/url'
|
||||||
|
|
||||||
|
export function useNavLink(item: DefaultTheme.NavItemWithLink) {
|
||||||
|
const route = useRoute()
|
||||||
|
const { withBase } = useUrl()
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
active: isActive.value,
|
||||||
|
external: isExternal.value
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isActive = computed(() => {
|
||||||
|
return normalizePath(withBase(item.link)) === normalizePath(route.path)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isExternal = computed(() => {
|
||||||
|
return isExternalCheck(item.link)
|
||||||
|
})
|
||||||
|
|
||||||
|
const href = computed(() => {
|
||||||
|
return isExternal.value ? item.link : withBase(item.link)
|
||||||
|
})
|
||||||
|
|
||||||
|
const target = computed(() => {
|
||||||
|
if (item.target) {
|
||||||
|
return item.target
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExternal.value ? '_blank' : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rel = computed(() => {
|
||||||
|
if (item.rel) {
|
||||||
|
return item.rel
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExternal.value ? 'noopener noreferrer' : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const ariaLabel = computed(() => item.ariaLabel)
|
||||||
|
|
||||||
|
const text = computed(() => item.text)
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes,
|
||||||
|
isActive,
|
||||||
|
isExternal,
|
||||||
|
href,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
|
ariaLabel,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
path = path
|
||||||
|
.replace(/#.*$/, '')
|
||||||
|
.replace(/\?.*$/, '')
|
||||||
|
.replace(/\.html$/, '')
|
||||||
|
|
||||||
|
if (path.endsWith('/')) {
|
||||||
|
path += 'index'
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useSiteDataByRoute } from 'vitepress'
|
||||||
|
import type { DefaultTheme } from '../config'
|
||||||
|
|
||||||
|
export const platforms = ['GitHub', 'GitLab', 'Bitbucket'].map((platform) => {
|
||||||
|
return [platform, new RegExp(platform, 'i')] as const
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useRepo() {
|
||||||
|
const site = useSiteDataByRoute()
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const theme = site.value.themeConfig as DefaultTheme.Config
|
||||||
|
const name = theme.docsRepo || theme.repo
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = getRepoUrl(name)
|
||||||
|
const text = getRepoText(link, theme.repoLabel)
|
||||||
|
|
||||||
|
return { text, link }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepoUrl(repo: string): string {
|
||||||
|
// if the full url is not provided, default to GitHub repo
|
||||||
|
return /^https?:/.test(repo) ? repo : `https://github.com/${repo}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepoText(url: string, text?: string): string {
|
||||||
|
if (text) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no label is provided, deduce it from the repo url
|
||||||
|
const hosts = url.match(/^https?:\/\/[^/]+/)
|
||||||
|
|
||||||
|
if (!hosts) {
|
||||||
|
return 'Source'
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = platforms.find(([_p, re]) => re.test(hosts[0]))
|
||||||
|
|
||||||
|
if (platform && platform[0]) {
|
||||||
|
return platform[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Source'
|
||||||
|
}
|
Loading…
Reference in new issue