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>
|
||||
<div class="dropdown-wrapper" :class="{ open }">
|
||||
<button
|
||||
class="dropdown-title"
|
||||
type="button"
|
||||
:aria-label="item.ariaLabel"
|
||||
@click="setOpen(!open)"
|
||||
>
|
||||
<span>{{ item.text }}</span>
|
||||
<span class="arrow" :class="open ? 'down' : 'right'" />
|
||||
<div class="nav-dropdown-link" :class="{ open }">
|
||||
<button class="button" :aria-label="item.ariaLabel" @click="toggle">
|
||||
<span class="button-text">{{ item.text }}</span>
|
||||
<span class="button-arrow" :class="open ? 'down' : 'right'" />
|
||||
</button>
|
||||
|
||||
<ul class="nav-dropdown">
|
||||
<li
|
||||
v-for="(subItem, index) in item.items"
|
||||
: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)"
|
||||
/>
|
||||
<ul class="dialog">
|
||||
<li v-for="item in item.items" :key="item.text" class="dialog-item">
|
||||
<NavDropdownLinkItem :item="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NavBarLink from './NavBarLink.vue'
|
||||
import { ref, watch, defineProps } from 'vue'
|
||||
import { watch, defineProps } from 'vue'
|
||||
import { useRoute } from 'vitepress'
|
||||
import type { DefaultTheme } from '../config'
|
||||
import NavDropdownLinkItem from './NavDropdownLinkItem.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithChildren
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
open.value = false
|
||||
}
|
||||
)
|
||||
ref: open = false
|
||||
|
||||
const setOpen = (value: boolean) => {
|
||||
open.value = value
|
||||
}
|
||||
watch(() => route.path, () => { open = false })
|
||||
|
||||
const isLastItemOfArray = <T>(item: T, array: T[]) => {
|
||||
return array.length && array.indexOf(item) === array.length - 1
|
||||
function toggle() {
|
||||
open = !open
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dropdown-wrapper {
|
||||
<style scoped>
|
||||
.nav-dropdown-link {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@media (min-width: 720px) {
|
||||
.nav-dropdown-link {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem-wrapper {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
.nav-dropdown-link:hover .dialog {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-wrapper
|
||||
.nav-dropdown
|
||||
.dropdown-item
|
||||
.dropdown-subitem-wrapper
|
||||
.dropdown-subitem {
|
||||
font-size: 0.9em;
|
||||
.nav-dropdown-link.open {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown .dropdown-item a {
|
||||
.button {
|
||||
display: block;
|
||||
line-height: 1.7rem;
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
font-weight: 400;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
padding: 0 1.5rem 0 1.25rem;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown .dropdown-item a:hover {
|
||||
color: var(--c-brand);
|
||||
}
|
||||
|
||||
.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;
|
||||
border: 0;
|
||||
padding: 0 1.5rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
line-height: 36px;
|
||||
font-family: var(--font-family-base);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text);
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-wrapper:hover .nav-dropdown,
|
||||
.dropdown-wrapper.open .nav-dropdown {
|
||||
display: block;
|
||||
.button:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.dropdown-wrapper.open:blur {
|
||||
display: none;
|
||||
@media (min-width: 720px) {
|
||||
.button {
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 0;
|
||||
line-height: 24px;
|
||||
font-size: .9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-wrapper .dropdown-title .arrow {
|
||||
border-left: 4px solid transparent;
|
||||
.button-arrow {
|
||||
display: inline-block;
|
||||
margin-top: -1px;
|
||||
margin-left: 8px;
|
||||
border-top: 6px solid #ccc;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 6px solid #aaa;
|
||||
border-bottom: 0;
|
||||
border-left: 4px solid transparent;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown {
|
||||
display: none;
|
||||
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;
|
||||
.button-arrow.right {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 719px) {
|
||||
.dropdown-wrapper {
|
||||
height: auto;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .dropdown-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
@media (min-width: 720px) {
|
||||
.button-arrow.right {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown {
|
||||
position: relative;
|
||||
top: none;
|
||||
right: none;
|
||||
border: none;
|
||||
padding: 4px 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
.dialog {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dropdown-wrapper:hover .nav-dropdown {
|
||||
@media (min-width: 720px) {
|
||||
.dialog {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-wrapper.open .nav-dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown .dropdown-item .nav-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropdown-wrapper .nav-dropdown .dropdown-item .nav-link {
|
||||
font-size: 0.9rem;
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
right: -8px;
|
||||
border-radius: 6px;
|
||||
padding: 12px 0;
|
||||
min-width: 128px;
|
||||
background-color: var(--c-bg);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
}
|
||||
</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