Use canonical paths for editor and fix route transitions

This commit is contained in:
2026-05-07 02:18:55 +00:00
parent 6d7f44bd88
commit bf8a049b92
6 changed files with 61 additions and 32 deletions
+54 -22
View File
@@ -24,21 +24,20 @@
/> />
</header> </header>
<main class="transition-wrapper"> <main class="transition-wrapper">
<RouterView v-slot="{ Component }"> <Transition
<Transition :name="routeTransitionName"
:name="routeTransitionName" @after-enter="store.transitionDirection = 'none'"
@after-enter="store.transitionDirection = 'none'" >
> <div :key="routeViewKey" class="explorer-content">
<KeepAlive> <KeepAlive>
<component <component
:is="Component" :is="routeViewComponent"
:key="routeViewKey" :key="routeViewKey"
class="explorer-content"
v-bind="routeViewProps" v-bind="routeViewProps"
/> />
</KeepAlive> </KeepAlive>
</Transition> </div>
</RouterView> </Transition>
</main> </main>
<footer v-if="store.selected.size || store.uprogress.total || store.dprogress.total"> <footer v-if="store.selected.size || store.uprogress.total || store.dprogress.total">
<SelectionToolbar :path="path.pathList" /> <SelectionToolbar :path="path.pathList" />
@@ -50,16 +49,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type HeaderMain from '@/components/HeaderMain.vue' import type HeaderMain from '@/components/HeaderMain.vue'
import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS' import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS'
import { getDocuments } from '@/stores/documentStore'
import { useMainStore } from '@/stores/main' import { useMainStore } from '@/stores/main'
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue' import { onMounted, onUnmounted, ref, watch } from 'vue'
import { RouterView } from 'vue-router'
import Router from '@/router/index' import Router from '@/router/index'
import { computed } from 'vue' import { computed } from 'vue'
import AboutModal from './components/AboutModal.vue' import AboutModal from './components/AboutModal.vue'
import AccessDeniedModal from './components/AccessDeniedModal.vue' import AccessDeniedModal from './components/AccessDeniedModal.vue'
import ExplorerView from './views/ExplorerView.vue'
import SelectionToolbar from './components/SelectionToolbar.vue' import SelectionToolbar from './components/SelectionToolbar.vue'
import TextEditorView from './views/TextEditorView.vue'
import type SettingsModalVue from './components/SettingsModal.vue' import type SettingsModalVue from './components/SettingsModal.vue'
import UserManagementModal from './components/UserManagementModal.vue' import UserManagementModal from './components/UserManagementModal.vue'
import UserTokensModal from './components/UserTokensModal.vue' import UserTokensModal from './components/UserTokensModal.vue'
@@ -67,6 +68,7 @@ import type { SortOrder } from './utils/docsort'
interface Path { interface Path {
path: string path: string
canonicalPath: string
isEditorPath: boolean isEditorPath: boolean
pathList: string[] pathList: string[]
breadcrumbPathList: string[] breadcrumbPathList: string[]
@@ -74,26 +76,42 @@ interface Path {
query: string query: string
} }
const store = useMainStore() const store = useMainStore()
const getDocByPath = (fullPath: string) =>
getDocuments().find(doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === fullPath)
const path: ComputedRef<Path> = computed(() => { const path: ComputedRef<Path> = computed(() => {
const p = decodeURIComponent(Router.currentRoute.value.path).split('//') const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
const routePathList = (p[0] ?? '').split('/').filter(value => value !== '') const rawPath = p[0] ?? ''
const routePathList = rawPath.split('/').filter(value => value !== '')
const query = p.slice(1).join('//') const query = p.slice(1).join('//')
const isEditorPath = routePathList[0] === 'edit' const fullPath = routePathList.join('/')
const pathList = isEditorPath ? routePathList.slice(1, -1) : routePathList // Access docVersion to make route mode reactive to tree updates
const breadcrumbPathList = isEditorPath void store.docVersion
? routePathList.slice(1) const doc = fullPath ? getDocByPath(fullPath) : null
: routePathList const isEditorPath = !!(doc && !doc.dir && doc.text)
const canonicalBase = !fullPath
? '/'
: doc?.dir
? `/${fullPath}/`
: `/${fullPath}`
const canonicalPath = query
? rawPath // keep search URL shape untouched
: canonicalBase
const pathList = isEditorPath ? routePathList.slice(0, -1) : routePathList
const breadcrumbPathList = routePathList
const breadcrumbLinks = isEditorPath const breadcrumbLinks = isEditorPath
? [ ? [
'/', '/',
...routePathList ...routePathList
.slice(1, -1) .slice(0, -1)
.map((_, index) => `/${routePathList.slice(1, index + 2).join('/')}/`), .map((_, index) => `/${routePathList.slice(0, index + 1).join('/')}/`),
`/${routePathList.join('/')}` `/${fullPath}`
] ]
: undefined : undefined
return { return {
path: p[0] ?? '', path: rawPath,
canonicalPath,
isEditorPath, isEditorPath,
pathList, pathList,
breadcrumbPathList, breadcrumbPathList,
@@ -106,15 +124,29 @@ const routeTransitionName = computed(() => {
if (store.transitionDirection === 'backward') return 'slide-backward' if (store.transitionDirection === 'backward') return 'slide-backward'
return '' return ''
}) })
const routeViewComponent = computed(() =>
path.value.isEditorPath ? TextEditorView : ExplorerView
)
const routeViewKey = computed(() => { const routeViewKey = computed(() => {
const route = Router.currentRoute.value return path.value.isEditorPath
return route.name === 'editor' ? route.path : String(route.name ?? route.path) ? `editor:${path.value.path}`
: 'explorer'
}) })
const routeViewProps = computed(() => const routeViewProps = computed(() =>
path.value.isEditorPath path.value.isEditorPath
? {} ? {}
: { path: path.value.pathList, query: path.value.query } : { path: path.value.pathList, query: path.value.query }
) )
watch(
() => path.value.canonicalPath,
canonical => {
const current = decodeURIComponent(Router.currentRoute.value.path)
if (canonical && current !== canonical) {
Router.replace(canonical.replaceAll('?', '%3F').replaceAll('#', '%23'))
}
},
{ immediate: true }
)
watch( watch(
() => path.value.path, () => path.value.path,
() => { () => {
+1 -1
View File
@@ -363,7 +363,7 @@ onUnmounted(() => {
clearInterval(modifiedTimer) clearInterval(modifiedTimer)
}) })
const editRoute = (path: string) => const editRoute = (path: string) =>
'/edit/' + '/' +
path path
.split('/') .split('/')
.map(part => encodeURIComponent(part)) .map(part => encodeURIComponent(part))
+1 -1
View File
@@ -454,7 +454,7 @@ onUnmounted(() => {
// Re-seed aspect ratios whenever docs update (e.g., ar patch from server) // Re-seed aspect ratios whenever docs update (e.g., ar patch from server)
watch(() => props.documents, seedFromDocs) watch(() => props.documents, seedFromDocs)
const editRoute = (path: string) => const editRoute = (path: string) =>
'/edit/' + '/' +
path path
.split('/') .split('/')
.map(part => encodeURIComponent(part)) .map(part => encodeURIComponent(part))
+1 -1
View File
@@ -95,7 +95,7 @@ export class Doc {
get editurl(): string { get editurl(): string {
if (!this.text) return '' if (!this.text) return ''
const p = this.loc ? `${this.loc}/${this.name}` : this.name const p = this.loc ? `${this.loc}/${this.name}` : this.name
return '/#/edit/' + p.replaceAll('?', '%3F').replaceAll('#', '%23') return '/#/' + p.replaceAll('?', '%3F').replaceAll('#', '%23')
} }
get complete(): boolean { get complete(): boolean {
return !this.ghost && (this.dir || this.size <= this.allocated) return !this.ghost && (this.dir || this.size <= this.allocated)
-6
View File
@@ -1,6 +1,5 @@
import { useMainStore } from '@/stores/main' import { useMainStore } from '@/stores/main'
import ExplorerView from '@/views/ExplorerView.vue' import ExplorerView from '@/views/ExplorerView.vue'
import TextEditorView from '@/views/TextEditorView.vue'
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
function getPathDepth(path: string): number { function getPathDepth(path: string): number {
@@ -11,11 +10,6 @@ function getPathDepth(path: string): number {
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{
path: '/edit/:pathMatch(.*)*',
name: 'editor',
component: TextEditorView
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'explorer', name: 'explorer',
+4 -1
View File
@@ -34,7 +34,10 @@ const store = useMainStore()
const MAX_SIZE = 1024 * 1024 // 1 MiB const MAX_SIZE = 1024 * 1024 // 1 MiB
const filePath = computed(() => decodeURIComponent(route.path.slice(6))) // strip /edit/ const filePath = computed(() => {
const raw = decodeURIComponent(route.path).split('//')[0] ?? ''
return raw.replace(/^\//, '').replace(/\/$/, '')
})
const filename = computed(() => filePath.value.split('/').pop() || '') const filename = computed(() => filePath.value.split('/').pop() || '')
const filesUrl = computed(() => { const filesUrl = computed(() => {