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(() => { // 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() 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() 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() 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, } }