From 444b362deacd786f1acf069811d2305a818686f4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 27 Apr 2020 22:17:54 -0400 Subject: [PATCH] checkJS + scroll position --- lib/app/composables/router.js | 57 ++++++++++++++++++++ lib/app/index.js | 98 ++++++++++++++++------------------- lib/shim.d.ts | 5 ++ package.json | 5 ++ tsconfig.json | 13 +++-- 5 files changed, 120 insertions(+), 58 deletions(-) create mode 100644 lib/app/composables/router.js create mode 100644 lib/shim.d.ts diff --git a/lib/app/composables/router.js b/lib/app/composables/router.js new file mode 100644 index 00000000..25230089 --- /dev/null +++ b/lib/app/composables/router.js @@ -0,0 +1,57 @@ +import { reactive, provide } from 'vue' + +export const RouteSymbol = Symbol() + +export function useRouter() { + const route = reactive({ + path: location.pathname, + scrollPosition: window.scrollY + }) + + window.addEventListener( + 'click', + /** + * @param {*} e + */ + (e) => { + if (e.target.tagName === 'A') { + const { href, target } = e.target + if ( + target !== `_blank` && + href.startsWith(`${location.protocol}//${location.host}`) + ) { + e.preventDefault() + // save scroll position before changing url + saveScrollPosition() + history.pushState(null, '', href) + route.path = location.pathname + route.scrollPosition = 0 + } + } + }, + { capture: true } + ) + + window.addEventListener( + 'popstate', + /** + * @param {*} e + */ + (e) => { + route.path = location.pathname + route.scrollPosition = e.state && e.state.scrollPosition || 0 + } + ) + + provide(RouteSymbol, route) +} + +function saveScrollPosition() { + history.replaceState( + { + scrollPosition: window.scrollY + }, + document.title, + '' + ) +} diff --git a/lib/app/index.js b/lib/app/index.js index 139d6cef..7a3f52d6 100644 --- a/lib/app/index.js +++ b/lib/app/index.js @@ -1,46 +1,14 @@ -import { - createApp, - ref, - h, - provide, - inject, - watchEffect, - shallowRef -} from '/@modules/vue' +import { createApp, h, inject, watchEffect, shallowRef, nextTick } from 'vue' import { Layout } from '/@theme/index.js' - -const PathSymbol = Symbol() +import { useRouter, RouteSymbol } from './composables/router.js' const App = { setup() { - const path = ref(location.pathname) - - window.addEventListener( - 'click', - (e) => { - if (e.target.tagName === 'A') { - const { href, target } = e.target - if ( - target !== `_blank` && - href.startsWith(`${location.protocol}//${location.host}`) - ) { - e.preventDefault() - // TODO save scroll position - history.pushState(null, '', href) - path.value = location.pathname - } - } - }, - { capture: true } - ) - - window.addEventListener('popstate', (e) => { - // TODO restore scroll position - path.value = location.pathname - }) - - provide(PathSymbol, path) - + if (typeof window !== 'undefined') { + useRouter() + } else { + // TODO inject static route for SSR + } return () => h(Layout) } } @@ -49,24 +17,46 @@ const Default404 = () => '404 Not Found' const Content = { setup() { - const path = inject(PathSymbol) const comp = shallowRef() - watchEffect(() => { - let pagePath = path.value.replace(/\.html$/, '') - if (pagePath.endsWith('/')) { - pagePath += 'index' - } + if (typeof window !== 'undefined') { + /** + * @type {{ path: string, scrollPosition: number }} + */ + const route = inject(RouteSymbol, { + path: '/', + scrollPosition: 0 + }) + + watchEffect(() => { + const pendingPath = route.path + let pagePath = pendingPath.replace(/\.html$/, '') + if (pagePath.endsWith('/')) { + pagePath += 'index' + } - // awlays force re-fetch content in dev - import(`${pagePath}.md?t=${Date.now()}`) - .then((m) => { - comp.value = m.default - }) - .catch((err) => { - comp.value = Default404 - }) - }) + // awlays force re-fetch content in dev + import(`${pagePath}.md?t=${Date.now()}`) + .then(async (m) => { + if (route.path === pendingPath) { + comp.value = m.default + await nextTick() + window.scrollTo({ + left: 0, + top: route.scrollPosition, + behavior: 'auto' + }) + } + }) + .catch((err) => { + if (route.path === pendingPath) { + comp.value = Default404 + } + }) + }) + } else { + // TODO SSR + } return () => (comp.value ? h(comp.value) : null) } diff --git a/lib/shim.d.ts b/lib/shim.d.ts new file mode 100644 index 00000000..1ae3df6b --- /dev/null +++ b/lib/shim.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import { ComponentOptions } from 'vue' + const comp: ComponentOptions + export default comp +} diff --git a/package.json b/package.json index d67a7f6f..b2df3349 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,11 @@ "bin": { "vitepress": "bin/vitepress.js" }, + "files": [ + "bin", + "lib", + "dist" + ], "scripts": { "dev": "tsc -w -p ." }, diff --git a/tsconfig.json b/tsconfig.json index 98bdbf01..db0aaf48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,19 +3,24 @@ "baseUrl": ".", "outDir": "./dist", "module": "commonjs", - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "sourceMap": false, "target": "esnext", "moduleResolution": "node", "esModuleInterop": true, "declaration": true, - "allowJs": false, + "allowJs": true, + "checkJs": true, "allowSyntheticDefaultImports": true, "noUnusedLocals": true, "strictNullChecks": true, "noImplicitAny": true, "removeComments": false, - "preserveSymlinks": true + "preserveSymlinks": true, + "paths": { + "/@app/*": ["lib/app/*"], + "/@theme/*": ["lib/theme-default/*"] + } }, - "include": ["src"] + "include": ["src", "lib"] }