refactor: code split optional features + improve typing

pull/165/head
Evan You 4 years ago
parent 170d72892e
commit 08b9e17a93

@ -1,26 +1,27 @@
// exports in this file are exposed to themes and md files via 'vitepress' // exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { useRoute, useSiteData } from 'vitepress'` // so the user can do `import { useRoute, useSiteData } from 'vitepress'`
import { ComponentOptions } from 'vue'
// generic types // generic types
export type { SiteData, PageData } from '/@types/shared' export type { SiteData, PageData } from '/@types/shared'
export type { Router, Route } from './router'
// theme types // theme types
export * from './theme' export * from './theme'
// composables // composables
export { useRouter, useRoute, Router, Route } from './router' export { useRouter, useRoute } from './router'
export { useSiteData } from './composables/siteData' export { useSiteData } from './composables/siteData'
export { useSiteDataByRoute } from './composables/siteDataByRoute' export { useSiteDataByRoute } from './composables/siteDataByRoute'
export { usePageData } from './composables/pageData' export { usePageData } from './composables/pageData'
export { useFrontmatter } from './composables/frontmatter' export { useFrontmatter } from './composables/frontmatter'
// utilities
export { inBrowser, joinPath } from './utils'
// components // components
export { Content } from './components/Content' export { Content } from './components/Content'
import { ComponentOptions } from 'vue'
import _Debug from './components/Debug.vue' import _Debug from './components/Debug.vue'
const Debug = _Debug as ComponentOptions const Debug = _Debug as ComponentOptions
export { Debug } export { Debug }

@ -10,3 +10,13 @@ declare module '@siteData' {
const data: string const data: string
export default data export default data
} }
// this module's typing is broken
declare module '@docsearch/js' {
function docsearch<T = any>(props: T): void
export default docsearch
}
declare module '@docsearch/css' {
export default string
}

@ -4,10 +4,7 @@
<NavBar> <NavBar>
<template #search> <template #search>
<slot name="navbar-search"> <slot name="navbar-search">
<AlgoliaSearchBox <AlgoliaSearchBox v-if="theme.algolia" :options="theme.algolia" />
v-if="$site.themeConfig.algolia"
:options="$site.themeConfig.algolia"
/>
</slot> </slot>
</template> </template>
</NavBar> </NavBar>
@ -41,10 +38,10 @@
<template #top> <template #top>
<slot name="page-top-ads"> <slot name="page-top-ads">
<CarbonAds <CarbonAds
v-if="$site.themeConfig.carbonAds" v-if="theme.carbonAds"
:key="'carbon' + $page.path" :key="'carbon' + page.relativePath"
:code="$site.themeConfig.carbonAds.carbon" :code="theme.carbonAds.carbon"
:placement="$site.themeConfig.carbonAds.placement" :placement="theme.carbonAds.placement"
/> />
</slot> </slot>
<slot name="page-top" /> <slot name="page-top" />
@ -53,10 +50,10 @@
<slot name="page-bottom" /> <slot name="page-bottom" />
<slot name="page-bottom-ads"> <slot name="page-bottom-ads">
<BuySellAds <BuySellAds
v-if="$site.themeConfig.carbonAds" v-if="theme.carbonAds && theme.carbonAds.custom"
:key="'custom' + $page.path" :key="'custom' + page.relativePath"
:code="$site.themeConfig.carbonAds.custom" :code="theme.carbonAds.custom"
:placement="$site.themeConfig.carbonAds.placement" :placement="theme.carbonAds.placement"
/> />
</slot> </slot>
</template> </template>
@ -67,24 +64,42 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch, defineAsyncComponent } from 'vue'
import {
useRoute,
useSiteData,
usePageData,
useSiteDataByRoute
} from 'vitepress'
import type { DefaultTheme } from './config'
// components
import NavBar from './components/NavBar.vue' import NavBar from './components/NavBar.vue'
import Home from './components/Home.vue' import Home from './components/Home.vue'
import ToggleSideBarButton from './components/ToggleSideBarButton.vue' import ToggleSideBarButton from './components/ToggleSideBarButton.vue'
import SideBar from './components/SideBar.vue' import SideBar from './components/SideBar.vue'
import Page from './components/Page.vue' import Page from './components/Page.vue'
import { useRoute, useSiteData, useSiteDataByRoute } from 'vitepress' const CarbonAds = defineAsyncComponent(
import AlgoliaSearchBox from './components/AlgoliaSearchBox.vue' () => import('./components/CarbonAds.vue')
import CarbonAds from './components/CarbonAds.vue' )
import BuySellAds from './components/BuySellAds.vue' const BuySellAds = defineAsyncComponent(
() => import('./components/BuySellAds.vue')
)
const AlgoliaSearchBox = defineAsyncComponent(
() => import('./components/AlgoliaSearchBox.vue')
)
// generic state
const route = useRoute() const route = useRoute()
const siteData = useSiteData() const siteData = useSiteData<DefaultTheme.Config>()
const siteRouteData = useSiteDataByRoute() const siteRouteData = useSiteDataByRoute()
const theme = computed(() => siteData.value.themeConfig)
const page = usePageData()
const openSideBar = ref(false) // home
const enableHome = computed(() => !!route.data.frontmatter.home) const enableHome = computed(() => !!route.data.frontmatter.home)
// navbar
const showNavbar = computed(() => { const showNavbar = computed(() => {
const { themeConfig } = siteRouteData.value const { themeConfig } = siteRouteData.value
const { frontmatter } = route.data const { frontmatter } = route.data
@ -99,6 +114,9 @@ const showNavbar = computed(() => {
) )
}) })
// sidebar
const openSideBar = ref(false)
const showSidebar = computed(() => { const showSidebar = computed(() => {
const { frontmatter } = route.data const { frontmatter } = route.data
const { themeConfig } = siteRouteData.value const { themeConfig } = siteRouteData.value
@ -111,6 +129,17 @@ const showSidebar = computed(() => {
) )
}) })
const toggleSidebar = (to?: boolean) => {
openSideBar.value = typeof to === 'boolean' ? to : !openSideBar.value
}
const hideSidebar = toggleSidebar.bind(null, false)
// close the sidebar when navigating to a different location
watch(route, hideSidebar)
// TODO: route only changes when the pathname changes
// listening to hashchange does nothing because it's prevented in router
// page classes
const pageClasses = computed(() => { const pageClasses = computed(() => {
return [ return [
{ {
@ -120,14 +149,4 @@ const pageClasses = computed(() => {
} }
] ]
}) })
const toggleSidebar = (to) => {
openSideBar.value = typeof to === 'boolean' ? to : !openSideBar.value
}
const hideSidebar = toggleSidebar.bind(null, false)
// close the sidebar when navigating to a different location
watch(route, hideSidebar)
// TODO: route only changes when the pathname changes
// listening to hashchange does nothing because it's prevented in router
</script> </script>

@ -2,23 +2,36 @@
<div class="algolia-search-box" id="docsearch" /> <div class="algolia-search-box" id="docsearch" />
</template> </template>
<script lang="ts">
// TODO: @vue/compiler-sfc currently has a bug that removes `import 'foo'`
// statements in <script setup> so we put it here for now
import '@docsearch/css'
</script>
<script setup lang="ts"> <script setup lang="ts">
import type { AlgoliaSearchOptions } from 'algoliasearch'
import { useRoute, useRouter } from 'vitepress' import { useRoute, useRouter } from 'vitepress'
import { defineProps, getCurrentInstance, onMounted, watch } from 'vue' import { defineProps, getCurrentInstance, onMounted, watch } from 'vue'
import docsearch from '@docsearch/js'
import type { DefaultTheme } from '../config'
import type { DocSearchHit } from '@docsearch/react/dist/esm/types'
const { options } = defineProps<{ const props = defineProps<{
options: AlgoliaSearchOptions options: DefaultTheme.AlgoliaSearchOptions
}>() }>()
const vm = getCurrentInstance() const vm = getCurrentInstance()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
watch(() => options, (value) => { update(value) }) watch(
() => props.options,
(value) => {
update(value)
}
)
onMounted(() => { onMounted(() => {
initialize(options) initialize(props.options)
}) })
function isSpecialClick(event: MouseEvent) { function isSpecialClick(event: MouseEvent) {
@ -39,85 +52,87 @@ function getRelativePath(absoluteUrl: string) {
function update(options: any) { function update(options: any) {
if (vm && vm.vnode.el) { if (vm && vm.vnode.el) {
vm.vnode.el.innerHTML = '<div class="algolia-search-box" id="docsearch"></div>' vm.vnode.el.innerHTML =
'<div class="algolia-search-box" id="docsearch"></div>'
initialize(options) initialize(options)
} }
} }
function initialize(userOptions: any) { function initialize(userOptions: any) {
Promise.all([ docsearch(
import('@docsearch/js'), Object.assign({}, userOptions, {
import('@docsearch/css') container: '#docsearch',
]).then(([docsearch]) => {
docsearch.default( searchParameters: Object.assign({}, userOptions.searchParameters),
Object.assign({}, userOptions, {
container: '#docsearch', navigator: {
navigate: ({ suggestionUrl }: { suggestionUrl: string }) => {
searchParameters: Object.assign({}, userOptions.searchParameters), const { pathname: hitPathname } = new URL(
window.location.origin + suggestionUrl
navigator: { )
navigate: ({ suggestionUrl }: { suggestionUrl: string }) => {
const { pathname: hitPathname } = new URL( // Router doesn't handle same-page navigation so we use the native
window.location.origin + suggestionUrl // browser location API for anchor navigation
) if (route.path === hitPathname) {
window.location.assign(window.location.origin + suggestionUrl)
// Router doesn't handle same-page navigation so we use the native } else {
// browser location API for anchor navigation router.go(suggestionUrl)
if (route.path === hitPathname) {
window.location.assign(window.location.origin + suggestionUrl)
} else {
router.go(suggestionUrl)
}
} }
}, }
},
transformItems: (items) => { transformItems: (items: DocSearchHit[]) => {
return items.map((item) => { return items.map((item) => {
return Object.assign({}, item, { return Object.assign({}, item, {
url: getRelativePath(item.url) url: getRelativePath(item.url)
})
}) })
}, })
},
hitComponent: ({ hit, children }) => {
const relativeHit = hit.url.startsWith('http') hitComponent: ({
? getRelativePath(hit.url as string) hit,
: hit.url children
}: {
return { hit: DocSearchHit
type: 'a', children: any
ref: undefined, }) => {
constructor: undefined, const relativeHit = hit.url.startsWith('http')
key: undefined, ? getRelativePath(hit.url as string)
props: { : hit.url
href: hit.url,
onClick: (event: MouseEvent) => { return {
if (isSpecialClick(event)) { type: 'a',
return ref: undefined,
} constructor: undefined,
key: undefined,
// we rely on the native link scrolling when user is already on props: {
// the right anchor because Router doesn't support duplicated href: hit.url,
// history entries onClick: (event: MouseEvent) => {
if (route.path === relativeHit) { if (isSpecialClick(event)) {
return return
} }
// if the hits goes to another page, we prevent the native link // we rely on the native link scrolling when user is already on
// behavior to leverage the Router loading feature // the right anchor because Router doesn't support duplicated
if (route.path !== relativeHit) { // history entries
event.preventDefault() if (route.path === relativeHit) {
} return
}
router.go(relativeHit)
}, // if the hits goes to another page, we prevent the native link
children // behavior to leverage the Router loading feature
} if (route.path !== relativeHit) {
event.preventDefault()
}
router.go(relativeHit)
},
children
} }
} }
}) }
) })
}) )
} }
</script> </script>
@ -139,7 +154,7 @@ function initialize(userOptions: any) {
.algolia-search-box .DocSearch-Button-Placeholder { .algolia-search-box .DocSearch-Button-Placeholder {
padding-left: 8px; padding-left: 8px;
font-size: .9rem; font-size: 0.9rem;
font-weight: 500; font-weight: 500;
} }
} }

@ -1,6 +1,5 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute, useSiteData } from 'vitepress' import { useRoute, useSiteData, inBrowser } from 'vitepress'
import { inBrowser } from '/@app/utils'
import type { DefaultTheme } from '../config' import type { DefaultTheme } from '../config'
export function useLocaleLinks() { export function useLocaleLinks() {

@ -1,5 +1,4 @@
import { useSiteData } from 'vitepress' import { useSiteData, joinPath } from 'vitepress'
import { joinPath } from '/@app/utils'
export function useUrl() { export function useUrl() {
const site = useSiteData() const site = useSiteData()

@ -3,7 +3,6 @@ export namespace DefaultTheme {
logo?: string logo?: string
nav?: NavItem[] | false nav?: NavItem[] | false
sidebar?: SideBarConfig | MultiSideBarConfig sidebar?: SideBarConfig | MultiSideBarConfig
search?: SearchConfig | false
/** /**
* GitHub repository following the format <user>/<project>. * GitHub repository following the format <user>/<project>.
@ -62,6 +61,14 @@ export namespace DefaultTheme {
nextLink?: boolean nextLink?: boolean
locales?: Record<string, LocaleConfig & Omit<Config, 'locales'>> locales?: Record<string, LocaleConfig & Omit<Config, 'locales'>>
algolia?: AlgoliaSearchOptions
carbonAds?: {
carbon: string
custom?: string
placement: string
}
} }
// navbar -------------------------------------------------------------------- // navbar --------------------------------------------------------------------
@ -110,26 +117,19 @@ export namespace DefaultTheme {
children: SideBarItem[] children: SideBarItem[]
} }
// search -------------------------------------------------------------------- // algolia ------------------------------------------------------------------
// partially copied from @docsearch/react/dist/esm/DocSearch.d.ts
export interface SearchConfig { export interface AlgoliaSearchOptions {
/** appId?: string
* @default 5 apiKey: string
*/ indexName: string
maxSuggestions?: number
/**
* @default ''
*/
placeholder?: string placeholder?: string
searchParameters?: any
algolia?: { disableUserPersonalization?: boolean
apiKey: string initialQuery?: string
indexName: string
}
} }
// locales -------------------------------------------------------------------- // locales -------------------------------------------------------------------
export interface LocaleConfig { export interface LocaleConfig {
/** /**

@ -7,7 +7,6 @@
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"types": ["vite"], "types": ["vite"],
"paths": { "paths": {
"/@app/*": ["app/*"],
"/@theme/*": ["theme-default/*"], "/@theme/*": ["theme-default/*"],
"/@shared/*": ["shared/*"], "/@shared/*": ["shared/*"],
"/@types/*": ["../../types/*"], "/@types/*": ["../../types/*"],

@ -16,11 +16,8 @@
"node/*": ["src/node/*"], "node/*": ["src/node/*"],
"shared/*": ["src/shared/*"], "shared/*": ["src/shared/*"],
"tests/*": ["__tests__/*"], "tests/*": ["__tests__/*"],
"/@app/*": ["src/client/app/*"],
"/@theme/*": ["src/client/theme-default/*"],
"/@shared/*": ["src/client/shared/*"], "/@shared/*": ["src/client/shared/*"],
"/@types/*": ["types/*"], "/@types/*": ["types/*"]
"vitepress": ["src/client/app/exports.ts"]
} }
}, },
"include": [ "include": [

Loading…
Cancel
Save