diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj index 8c8503fa0..6306a2162 100644 --- a/CrossPlatformModules.csproj +++ b/CrossPlatformModules.csproj @@ -149,6 +149,7 @@ + @@ -1958,6 +1959,7 @@ + \ No newline at end of file diff --git a/application/application-common.ts b/application/application-common.ts index ec4ac8e66..377c8cdf9 100644 --- a/application/application-common.ts +++ b/application/application-common.ts @@ -3,6 +3,7 @@ import definition = require("application"); import fs = require("file-system"); import styleScope = require("ui/styling/style-scope"); import observable = require("data/observable"); +import frame = require("ui/frame"); var events = new observable.Observable(); global.moduleMerge(events, exports); @@ -15,6 +16,9 @@ export var lowMemoryEvent = "lowMemory"; export var uncaughtErrorEvent = "uncaughtError"; export var orientationChangedEvent = "orientationChanged"; +export var mainModule: string; +export var mainEntry: frame.NavigationEntry; + export var cssFile: string = "app.css" export var resources: any = {}; diff --git a/application/application.android.ts b/application/application.android.ts index 213a44bd7..85340b804 100644 --- a/application/application.android.ts +++ b/application/application.android.ts @@ -7,8 +7,6 @@ import enums = require("ui/enums"); global.moduleMerge(appModule, exports); -export var mainModule: string; - // We are using the exports object for the common events since we merge the appModule with this module's exports, which is what users will receive when require("application") is called; // TODO: This is kind of hacky and is "pure JS in TypeScript" @@ -218,10 +216,15 @@ export class AndroidApplication extends observable.Observable implements dts.And var topFrame = frame.topmost(); if (!topFrame) { - // try to navigate to the mainModule (if specified) - if (mainModule) { + // try to navigate to the mainEntry/Module (if specified) + var navParam = dts.mainEntry; + if (!navParam) { + navParam = dts.mainModule; + } + + if (navParam) { topFrame = new frame.Frame(); - topFrame.navigate(mainModule); + topFrame.navigate(navParam); } else { // TODO: Throw an exception? throw new Error("A Frame must be used to navigate to a Page."); diff --git a/application/application.d.ts b/application/application.d.ts index 60c3c70f9..ec75aedcd 100644 --- a/application/application.d.ts +++ b/application/application.d.ts @@ -4,6 +4,7 @@ declare module "application" { import cssSelector = require("ui/styling/css-selector"); import observable = require("data/observable"); + import frame = require("ui/frame"); /** * An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code. @@ -94,6 +95,11 @@ declare module "application" { */ export var mainModule: string; + /** + * The main navigation entry to be used when loading the main Page. + */ + export var mainEntry: frame.NavigationEntry; + /** * An application level static resources. */ diff --git a/application/application.ios.ts b/application/application.ios.ts index 3ffb911f1..97c84c793 100644 --- a/application/application.ios.ts +++ b/application/application.ios.ts @@ -5,9 +5,8 @@ import types = require("utils/types"); import view = require("ui/core/view"); import definition = require("application"); import enums = require("ui/enums"); -global.moduleMerge(appModule, exports); -export var mainModule: string; +global.moduleMerge(appModule, exports); class Responder extends UIResponder { // @@ -120,13 +119,18 @@ class IOSApplication implements definition.iOSApplication { var topFrame = frame.topmost(); if (!topFrame) { - if (mainModule) { + // try to navigate to the mainEntry/Module (if specified) + var navParam = definition.mainEntry; + if (!navParam) { + navParam = definition.mainModule; + } + + if (navParam) { topFrame = new frame.Frame(); - topFrame.navigate(mainModule); + topFrame.navigate(navParam); } else { // TODO: Throw an exception? - // throw new Error("A Frame must be used to navigate to a Page."); - return; + throw new Error("A Frame must be used to navigate to a Page."); } } diff --git a/apps/tests/navigation-tests.ts b/apps/tests/navigation-tests.ts new file mode 100644 index 000000000..5424015a7 --- /dev/null +++ b/apps/tests/navigation-tests.ts @@ -0,0 +1,32 @@ +import TKUnit = require("./TKUnit"); +import pageModule = require("ui/page"); +import frame = require("ui/frame"); + +export var test_backstackVisible = function() { + var pageFactory = function (): pageModule.Page { + return new pageModule.Page(); + }; + + var mainTestPage = frame.topmost().currentPage; + frame.topmost().navigate({ create: pageFactory }); + TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== mainTestPage; }); + + // page1 should not be added to the backstack + var page0 = frame.topmost().currentPage; + frame.topmost().navigate({ create: pageFactory, backstackVisible: false }); + TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== page0; }); + + var page1 = frame.topmost().currentPage; + frame.topmost().navigate({ create: pageFactory }); + TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== page1; }); + + var page2 = frame.topmost().currentPage; + frame.topmost().goBack(); + TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== page2; }); + + // From page2 we have to go directly to page0, skipping page1. + TKUnit.assert(frame.topmost().currentPage === page0, "Page 1 should be skipped when going back."); + + frame.topmost().goBack(); + TKUnit.waitUntilReady(() => { return frame.topmost().currentPage === mainTestPage; }); +} \ No newline at end of file diff --git a/apps/tests/testRunner.ts b/apps/tests/testRunner.ts index 25399b0b2..6f63cdd1a 100644 --- a/apps/tests/testRunner.ts +++ b/apps/tests/testRunner.ts @@ -79,6 +79,7 @@ allTests["REPEATER"] = require("./ui/repeater/repeater-tests"); allTests["SEARCH-BAR"] = require('./ui/search-bar/search-bar-tests'); allTests["CONNECTIVITY"] = require("./connectivity-tests"); allTests["ANIMATION"] = require("./ui/animation/animation-tests"); +allTests["NAVIGATION"] = require("./navigation-tests"); if (!isRunningOnEmulator()) { allTests["LOCATION"] = require("./location-tests"); diff --git a/ui/frame/frame-common.ts b/ui/frame/frame-common.ts index f76be238c..4c1f52d7a 100644 --- a/ui/frame/frame-common.ts +++ b/ui/frame/frame-common.ts @@ -196,6 +196,21 @@ export class Frame extends view.CustomLayoutView implements definition.Frame { } } + public _isEntryBackstackVisible(entry: definition.BackstackEntry): boolean { + if (!entry) { + return false; + } + + var backstackVisibleValue = entry.entry.backstackVisible; + var backstackHidden = types.isDefined(backstackVisibleValue) && !backstackVisibleValue; + + return !backstackHidden; + } + + public _updateActionBar(page?: pages.Page) { + trace.write("calling _updateActionBar on Frame", trace.categories.Navigation); + } + private _processNavigationContext(navigationContext: NavigationContext) { if (navigationContext.isBackNavigation) { this.performGoBack(navigationContext); @@ -209,7 +224,7 @@ export class Frame extends view.CustomLayoutView implements definition.Frame { var navContext = navigationContext.entry; this._onNavigatingTo(navContext); - if (this.currentPage) { + if (this._isEntryBackstackVisible(this._currentEntry)) { this._backStack.push(this._currentEntry); } diff --git a/ui/frame/frame.android.ts b/ui/frame/frame.android.ts index 68a9e4339..f23212a90 100644 --- a/ui/frame/frame.android.ts +++ b/ui/frame/frame.android.ts @@ -13,8 +13,10 @@ var OWNER = "_owner"; var HIDDEN = "_hidden"; var INTENT_EXTRA = "com.tns.activity"; var ANDROID_FRAME = "android_frame"; +var BACKSTACK_TAG = "_backstackTag"; +var NAV_DEPTH = "_navDepth"; -var navDepth = 0; +var navDepth = -1; class PageFragmentBody extends android.app.Fragment { public frame: Frame; @@ -224,16 +226,19 @@ export class Frame extends frameCommon.Frame { return; } + navDepth++; + var manager = activity.getFragmentManager(); var fragmentTransaction = manager.beginTransaction(); - var newFragmentTag = "fragment" + this.backStack.length; + var newFragmentTag = "fragment" + navDepth; var newFragment = new PageFragmentBody(this, backstackEntry); + backstackEntry[BACKSTACK_TAG] = newFragmentTag; + backstackEntry[NAV_DEPTH] = navDepth; // remember the fragment tag at page level so that we can retrieve the fragment associated with a Page instance backstackEntry.resolvedPage[TAG] = newFragmentTag; - - navDepth++; + trace.write("Frame<" + this._domId + ">.fragmentTransaction PUSH depth = " + navDepth, trace.categories.Navigation); if (this._isFirstNavigation) { @@ -261,8 +266,10 @@ export class Frame extends frameCommon.Frame { } if (this.backStack.length > 0) { - fragmentTransaction.addToBackStack(newFragmentTag); - trace.write("fragmentTransaction.addToBackStack(" + newFragmentTag + ");", trace.categories.NativeLifecycle); + // We add each entry in the backstack to avoid the "Stack corrupted" mismatch + var backstackTag = this._currentEntry[BACKSTACK_TAG]; + fragmentTransaction.addToBackStack(backstackTag); + trace.write("fragmentTransaction.addToBackStack(" + backstackTag + ");", trace.categories.NativeLifecycle); } } @@ -285,13 +292,15 @@ export class Frame extends frameCommon.Frame { trace.write("fragmentTransaction.commit();", trace.categories.NativeLifecycle); } - public _goBackCore(entry: definition.NavigationEntry) { - navDepth--; + public _goBackCore(backstackEntry: definition.BackstackEntry) { + navDepth = backstackEntry[NAV_DEPTH]; trace.write("Frame<" + this._domId + ">.fragmentTransaction POP depth = " + navDepth, trace.categories.Navigation); var manager = this._android.activity.getFragmentManager(); if (manager.getBackStackEntryCount() > 0) { - manager.popBackStack(); + // pop all other fragments up until the named one + // this handles cases where user may navigate to an inner page without adding it on the backstack + manager.popBackStack(backstackEntry[BACKSTACK_TAG], android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); } } diff --git a/ui/frame/frame.d.ts b/ui/frame/frame.d.ts index e22e1d69f..c0aaf8274 100644 --- a/ui/frame/frame.d.ts +++ b/ui/frame/frame.d.ts @@ -89,6 +89,7 @@ declare module "ui/frame" { //@private _processNavigationQueue(page: pages.Page); + _updateActionBar(page?: pages.Page); //@endprivate /** @@ -144,6 +145,12 @@ declare module "ui/frame" { * True to navigate to the new Page using animated transitions, false otherwise. */ animated?: boolean; + + /** + * True to record the navigation in the backstack, false otherwise. + * If the parameter is set to false then the Page will be displayed but once navigated from it will not be able to be navigated back to. + */ + backstackVisible?: boolean; } /** diff --git a/ui/frame/frame.ios.ts b/ui/frame/frame.ios.ts index 6c0180b18..00e7c5f2f 100644 --- a/ui/frame/frame.ios.ts +++ b/ui/frame/frame.ios.ts @@ -10,13 +10,13 @@ import types = require("utils/types"); global.moduleMerge(frameCommon, exports); var ENTRY = "_entry"; +var NAV_DEPTH = "_navDepth"; -var navDepth = 0; +var navDepth = -1; export class Frame extends frameCommon.Frame { private _ios: iOSFrame; private _paramToNavigate: any; - public _shouldSkipNativePop: boolean = false; public _navigateToEntry: definition.BackstackEntry; constructor() { @@ -48,31 +48,73 @@ export class Frame extends frameCommon.Frame { throw new Error("Required page does have an viewController created."); } + navDepth++; + var animated = false; if (this.currentPage) { animated = this._getIsAnimatedNavigation(backstackEntry.entry); } - this.updateNavigationBar(); - + backstackEntry[NAV_DEPTH] = navDepth; viewController[ENTRY] = backstackEntry; + this._navigateToEntry = backstackEntry; - navDepth++; - trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); - this._ios.controller.pushViewControllerAnimated(viewController, animated); - } + this._updateActionBar(backstackEntry.resolvedPage); - public _goBackCore(entry: definition.NavigationEntry) { - navDepth--; - trace.write("Frame<" + this._domId + ">.popViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); - if (!this._shouldSkipNativePop) { - this._ios.controller.popViewControllerAnimated(this._getIsAnimatedNavigation(entry)); + if (this._currentEntry && !this._isEntryBackstackVisible(this._currentEntry)) { + var newControllers = NSMutableArray.alloc().initWithArray(this._ios.controller.viewControllers); + if (newControllers.count === 0) { + throw new Error("Wrong controllers count."); + } + + var newController: UIViewController = backstackEntry.resolvedPage.ios; + + // the code below fixes a phantom animation that appears on the Back button in this case + // TODO: investigate why the animation happens at first place before working around it + newController.navigationItem.hidesBackButton = this.backStack.length === 0; + + // swap the top entry with the new one + newControllers.removeLastObject(); + newControllers.addObject(newController); + + // replace the controllers instead of pushing directly + this._ios.controller.setViewControllersAnimated(newControllers, animated); } + else { + this._ios.controller.pushViewControllerAnimated(viewController, animated); + } + + trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); } - public updateNavigationBar(page?: pages.Page): void { - var previousValue = !!this._ios.showNavigationBar; - var newValue: boolean = false; + public _goBackCore(backstackEntry: definition.BackstackEntry) { + navDepth = backstackEntry[NAV_DEPTH]; + trace.write("Frame<" + this._domId + ">.popViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); + + var controller = backstackEntry.resolvedPage.ios; + var animated = this._getIsAnimatedNavigation(backstackEntry.entry); + this._navigateToEntry = backstackEntry; + + this._updateActionBar(backstackEntry.resolvedPage); + this._ios.controller.popToViewControllerAnimated(controller, animated); + } + + public _updateActionBar(page?: pages.Page): void { + super._updateActionBar(page); + + var page = page || this.currentPage; + var newValue = this._getNavBarVisible(page); + + this._ios.showNavigationBar = newValue; + } + + public _getNavBarVisible(page: pages.Page) { + if (!page) { + return false; + } + + var newValue = false; + switch (this._ios.navBarVisibility) { case enums.NavigationBarVisibility.always: newValue = true; @@ -83,21 +125,17 @@ export class Frame extends frameCommon.Frame { break; case enums.NavigationBarVisibility.auto: - var pageInstance: pages.Page = page || this.currentPage; - if (pageInstance && types.isDefined(pageInstance.actionBarHidden)) { - newValue = !pageInstance.actionBarHidden; + if (page && types.isDefined(page.actionBarHidden)) { + newValue = !page.actionBarHidden; } else { - newValue = this.backStack.length > 0 || (pageInstance && !pageInstance.actionBar._isEmpty()); + newValue = this.backStack.length > 0 || (page && !page.actionBar._isEmpty()); } newValue = !!newValue; // Make sure it is boolean break; } - this._ios.showNavigationBar = newValue; - if (previousValue !== newValue) { - this.requestLayout(); - } + return newValue; } public get ios(): definition.iOSFrame { @@ -160,7 +198,7 @@ class UINavigationControllerImpl extends UINavigationController implements UINav public static ObjCProtocols = [UINavigationControllerDelegate]; static new(): UINavigationControllerImpl { - return super.new(); + return new UINavigationControllerImpl(); } private _owner: Frame; @@ -170,6 +208,10 @@ class UINavigationControllerImpl extends UINavigationController implements UINav return this; } + get owner(): Frame { + return this._owner; + } + public viewDidLoad(): void { this.view.autoresizesSubviews = false; this.view.autoresizingMask = UIViewAutoresizing.UIViewAutoresizingNone; @@ -181,65 +223,37 @@ class UINavigationControllerImpl extends UINavigationController implements UINav this._owner._updateLayout(); } + public popViewControllerAnimated(animated: boolean): UIViewController { + // this method is called when the Back button on the NavBar is pressed + // we are always using the other method - 'popToViewControllerAnimated', so it is safe to call goBack on the Frame here + this._owner.goBack(); + + // skip the base implementation + return null; + } + public navigationControllerWillShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { // In this method we need to layout the new page otherwise page will be shown empty and update after that which is bad UX. - var frame = this._owner; + var currEntry = this._owner._currentEntry; + if (currEntry) { + this._owner._removeView(currEntry.resolvedPage); + } + var newEntry: definition.BackstackEntry = viewController[ENTRY]; var newPage = newEntry.resolvedPage; - if (!newPage.parent) { - if (!frame._currentEntry) { - // First navigation - frame._currentEntry = newEntry; - } - else { - frame._navigateToEntry = newEntry; - } - frame._addView(newPage); - } - else if (newPage.parent !== frame) { - throw new Error("Page is already shown on another frame."); - } + this._owner._currentEntry = newEntry; + this._owner._navigateToEntry = undefined; + this._owner._addView(newEntry.resolvedPage); + this._owner._updateActionBar(newEntry.resolvedPage); + newPage.onNavigatedTo(); newPage.actionBar.update(); } public navigationControllerDidShowViewControllerAnimated(navigationController: UINavigationController, viewController: UIViewController, animated: boolean): void { - - var frame: Frame = this._owner; - var backStack = frame.backStack; - var currentEntry = backStack.length > 0 ? backStack[backStack.length - 1] : null; - var newEntry: definition.BackstackEntry = viewController[ENTRY]; - - // This code check if navigation happened through UI (e.g. back button or swipe gesture). - // When calling goBack on frame isBack will be false. - var isBack: boolean = currentEntry && newEntry === currentEntry; - if (isBack) { - try { - frame._shouldSkipNativePop = true; - frame.goBack(); - } - finally { - frame._shouldSkipNativePop = false; - } - } - - var page = frame.currentPage; - if (page && !navigationController.viewControllers.containsObject(page.ios)) { - frame._removeView(page); - } - - frame._navigateToEntry = null; - frame._currentEntry = newEntry; - frame.updateNavigationBar(); - - frame.ios.controller.navigationBar.backIndicatorImage - - var newPage = newEntry.resolvedPage; - - // notify the page - newPage.onNavigatedTo(); - frame._processNavigationQueue(newPage); + var entry: definition.BackstackEntry = viewController[ENTRY]; + this._owner._processNavigationQueue(entry.resolvedPage); } public supportedInterfaceOrientation(): number { @@ -258,7 +272,7 @@ class iOSFrame implements definition.iOSFrame { this._controller = UINavigationControllerImpl.new().initWithOwner(owner); this._controller.delegate = this._controller; this._controller.automaticallyAdjustsScrollViewInsets = false; - this.showNavigationBar = false; + //this.showNavigationBar = false; this._navBarVisibility = enums.NavigationBarVisibility.auto; } @@ -270,8 +284,13 @@ class iOSFrame implements definition.iOSFrame { return this._showNavigationBar; } public set showNavigationBar(value: boolean) { + var change = this._showNavigationBar !== value; this._showNavigationBar = value; this._controller.navigationBarHidden = !value; + + if (change) { + this._controller.owner.requestLayout(); + } } public get navBarVisibility(): string { diff --git a/ui/page/page.ios.ts b/ui/page/page.ios.ts index f79348e85..bd8e081d0 100644 --- a/ui/page/page.ios.ts +++ b/ui/page/page.ios.ts @@ -3,7 +3,6 @@ import definition = require("ui/page"); import viewModule = require("ui/core/view"); import trace = require("trace"); import utils = require("utils/utils"); -import types = require("utils/types"); global.moduleMerge(pageCommon, exports); @@ -149,9 +148,9 @@ export class Page extends pageCommon.Page { } public _updateActionBar(hidden: boolean) { - if (types.isDefined(hidden) && this.ios.navigationController.navigationBarHidden !== hidden) { - this.ios.navigationController.navigationBarHidden = hidden; - this.requestLayout(); + var frame = this.frame; + if (frame) { + frame._updateActionBar(this); } }