import type { BackstackEntry , NavigationContext , NavigationEntry , NavigationTransition } from './frame-interfaces' ; import { NavigationType } from './frame-interfaces' ; import { Page } from '../page' ; import { View , CustomLayoutView , CSSType } from '../core/view' ; import { Property } from '../core/properties' ; import { Trace } from '../../trace' ; import { frameStack , topmost as frameStackTopmost , _pushInFrameStack , _popFromFrameStack , _removeFromFrameStack } from './frame-stack' ; import { viewMatchesModuleContext } from '../core/view/view-common' ; import { getAncestor } from '../core/view-base' ; import { Builder } from '../builder' ; import { sanitizeModuleName } from '../builder/module-name-sanitizer' ; import { profile } from '../../profiling' ; import { FRAME_SYMBOL } from './frame-helpers' ; export { NavigationType } from './frame-interfaces' ; export type { AndroidActivityCallbacks , AndroidFragmentCallbacks , AndroidFrame , BackstackEntry , NavigationContext , NavigationEntry , NavigationTransition , TransitionState , ViewEntry , iOSFrame } from './frame-interfaces' ; function buildEntryFromArgs ( arg ) { let entry ; if ( typeof arg === 'string' ) { entry = { moduleName : arg , } ; } else if ( typeof arg === 'function' ) { entry = { create : arg , } ; } else { entry = arg ; } return entry ; } @ CSSType ( 'Frame' ) export class FrameBase extends CustomLayoutView { public static androidOptionSelectedEvent = 'optionSelected' ; private _animated : boolean ; private _transition : NavigationTransition ; private _backStack = new Array < BackstackEntry > ( ) ; private _navigationQueue = new Array < NavigationContext > ( ) ; public actionBarVisibility : 'auto' | 'never' | 'always' ; public _currentEntry : BackstackEntry ; public _animationInProgress = false ; public _executingContext : NavigationContext ; public _isInFrameStack = false ; public static defaultAnimatedNavigation = true ; public static defaultTransition : NavigationTransition ; static getFrameById ( id ) { return frameStack . find ( ( frame ) => frame . id && frame . id === id ) ; } static topmost ( ) { return frameStackTopmost ( ) ; } static goBack ( ) { const top = FrameBase . topmost ( ) ; if ( top && top . canGoBack ( ) ) { top . goBack ( ) ; return true ; } else if ( top ) { let parentFrameCanGoBack = false ; let parentFrame = < FrameBase > getAncestor ( top , 'Frame' ) ; while ( parentFrame && ! parentFrameCanGoBack ) { if ( parentFrame && parentFrame . canGoBack ( ) ) { parentFrameCanGoBack = true ; } else { parentFrame = < FrameBase > getAncestor ( parentFrame , 'Frame' ) ; } } if ( parentFrame && parentFrameCanGoBack ) { parentFrame . goBack ( ) ; return true ; } } if ( frameStack . length > 1 ) { top . _popFromFrameStack ( ) ; } return false ; } private static reloadPage ( ) { } private static _stack ( ) { return frameStack ; } public _addChildFromBuilder ( name , value ) { throw new Error ( `Frame should not have a view. Use 'defaultPage' property instead.` ) ; } @ profile public onLoaded ( ) { super . onLoaded ( ) ; this . _processNextNavigationEntry ( ) ; } public canGoBack ( ) { let backstack = this . _backStack . length ; let previousForwardNotInBackstack = false ; this . _navigationQueue . forEach ( ( item ) => { const entry = item . entry ; const isBackNavigation = item . navigationType === NavigationType . back ; if ( isBackNavigation ) { previousForwardNotInBackstack = false ; if ( ! entry ) { backstack -- ; } else { const backstackIndex = this . _backStack . indexOf ( entry ) ; if ( backstackIndex !== - 1 ) { backstack = backstackIndex ; } else { backstack -- ; } } } else if ( entry . entry . clearHistory ) { previousForwardNotInBackstack = false ; backstack = 0 ; } else { backstack ++ ; if ( previousForwardNotInBackstack ) { backstack -- ; } previousForwardNotInBackstack = entry . entry . backstackVisible === false ; } } ) ; if ( this . _navigationQueue . length > 0 && ! this . _currentEntry ) { backstack -- ; } return backstack > 0 ; } param to public goBack ( backstackEntry ? ) { if ( Trace . isEnabled ( ) ) { Trace . write ( `GO BACK` , Trace . categories . Navigation ) ; } if ( ! this . canGoBack ( ) ) { return ; } if ( backstackEntry ) { const index = this . _backStack . indexOf ( backstackEntry ) ; if ( index < 0 ) { return ; } } const navigationContext = { entry : backstackEntry , isBackNavigation : true , navigationType : NavigationType . back , } ; this . _navigationQueue . push ( navigationContext ) ; this . _processNextNavigationEntry ( ) ; } public _removeEntry ( removed ) { const page = removed . resolvedPage ; const frame = page . frame ; page . _frame = null ; if ( frame ) { frame . _removeView ( page ) ; } else { page . _tearDownUI ( true ) ; } removed . resolvedPage = null ; } public navigate ( param ) { if ( Trace . isEnabled ( ) ) { Trace . write ( `NAVIGATE` , Trace . categories . Navigation ) ; } this . _pushInFrameStack ( ) ; const entry = buildEntryFromArgs ( param ) ; const page = Builder . createViewFromEntry ( entry ) as Page ; const backstackEntry = { entry : entry , resolvedPage : page , navDepth : undefined , fragmentTag : undefined , } ; const navigationContext = { entry : backstackEntry , isBackNavigation : false , navigationType : NavigationType . forward , } ; this . _navigationQueue . push ( navigationContext ) ; this . _processNextNavigationEntry ( ) ; } public isCurrent ( entry ) { return this . _currentEntry === entry ; } public setCurrent ( entry , navigationType ) { const newPage = entry . resolvedPage ; if ( ! newPage . frame ) { this . _addView ( newPage ) ; newPage . _frame = this ; } this . _currentEntry = entry ; const isBack = navigationType === NavigationType . back ; if ( isBack ) { this . _pushInFrameStack ( ) ; } newPage . onNavigatedTo ( isBack ) ; this . notify ( { eventName : Page . navigatedToEvent , object : this , isBack , entry , } ) ; this . _executingContext = null ; } public _updateBackstack ( entry , navigationType ) { const isBack = navigationType === NavigationType . back ; const isReplace = navigationType === NavigationType . replace ; this . raiseCurrentPageNavigatedEvents ( isBack ) ; const current = this . _currentEntry ; if ( isBack ) { const index = this . _backStack . indexOf ( entry ) ; this . _backStack . splice ( index + 1 ) . forEach ( ( e ) => this . _removeEntry ( e ) ) ; this . _backStack . pop ( ) ; } else if ( ! isReplace ) { if ( entry . entry . clearHistory ) { this . _backStack . forEach ( ( e ) => this . _removeEntry ( e ) ) ; this . _backStack . length = 0 ; } else if ( FrameBase . _isEntryBackstackVisible ( current ) ) { this . _backStack . push ( current ) ; } } if ( current && this . _backStack . indexOf ( current ) < 0 ) { this . _removeEntry ( current ) ; } } private isNestedWithin ( parentFrameCandidate ) { let frameAncestor = this ; while ( frameAncestor ) { frameAncestor = < FrameBase > getAncestor ( frameAncestor , FrameBase ) ; if ( frameAncestor === parentFrameCandidate ) { return true ; } } return false ; } private raiseCurrentPageNavigatedEvents ( isBack ) { const page = this . currentPage ; if ( page ) { if ( page . isLoaded ) { page . callUnloaded ( ) ; } page . onNavigatedFrom ( isBack ) ; } } public _processNavigationQueue ( page ) { if ( this . _navigationQueue . length === 0 ) { return ; } const entry = this . _navigationQueue [ 0 ] . entry ; const currentNavigationPage = entry . resolvedPage ; if ( page !== currentNavigationPage ) { return ; } this . _navigationQueue . shift ( ) ; this . _processNextNavigationEntry ( ) ; this . _updateActionBar ( ) ; } public _findEntryForTag ( fragmentTag ) { let entry ; if ( this . _currentEntry && this . _currentEntry . fragmentTag === fragmentTag ) { entry = this . _currentEntry ; } else { entry = this . _backStack . find ( ( value ) => value . fragmentTag === fragmentTag ) ; if ( ! entry ) { const navigationItem = this . _navigationQueue . find ( ( value ) => value . entry . fragmentTag === fragmentTag ) ; entry = navigationItem ? navigationItem . entry : undefined ; } } return entry ; } public navigationQueueIsEmpty ( ) { return this . _navigationQueue . length === 0 ; } public static _isEntryBackstackVisible ( entry ) { if ( ! entry ) { return false ; } const backstackVisibleValue = entry . entry . backstackVisible ; const backstackHidden = backstackVisibleValue !== undefined && ! backstackVisibleValue ; return ! backstackHidden ; } public _updateActionBar ( page ? , disableNavBarAnimation ? ) { } protected _processNextNavigationEntry ( ) { if ( ! this . isLoaded || this . _executingContext ) { return ; } if ( this . _navigationQueue . length > 0 ) { const navigationContext = this . _navigationQueue [ 0 ] ; const isBackNavigation = navigationContext . navigationType === NavigationType . back ; if ( isBackNavigation ) { this . performGoBack ( navigationContext ) ; } else { this . performNavigation ( navigationContext ) ; } } } @ profile public performNavigation ( navigationContext ) { this . _executingContext = navigationContext ; const backstackEntry = navigationContext . entry ; const isBackNavigation = navigationContext . navigationType === NavigationType . back ; this . _onNavigatingTo ( backstackEntry , isBackNavigation ) ; this . _navigateCore ( backstackEntry ) ; } @ profile private performGoBack ( navigationContext ) { let backstackEntry = navigationContext . entry ; const backstack = this . _backStack ; if ( ! backstackEntry ) { backstackEntry = backstack [ backstack . length - 1 ] ; navigationContext . entry = backstackEntry ; } this . _executingContext = navigationContext ; this . _onNavigatingTo ( backstackEntry , true ) ; this . _goBackCore ( backstackEntry ) ; } public _goBackCore ( backstackEntry ) { if ( Trace . isEnabled ( ) ) { Trace . write ( `GO BACK CORE(${ this . _backstackEntryTrace ( backstackEntry ) }); currentPage: ${ this . currentPage }` , Trace . categories . Navigation ) ; } } public _navigateCore ( backstackEntry ) { if ( Trace . isEnabled ( ) ) { Trace . write ( `NAVIGATE CORE(${ this . _backstackEntryTrace ( backstackEntry ) }); currentPage: ${ this . currentPage }` , Trace . categories . Navigation ) ; } } public _onNavigatingTo ( backstackEntry , isBack ) { if ( this . currentPage ) { this . currentPage . onNavigatingFrom ( isBack ) ; } backstackEntry . resolvedPage . onNavigatingTo ( backstackEntry . entry . context , isBack , backstackEntry . entry . bindingContext ) ; this . notify ( { eventName : Page . navigatingToEvent , object : this , isBack , entry : backstackEntry . entry , fromEntry : this . currentEntry , } ) ; } public get animated ( ) : boolean { return this . _animated ; } public set animated ( value ) { this . _animated = value ; } public get transition ( ) : NavigationTransition { return this . _transition ; } public set transition ( value ) { this . _transition = value ; } get backStack ( ) : Array < BackstackEntry > { return this . _backStack . slice ( ) ; } get currentPage ( ) : Page { if ( this . _currentEntry ) { return this . _currentEntry . resolvedPage ; } return null ; } get currentEntry ( ) : NavigationEntry { if ( this . _currentEntry ) { return this . _currentEntry . entry ; } return null ; } public _pushInFrameStackRecursive ( ) { this . _pushInFrameStack ( ) ; const framesToPush = [ ] ; for ( const frame of frameStack ) { if ( frame . isNestedWithin ( this ) ) { framesToPush . push ( frame ) ; } } for ( const frame of framesToPush ) { frame . _pushInFrameStack ( ) ; } } public _pushInFrameStack ( ) { _pushInFrameStack ( this ) ; } public _popFromFrameStack ( ) { _popFromFrameStack ( this ) ; } public _removeFromFrameStack ( ) { _removeFromFrameStack ( this ) ; } public _dialogClosed ( ) { this . _removeFromFrameStack ( ) ; } public _onRootViewReset ( ) { super . _onRootViewReset ( ) ; this . _removeFromFrameStack ( ) ; } get _childrenCount ( ) : number { if ( this . currentPage ) { return 1 ; } return 0 ; } public eachChildView ( callback ) { const page = this . currentPage ; if ( page ) { callback ( page ) ; } } public _getIsAnimatedNavigation ( entry ) { if ( entry && entry . animated !== undefined ) { return entry . animated ; } if ( this . animated !== undefined ) { return this . animated ; } return FrameBase . defaultAnimatedNavigation ; } public _getNavigationTransition ( entry ) { if ( entry ) { if ( global . isIOS && entry . transitioniOS !== undefined ) { return entry . transitioniOS ; } if ( global . isAndroid && entry . transitionAndroid !== undefined ) { return entry . transitionAndroid ; } if ( entry . transition !== undefined ) { return entry . transition ; } } if ( this . transition !== undefined ) { return this . transition ; } return FrameBase . defaultTransition ; } public get navigationBarHeight ( ) : number { return 0 ; } public _getNavBarVisible ( page ) { throw new Error ( ) ; } public _addViewToNativeVisualTree ( child ) { return true ; } public _removeViewFromNativeVisualTree ( child ) { child . _isAddedToNativeVisualTree = false ; } public _printFrameBackStack ( ) { const length = this . backStack . length ; let i = length - 1 ; console . log ( `Frame Back Stack: ` ) ; while ( i >= 0 ) { const backstackEntry = < BackstackEntry > this . backStack [ i -- ] ; console . log ( `\t${ backstackEntry . resolvedPage }` ) ; } } public _backstackEntryTrace ( b ) { let result = `${ b . resolvedPage }` ; const backstackVisible = FrameBase . _isEntryBackstackVisible ( b ) ; if ( ! backstackVisible ) { result += ` | INVISIBLE` ; } if ( b . entry . clearHistory ) { result += ` | CLEAR HISTORY` ; } const animated = this . _getIsAnimatedNavigation ( b . entry ) ; if ( ! animated ) { result += ` | NOT ANIMATED` ; } const t = this . _getNavigationTransition ( b . entry ) ; if ( t ) { result += ` | Transition[${ JSON . stringify ( t ) }]` ; } return result ; } public _onLivesync ( context ? ) { if ( super . _onLivesync ( context ) ) { return true ; } if ( ! context ) { return this . legacyLivesync ( ) ; } return false ; } public _handleLivesync ( context ? ) { if ( super . _handleLivesync ( context ) ) { return true ; } if ( this . currentPage && viewMatchesModuleContext ( this . currentPage , context , [ 'markup' , 'script' ] ) ) { Trace . write ( `Change Handled: Replacing page ${ context . path }` , Trace . categories . Livesync ) ; this . replacePage ( { moduleName : context . path , transition : { name : 'fade' , duration : 100 , } , } ) ; return true ; } return false ; } private legacyLivesync ( ) { if ( Trace . isEnabled ( ) ) { Trace . write ( `${ this }._onLivesync()` , Trace . categories . Livesync ) ; } if ( ! this . _currentEntry || ! this . _currentEntry . entry ) { return false ; } const currentEntry = this . _currentEntry . entry ; const newEntry = { animated : false , clearHistory : true , context : currentEntry . context , create : currentEntry . create , moduleName : currentEntry . moduleName , backstackVisible : currentEntry . backstackVisible , } ; if ( newEntry . create ) { const page = newEntry . create ( ) ; if ( page === this . currentPage ) { return false ; } } this . navigate ( newEntry ) ; return true ; } public replacePage ( entry ) { const currentBackstackEntry = this . _currentEntry ; if ( typeof entry === 'string' ) { const contextModuleName = sanitizeModuleName ( entry ) ; entry = { moduleName : contextModuleName } ; } const newPage = Builder . createViewFromEntry ( entry ) as Page ; const newBackstackEntry = { entry : Object . assign ( { } , currentBackstackEntry . entry , entry ) , resolvedPage : newPage , navDepth : currentBackstackEntry . navDepth , fragmentTag : currentBackstackEntry . fragmentTag , frameId : currentBackstackEntry . frameId , } ; const navigationContext = { entry : newBackstackEntry , isBackNavigation : false , navigationType : NavigationType . replace , } ; this . _navigationQueue . push ( navigationContext ) ; this . _processNextNavigationEntry ( ) ; } } FrameBase . prototype [ FRAME_SYMBOL ] = true ; export function getFrameById ( id ) { console . log ( 'getFrameById() is deprecated. Use Frame.getFrameById() instead.' ) ; return FrameBase . getFrameById ( id ) ; } export function topmost ( ) { console . log ( 'topmost() is deprecated. Use Frame.topmost() instead.' ) ; return FrameBase . topmost ( ) ; } export function goBack ( ) { console . log ( 'goBack() is deprecated. Use Frame.goBack() instead.' ) ; return FrameBase . goBack ( ) ; } export function _stack ( ) { console . log ( '_stack() is deprecated. Use Frame._stack() instead.' ) ; return FrameBase . _stack ( ) ; } export const defaultPage = new Property < FrameBase , string > ( { name : 'defaultPage' , valueChanged : ( frame , oldValue , newValue ) => { frame . navigate ( { moduleName : newValue } ) ; } , } ) ; defaultPage . register ( FrameBase ) ; export const actionBarVisibilityProperty = new Property < FrameBase , 'auto' | 'never' | 'always' > ( { name : 'actionBarVisibility' , defaultValue : 'auto' , affectsLayout : global . isIOS } ) ; actionBarVisibilityProperty . register ( FrameBase ) ;