This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/composables/useRouteWithModal.ts

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,
}
}