200 lines
6.4 KiB
TypeScript
200 lines
6.4 KiB
TypeScript
import {computed, shallowRef, watch, h, type VNode, ref} from 'vue'
|
|
import {useRoute, useRouter, loadRouteLocation, type RouteLocationNormalizedLoaded, type RouteLocationRaw, START_LOCATION} from 'vue-router'
|
|
import router, { handleRedirectRecord } from '@/router'
|
|
|
|
// this is adapted from vue-router
|
|
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
|
function getRouteProps(route: RouteLocationNormalizedLoaded) {
|
|
const routePropsOption = route.matched[0]?.props.default
|
|
return routePropsOption
|
|
? routePropsOption === true
|
|
? route.params
|
|
: typeof routePropsOption === 'function'
|
|
? routePropsOption(route)
|
|
: routePropsOption
|
|
: {}
|
|
}
|
|
|
|
function resolveAndLoadRoute(route: RouteLocationRaw, currentLocation?: RouteLocationNormalizedLoaded) {
|
|
const resolvedRoute = router.resolve(route, currentLocation)
|
|
|
|
// resolvedRoute.matched.forEach((record) => console.log(record.redirect))
|
|
|
|
// e.g. 'list.index' will always redirect, so we need to resolve the redirected route record
|
|
const redirectedRoute = handleRedirectRecord(resolvedRoute) || resolvedRoute
|
|
|
|
return loadRouteLocation(router.resolve(redirectedRoute, currentLocation))
|
|
}
|
|
|
|
export function useRouteWithModal() {
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
const historyStateBackdropRoutePath = computed<RouteLocationRaw | undefined>(() => {
|
|
// every time the fullPath changes we check the history state
|
|
// this happens also initially
|
|
return route.fullPath
|
|
? history.state?.backdropRoutePath
|
|
: undefined
|
|
})
|
|
|
|
const isInitialNavigation = ref(true)
|
|
|
|
router.beforeEach((to, from) => {
|
|
isInitialNavigation.value = from === START_LOCATION
|
|
})
|
|
|
|
const lastBackdropRoutePath = ref<string>()
|
|
router.afterEach((to, from) => {
|
|
const resolvedRoute = router.resolve(to)
|
|
if (!resolvedRoute.meta.modal) {
|
|
// this route doesn't define that it can be a modal
|
|
return
|
|
}
|
|
|
|
let backdropRoutePath
|
|
if (resolvedRoute.meta?.modal === true || resolvedRoute.meta.modal?.force !== true) {
|
|
// this route can be shown as modal or as normal view
|
|
|
|
// TODO: add new state or prop instead of checking backdropRoutePath here
|
|
// e.g. `modal` with `'normal' | 'sidebar' | 'modal' | 'sheet'`
|
|
if (!history.state.backdropRoutePath) {
|
|
// since this modal is optional and we don't have a backdropRoutePath we don't do anything
|
|
return
|
|
}
|
|
} else if (lastBackdropRoutePath.value) {
|
|
// there was already a modal in the last route
|
|
// we have to save this in the lastBackdropRoutePath ref because
|
|
// the state gets overwritten
|
|
|
|
// let's show that backdropRoute again
|
|
// TODO: add support for multiple modal layers here
|
|
// FIXME: use `history.state.backdropRoutePath` from last route
|
|
// FIXME: this might be wrong, because we redirect to our current path forever??
|
|
backdropRoutePath = lastBackdropRoutePath.value
|
|
} else if (isInitialNavigation.value === false) {
|
|
backdropRoutePath = from.fullPath
|
|
} else if (resolvedRoute.meta.modal?.defaultBackdropRoute) {
|
|
const {defaultBackdropRoute} = resolvedRoute.meta.modal
|
|
const routeRaw = typeof defaultBackdropRoute === 'function'
|
|
? defaultBackdropRoute(resolvedRoute)
|
|
: defaultBackdropRoute
|
|
backdropRoutePath = router.resolve(routeRaw).fullPath
|
|
}
|
|
|
|
if (backdropRoutePath === undefined) {
|
|
console.log('No defaultBackdropRoute defined for this route')
|
|
|
|
// TODO: maybe load parent route here in the future,
|
|
// via: route.matched[route.matched.length - 2]
|
|
// see: https://router.vuejs.org/guide/migration/#removal-of-parent-from-route-locations
|
|
backdropRoutePath = router.resolve({ name: 'home' }).fullPath
|
|
}
|
|
|
|
lastBackdropRoutePath.value = backdropRoutePath
|
|
|
|
history.replaceState({
|
|
...history.state,
|
|
backdropRoutePath,
|
|
}, '')
|
|
})
|
|
|
|
const routerIsReady = ref(false)
|
|
router.isReady().then(() => {
|
|
routerIsReady.value = true
|
|
})
|
|
|
|
const hasModal = ref(false)
|
|
const baseRoute = shallowRef<RouteLocationNormalizedLoaded>()
|
|
watch(
|
|
[routerIsReady, historyStateBackdropRoutePath],
|
|
async () => {
|
|
if (routerIsReady.value === false || !route.fullPath) {
|
|
// wait until we can work with routes
|
|
hasModal.value = false
|
|
return
|
|
}
|
|
|
|
if (historyStateBackdropRoutePath.value === undefined) {
|
|
if (typeof route.meta?.modal === 'boolean' || route.meta.modal?.force !== true) {
|
|
hasModal.value = false
|
|
baseRoute.value = route
|
|
return
|
|
}
|
|
// the route forces to be shown as a modal
|
|
hasModal.value = true
|
|
let routeRaw: RouteLocationRaw
|
|
if (route.meta.modal?.defaultBackdropRoute) {
|
|
const {defaultBackdropRoute} = route.meta.modal
|
|
routeRaw = typeof defaultBackdropRoute === 'function'
|
|
? defaultBackdropRoute(route)
|
|
: defaultBackdropRoute
|
|
} else {
|
|
// TODO: maybe load parent route here in the future,
|
|
// via: route.matched[route.matched.length - 2]
|
|
// see: https://router.vuejs.org/guide/migration/#removal-of-parent-from-route-locations
|
|
routeRaw = { name: 'home' }
|
|
}
|
|
|
|
baseRoute.value = await resolveAndLoadRoute(routeRaw, baseRoute.value)
|
|
return
|
|
}
|
|
|
|
// we get the resolved route from the fullpath
|
|
// and wait for the route component to be loaded before we assign it
|
|
hasModal.value = true
|
|
baseRoute.value = await resolveAndLoadRoute(historyStateBackdropRoutePath.value, baseRoute.value)
|
|
},
|
|
{immediate: true},
|
|
)
|
|
|
|
const backdropRoute = computed(() => route.fullPath !== baseRoute.value?.fullPath ? baseRoute.value : undefined)
|
|
|
|
const modalRoute = shallowRef<VNode>()
|
|
watch(
|
|
() => [hasModal.value, backdropRoute.value, route],
|
|
() => {
|
|
if (hasModal.value === false) {
|
|
modalRoute.value = undefined
|
|
return
|
|
}
|
|
|
|
const props = getRouteProps(route)
|
|
|
|
props.isModal = true
|
|
props.backdropRoutePath = backdropRoute.value?.fullPath
|
|
props.onClose = closeModal
|
|
|
|
const component = route.matched[0]?.components?.default
|
|
|
|
if (!component) {
|
|
modalRoute.value = undefined
|
|
return
|
|
}
|
|
modalRoute.value = h(component, props)
|
|
},
|
|
{immediate: true},
|
|
)
|
|
|
|
async function closeModal() {
|
|
await router.isReady()
|
|
if (isInitialNavigation.value === false) {
|
|
return router.back()
|
|
} else if (backdropRoute.value !== undefined) {
|
|
// TODO: Dialog modals might want to replace the route here via router.replace()
|
|
return router.push(backdropRoute.value)
|
|
}
|
|
if (history.state === undefined) {
|
|
return router.push({ name: 'home' })
|
|
} else {
|
|
router.back()
|
|
// this should never happen
|
|
throw new Error('')
|
|
}
|
|
}
|
|
|
|
return {
|
|
baseRoute,
|
|
modalRoute,
|
|
}
|
|
} |