import { Injectable } from '@angular/core' ; import { ActivatedRoute , NavigationEnd , Params , Route , Router , Routes } from '@angular/router' ; import { EMPTY , Observable } from 'rxjs' ; import { filter , first , map , pairwise , startWith } from 'rxjs/operators' ; import { ModalRouteActivation } from './modal.interfaces' ; @ Injectable ( { providedIn : 'root' } ) export class ModalNavigationService { constructor ( private router , private route ) { } isModalRoute ( url ) { return url . includes ( '(modal:' ) ; } private getCurrentActivatedRoute ( ) { let childRoute = this . route . root ; while ( childRoute . firstChild ) { childRoute = childRoute . firstChild ; } return childRoute ; } private async getModalRoutes ( routeConfig , moduleRootRoutePath ? ) { const flattenedRoutes = [ ] . concat ( ... routeConfig ) ; let modalRoutes = [ ] ; const moduleRootPaths = await this . getModuleRootPath ( flattenedRoutes , moduleRootRoutePath ) ; if ( moduleRootPaths ) { modalRoutes = this . getModalRoutePaths ( flattenedRoutes , moduleRootPaths ) ; } return modalRoutes ; } private async getModuleRootPath ( routes , moduleRootRoutePath ? ) { if ( moduleRootRoutePath ) { const trimmedPaths = moduleRootRoutePath . trim ( ) . split ( '/' ) . filter ( ( path ) => ! ! path ) ; const rootPath = [ '' ] ; return rootPath . concat ( trimmedPaths ) ; } const currentRoutePaths = await this . getCurrentRoutePaths ( ) ; const moduleRootPaths = this . getRoutePathsWithoutChildSegments ( currentRoutePaths , routes ) ; return moduleRootPaths ; } private async getCurrentRoutePaths ( ) { const rootPath = [ '' ] ; const currentNavigation = this . router . getCurrentNavigation ( ) ; if ( ! this . router . navigated && ! currentNavigation ) { return rootPath ; } if ( currentNavigation ) { await this . navigationEndListener$ . pipe ( first ( ) ) . toPromise ( ) ; } let childRoute = this . route . snapshot . root ; while ( childRoute . firstChild ) { childRoute = childRoute . firstChild ; } const currentBackdropRoutePath = childRoute . pathFromRoot . filter ( ( route ) => route . outlet !== 'modal' ) ; return rootPath . concat ( ... currentBackdropRoutePath . map ( ( route ) => route . url . filter ( ( segment ) => ! ! segment . path ) . map ( ( segment ) => segment . path ) ) ) ; } private getRoutePathsWithoutChildSegments ( routePaths , routes ) { if ( ! routePaths . length ) return routePaths ; const matchedChildRoute = this . findChildRouteForUrl ( routePaths . join ( '/' ) , routes ) ; if ( ! matchedChildRoute ) return routePaths ; const startSlashRegex = /^\// ; const relativeChildRoute = matchedChildRoute . replace ( startSlashRegex , '' ) ; const childSegmentCount = relativeChildRoute . split ( '/' ) . length ; const routePathsWithoutChildSegments = routePaths . slice ( 0 , - childSegmentCount ) ; return routePathsWithoutChildSegments ; } private findChildRouteForUrl ( url , routes ) { const moduleRelativePaths = this . getRoutePaths ( routes , [ '' ] ) . sort ( ) . reverse ( ) ; let matchedChildRoute = moduleRelativePaths . find ( ( path ) => url . endsWith ( path ) ) ; if ( ! matchedChildRoute ) { const exactMatch = false ; matchedChildRoute = moduleRelativePaths . find ( ( path ) => path . includes ( '/:' ) && this . pathContainsChildRouteWithUrlParams ( url , path , exactMatch ) ) ; } return matchedChildRoute ; } private pathContainsChildRouteWithUrlParams ( path , childRouteWithUrlParams , exactMatch ) { const pathSegments = path . split ( '/' ) ; const startSlashRegex = /^\// ; let childRouteToMatch = childRouteWithUrlParams ; if ( ! exactMatch ) { const relativeChildRoute = childRouteWithUrlParams . replace ( startSlashRegex , '' ) ; childRouteToMatch = relativeChildRoute ; } const childRouteSegments = childRouteToMatch . split ( '/' ) ; const pathContainsChildRoute = childRouteSegments . reverse ( ) . every ( ( childRouteSegment ) => { const pathSegment = pathSegments . pop ( ) ; return childRouteSegment . startsWith ( ':' ) || childRouteSegment === pathSegment ; } ) ; if ( exactMatch ) { return pathSegments . length === 0 && pathContainsChildRoute ; } return pathContainsChildRoute ; } private getRoutePaths ( routes , parentPath ) { return Array . isArray ( routes ) ? ( [ ] as string [ ] ) . concat ( ... routes . map ( ( route ) => this . getRoutePath ( route , parentPath ) ) ) : [ ] ; } private getRoutePath ( route , parentPath ) { const routes = [ ] ; if ( ! ! route . outlet ) return routes ; const currentPath = [ ... parentPath ] ; if ( ! ! route . path ) { currentPath . push ( route . path ) ; routes . push ( currentPath . join ( '/' ) ) ; } return routes . concat ( this . getRoutePaths ( route . children , currentPath ) ) ; } private getModalRoutePath ( route , parentPath ) { const modalOutletName = 'modal' ; if ( ! ! route . path && route . outlet === modalOutletName ) { const modalOutletPath = `(${ modalOutletName }:${ route . path })` ; const modalRoutePath = [ ... parentPath , modalOutletPath ] . join ( '/' ) ; return [ modalRoutePath ] ; } const currentPath = [ ... parentPath ] ; if ( ! ! route . path ) { currentPath . push ( route . path ) ; } return ( [ ] as string [ ] ) . concat ( ... this . getModalRoutePaths ( route . children , currentPath ) ) ; } private getModalRoutePaths ( routes , parentPath ) { return Array . isArray ( routes ) ? ( [ ] as string [ ] ) . concat ( ... routes . map ( ( route ) => this . getModalRoutePath ( route , parentPath ) ) ) : [ ] ; } private isNewModalWindow ( navigationEnd ) { const currentNavigation = this . router . getCurrentNavigation ( ) ; if ( ! currentNavigation || ! currentNavigation . previousNavigation ) { return true ; } const previousRoutePath = ( currentNavigation . previousNavigation . finalUrl || currentNavigation . previousNavigation . extractedUrl ) . toString ( ) ; const wasModalRoute = this . isModalRoute ( previousRoutePath ) ; const isModalRoute = this . isModalRoute ( navigationEnd . urlAfterRedirects ) ; if ( ! wasModalRoute && isModalRoute ) { return true ; } const currentModalRouteParent = navigationEnd . urlAfterRedirects . split ( '/(modal:' ) [ 0 ] ; const previousModalRouteParent = previousRoutePath . split ( '/(modal:' ) [ 0 ] ; return previousModalRouteParent !== currentModalRouteParent ; } private navigationEndListener$ = this . router . events . pipe ( filter ( ( event ) : event is NavigationEnd => event instanceof NavigationEnd ) ) ; private async waitForCurrentThenGetNavigationEndStream ( ) { if ( this . router . getCurrentNavigation ( ) ) { const currentNavigationEnd = await this . navigationEndListener$ . pipe ( first ( ) ) . toPromise ( ) ; return this . navigationEndListener$ . pipe ( startWith ( currentNavigationEnd ) ) ; } return this . navigationEndListener$ ; } private modalRouteActivatedFor ( navigationEnd$ , modalRouteSet , modalRoutesContainsUrlParams ) { return navigationEnd$ . pipe ( filter ( ( navigationEnd ) => this . modalRouteSetContainsPath ( modalRouteSet , navigationEnd , modalRoutesContainsUrlParams ) ) , map ( ( navigationEnd ) => ( { route : this . getCurrentActivatedRoute ( ) , isNewModal : this . isNewModalWindow ( navigationEnd ) , } ) ) ) ; } private modalRouteDeactivatedFor ( navigationEnd$ , modalRouteSet , modalRoutesContainsUrlParams ) { return navigationEnd$ . pipe ( pairwise ( ) , filter ( ( [ prevNavigation , _ ] ) => this . modalRouteSetContainsPath ( modalRouteSet , prevNavigation , modalRoutesContainsUrlParams ) ) , map ( ( [ _ , currentNavigation ] ) => { const isNewModalRoute = this . isModalRoute ( currentNavigation . urlAfterRedirects ) ; return ! isNewModalRoute || this . isNewModalWindow ( currentNavigation ) ; } ) , filter ( ( isDeactivation ) => isDeactivation ) ) ; } private modalRouteSetContainsPath ( modalRouteSet , navigationEnd , modalRoutesContainsUrlParams ) { const pathname = navigationEnd . urlAfterRedirects . split ( '?' ) [ 0 ] ; let hasRoute = modalRouteSet . has ( pathname ) ; if ( ! hasRoute && modalRoutesContainsUrlParams ) { for ( let route of modalRouteSet ) { const exactMatch = true ; const routeMatchesPath = this . pathContainsChildRouteWithUrlParams ( pathname , route , exactMatch ) ; if ( routeMatchesPath ) { hasRoute = true ; break ; } } } return hasRoute ; } async getModalNavigation ( routeConfig , moduleRootRoutePath ? ) { if ( Array . isArray ( routeConfig ) ) { const navigationEnd$ = await this . waitForCurrentThenGetNavigationEndStream ( ) ; const modalRoutes = await this . getModalRoutes ( routeConfig , moduleRootRoutePath ) ; const hasModalRoutes = modalRoutes . length > 0 ; if ( hasModalRoutes ) { const modalRoutesContainsUrlParams = modalRoutes . some ( ( route ) => route . includes ( '/:' ) ) ; const modalRouteSet = new Set ( modalRoutes ) ; const activated$ = this . modalRouteActivatedFor ( navigationEnd$ , modalRouteSet , modalRoutesContainsUrlParams ) ; const deactivated$ = this . modalRouteDeactivatedFor ( navigationEnd$ , modalRouteSet , modalRoutesContainsUrlParams ) ; return { activated$ , deactivated$ } ; } } return { activated$ : EMPTY , deactivated$ : EMPTY } ; } async navigateToModal ( path , queryParams ? ) { const commands = Array . isArray ( path ) ? path : path . split ( '/' ) ; const childPath = commands . pop ( ) ; const result = await this . router . navigate ( [ ... commands , { outlets : { modal : [ childPath ] } } ] , { queryParams , relativeTo : this . getCurrentActivatedRoute ( ) , } ) ; return result ; } async navigateWithinModal ( relativePath , queryParams ? ) { return this . router . navigate ( [ relativePath ] , { queryParams , relativeTo : this . getCurrentActivatedRoute ( ) , } ) ; } async navigateOutOfModalOutlet ( ) { let result = Promise . resolve ( true ) ; const currentActivatedRoute = this . getCurrentActivatedRoute ( ) ; if ( currentActivatedRoute . outlet === 'modal' ) { const parentRoute = this . getBackdropRoute ( currentActivatedRoute ) ; result = this . router . navigate ( [ './' ] , { relativeTo : parentRoute , } ) ; } return result ; } private getBackdropRoute ( currentActivatedRoute ) { let parentRoute = currentActivatedRoute . parent ; while ( parentRoute && ! parentRoute . component && ! ! parentRoute . parent ) { parentRoute = parentRoute . parent ; } return parentRoute ; } }