Files
NativeScript/tns-core-modules/ui/frame/frame-common.ts
Hristo Hristov 7c68953009 Fix clear history transition (#4951)
* fix: Navigation test app added

* Removed native popToBackstack call.
Implemented custom fragment save/restore state.
When navigating back we reverse manually transitions/animations because we no longer add them to navite backstack.
Fragment instance stored on entry.
Animation and Transition listeners now holds reference to entry instead of fragment for easier update of fragment.
Animation and Transition listeners removed when entry removed from backstack.
Animation and Transition removed from fragment when fragment activity is destroyed.

* Revert package.json start up entry
Fixed bug where goBack took the last element in backstack while navigationQueue is not empty.
Fixed bug where goBack to specific entry in the backstack was removing that entry...
Removed duplicated method
Refactored method name
Fixed TS
2017-10-20 08:37:36 +03:00

630 lines
19 KiB
TypeScript

// Definitions.
import { Frame as FrameDefinition, NavigationEntry, BackstackEntry, NavigationTransition } from ".";
import { Page } from "../page";
// Types.
import { View, CustomLayoutView, isIOS, isAndroid, traceEnabled, traceWrite, traceCategories, EventData } from "../core/view";
import { resolveFileName } from "../../file-system/file-name-resolver";
import { knownFolders, path } from "../../file-system";
import { parse, loadPage } from "../builder";
import * as application from "../../application";
import { profile } from "tns-core-modules/profiling";
export { application };
export * from "../core/view";
function onLivesync(args: EventData): void {
// give time to allow fileNameResolver & css to reload.
setTimeout(() => {
let g = <any>global;
// Close the error page if available and remove the reference from global context.
if (g.errorPage) {
g.errorPage.closeModal();
g.errorPage = undefined;
}
try {
g.__onLiveSyncCore();
} catch (ex) {
// Show the error as modal page, save reference to the page in global context.
g.errorPage = parse(`<Page><ScrollView><Label text="${ex}" textWrap="true" style="color: red;" /></ScrollView></Page>`);
g.errorPage.showModal();
}
});
}
application.on("livesync", onLivesync);
let frameStack: Array<FrameBase> = [];
function buildEntryFromArgs(arg: any): NavigationEntry {
let entry: NavigationEntry;
if (typeof arg === "string") {
entry = {
moduleName: arg
};
} else if (typeof arg === "function") {
entry = {
create: arg
}
} else {
entry = arg;
}
return entry;
}
export function reloadPage(): void {
const frame = topmost();
if (frame) {
if (frame.currentPage && frame.currentPage.modal) {
frame.currentPage.modal.closeModal();
}
const currentEntry = frame._currentEntry.entry;
const newEntry: NavigationEntry = {
animated: false,
clearHistory: true,
context: currentEntry.context,
create: currentEntry.create,
moduleName: currentEntry.moduleName,
backstackVisible: currentEntry.backstackVisible
}
frame.navigate(newEntry);
}
}
// attach on global, so it can be overwritten in NativeScript Angular
(<any>global).__onLiveSyncCore = reloadPage;
const entryCreatePage = profile("entry.create", (entry: NavigationEntry): Page => {
const page = entry.create();
if (!page) {
throw new Error("Failed to create Page with entry.create() function.");
}
return page;
});
interface PageModuleExports {
createPage?: () => Page;
}
const moduleCreatePage = profile("module.createPage", (moduleNamePath: string, moduleExports: PageModuleExports): Page => {
if (traceEnabled()) {
traceWrite("Calling createPage()", traceCategories.Navigation);
}
var page = moduleExports.createPage();
let cssFileName = resolveFileName(moduleNamePath, "css");
// If there is no cssFile only appCss will be applied at loaded.
if (cssFileName) {
page.addCssFile(cssFileName);
}
return page;
});
const loadPageModule = profile("loadPageModule", (moduleNamePath: string, entry: NavigationEntry): PageModuleExports => {
// web-pack case where developers register their page JS file manually.
if (global.moduleExists(entry.moduleName)) {
if (traceEnabled()) {
traceWrite("Loading pre-registered JS module: " + entry.moduleName, traceCategories.Navigation);
}
return global.loadModule(entry.moduleName);
} else {
let moduleExportsResolvedPath = resolveFileName(moduleNamePath, "js");
if (moduleExportsResolvedPath) {
if (traceEnabled()) {
traceWrite("Loading JS file: " + moduleExportsResolvedPath, traceCategories.Navigation);
}
// Exclude extension when doing require.
moduleExportsResolvedPath = moduleExportsResolvedPath.substr(0, moduleExportsResolvedPath.length - 3)
return global.loadModule(moduleExportsResolvedPath);
}
}
return null;
});
const pageFromBuilder = profile("pageFromBuilder", (moduleNamePath: string, moduleExports: any): Page => {
let page: Page;
// Possible XML file path.
let fileName = resolveFileName(moduleNamePath, "xml");
if (fileName) {
if (traceEnabled()) {
traceWrite("Loading XML file: " + fileName, traceCategories.Navigation);
}
// Or check if the file exists in the app modules and load the page from XML.
page = loadPage(moduleNamePath, fileName, moduleExports);
}
// Attempts to implement https://github.com/NativeScript/NativeScript/issues/1311
// if (page && fileName === `${moduleNamePath}.port.xml` || fileName === `${moduleNamePath}.land.xml`){
// page["isBiOrientational"] = true;
// }
return page;
});
export const resolvePageFromEntry = profile("resolvePageFromEntry", (entry: NavigationEntry): Page => {
let page: Page;
if (entry.create) {
page = entryCreatePage(entry);
} else if (entry.moduleName) {
// Current app full path.
let currentAppPath = knownFolders.currentApp().path;
//Full path of the module = current app full path + module name.
const moduleNamePath = path.join(currentAppPath, entry.moduleName);
traceWrite("frame module path: " + moduleNamePath, traceCategories.Navigation);
traceWrite("frame module module: " + entry.moduleName, traceCategories.Navigation);
const moduleExports = loadPageModule(moduleNamePath, entry);
if (moduleExports && moduleExports.createPage) {
page = moduleCreatePage(moduleNamePath, moduleExports);
} else {
// cssFileName is loaded inside pageFromBuilder->loadPage
page = pageFromBuilder(moduleNamePath, moduleExports);
}
if (!page) {
throw new Error("Failed to load page XML file for module: " + entry.moduleName);
}
}
return page;
});
export interface NavigationContext {
entry: BackstackEntry;
isBackNavigation: boolean;
}
export class FrameBase extends CustomLayoutView implements FrameDefinition {
public static androidOptionSelectedEvent = "optionSelected";
private _animated: boolean;
public _currentEntry: BackstackEntry;
private _transition: NavigationTransition;
private _backStack = new Array<BackstackEntry>();
private _navigationQueue = new Array<NavigationContext>();
public _isInFrameStack = false;
public static defaultAnimatedNavigation = true;
public static defaultTransition: NavigationTransition;
// TODO: Currently our navigation will not be synchronized in case users directly call native navigation methods like Activity.startActivity.
public canGoBack(): boolean {
return this._backStack.length > 0;
}
/**
* Navigates to the previous entry (if any) in the back stack.
* @param to The backstack entry to navigate back to.
*/
public goBack(backstackEntry?: BackstackEntry) {
if (traceEnabled()) {
traceWrite(`GO BACK`, traceCategories.Navigation);
}
if (!this.canGoBack()) {
// TODO: Do we need to throw an error?
return;
}
if (backstackEntry) {
const backIndex = this._backStack.indexOf(backstackEntry);
if (backIndex < 0) {
return;
}
}
const navigationContext: NavigationContext = {
entry: backstackEntry,
isBackNavigation: true
}
this._navigationQueue.push(navigationContext);
if (this._navigationQueue.length === 1) {
this._processNavigationContext(navigationContext);
}
else {
if (traceEnabled()) {
traceWrite(`Going back scheduled`, traceCategories.Navigation);
}
}
}
public _removeBackstackEntries(removed: BackstackEntry[]): void {
// Handled in android.
}
// Attempts to implement https://github.com/NativeScript/NativeScript/issues/1311
// private _subscribedToOrientationChangedEvent = false;
// private _onOrientationChanged(){
// if (!this._currentEntry){
// return;
// }
// let currentPage = this._currentEntry.resolvedPage;
// let currentNavigationEntry = this._currentEntry.entry;
// if (currentPage["isBiOrientational"] && currentNavigationEntry.moduleName) {
// if (this.canGoBack()){
// this.goBack();
// }
// else {
// currentNavigationEntry.backstackVisible = false;
// }
// // Re-navigate to the same page so the other (.port or .land) xml is loaded.
// this.navigate(currentNavigationEntry);
// }
// }
public navigate(param: any) {
if (traceEnabled()) {
traceWrite(`NAVIGATE`, traceCategories.Navigation);
}
const entry = buildEntryFromArgs(param);
const page = resolvePageFromEntry(entry);
// Attempts to implement https://github.com/NativeScript/NativeScript/issues/1311
// if (page["isBiOrientational"] && entry.moduleName && !this._subscribedToOrientationChangedEvent){
// this._subscribedToOrientationChangedEvent = true;
// let app = require("application");
// if (trace.enabled) {
// trace.write(`${this} subscribed to orientationChangedEvent.`, trace.categories.Navigation);
// }
// app.on(app.orientationChangedEvent, (data) => this._onOrientationChanged());
// }
this._pushInFrameStack();
const backstackEntry: BackstackEntry = {
entry: entry,
resolvedPage: page,
navDepth: undefined,
fragmentTag: undefined
};
const navigationContext: NavigationContext = {
entry: backstackEntry,
isBackNavigation: false
}
this._navigationQueue.push(navigationContext);
if (this._navigationQueue.length === 1) {
this._processNavigationContext(navigationContext);
} else {
if (traceEnabled()) {
traceWrite(`Navigation scheduled`, traceCategories.Navigation);
}
}
}
public isCurrent(entry: BackstackEntry): boolean {
return this._currentEntry === entry;
}
public setCurrent(entry: BackstackEntry): void {
this._currentEntry = entry;
}
public _processNavigationQueue(page: Page) {
if (this._navigationQueue.length === 0) {
// This could happen when showing recreated page after activity has been destroyed.
return;
}
let entry = this._navigationQueue[0].entry;
let currentNavigationPage = entry.resolvedPage;
if (page !== currentNavigationPage) {
// If the page is not the one that requested navigation - skip it.
return;
}
// remove completed operation.
this._navigationQueue.shift();
if (this._navigationQueue.length > 0) {
let navigationContext = this._navigationQueue[0];
this._processNavigationContext(navigationContext);
}
this._updateActionBar();
}
public _findEntryForTag(fragmentTag: string): BackstackEntry {
let entry: BackstackEntry;
if (this._currentEntry && this._currentEntry.fragmentTag === fragmentTag) {
entry = this._currentEntry;
} else {
entry = this._backStack.find((value) => value.fragmentTag === fragmentTag);
// on API 26 fragments are recreated lazily after activity is destroyed.
if (!entry) {
const navigationItem = this._navigationQueue.find((value) => value.entry.fragmentTag === fragmentTag);
entry = navigationItem ? navigationItem.entry : undefined;
}
}
return entry;
}
public navigationQueueIsEmpty(): boolean {
return this._navigationQueue.length === 0;
}
public static _isEntryBackstackVisible(entry: BackstackEntry): boolean {
if (!entry) {
return false;
}
const backstackVisibleValue = entry.entry.backstackVisible;
const backstackHidden = backstackVisibleValue !== undefined && !backstackVisibleValue;
return !backstackHidden;
}
public _updateActionBar(page?: Page, disableNavBarAnimation?: boolean) {
//traceWrite("calling _updateActionBar on Frame", traceCategories.Navigation);
}
protected _processNavigationContext(navigationContext: NavigationContext) {
if (navigationContext.isBackNavigation) {
this.performGoBack(navigationContext);
} else {
this.performNavigation(navigationContext);
}
}
public _clearBackStack(): void {
this._backStack.length = 0;
}
@profile
private performNavigation(navigationContext: NavigationContext) {
let navContext = navigationContext.entry;
// TODO: This should happen once navigation is completed.
if (navigationContext.entry.entry.clearHistory) {
// Don't clear backstack immediately or we can't remove pages from frame.
} else if (FrameBase._isEntryBackstackVisible(this._currentEntry)) {
this._backStack.push(this._currentEntry);
}
this._onNavigatingTo(navContext, navigationContext.isBackNavigation);
this._navigateCore(navContext);
}
@profile
private performGoBack(navigationContext: NavigationContext) {
let backstackEntry = navigationContext.entry;
if (!backstackEntry) {
backstackEntry = this._backStack.pop();
navigationContext.entry = backstackEntry;
} else {
const index = this._backStack.indexOf(backstackEntry);
const removed = this._backStack.splice(index + 1);
this._backStack.pop();
this._removeBackstackEntries(removed);
}
this._onNavigatingTo(backstackEntry, true);
this._goBackCore(backstackEntry);
}
public _goBackCore(backstackEntry: BackstackEntry) {
if (traceEnabled()) {
traceWrite(`GO BACK CORE(${this._backstackEntryTrace(backstackEntry)}); currentPage: ${this.currentPage}`, traceCategories.Navigation);
}
}
public _navigateCore(backstackEntry: BackstackEntry) {
if (traceEnabled()) {
traceWrite(`NAVIGATE CORE(${this._backstackEntryTrace(backstackEntry)}); currentPage: ${this.currentPage}`, traceCategories.Navigation);
}
}
public _onNavigatingTo(backstackEntry: BackstackEntry, isBack: boolean) {
if (this.currentPage) {
this.currentPage.onNavigatingFrom(isBack);
}
backstackEntry.resolvedPage.onNavigatingTo(backstackEntry.entry.context, isBack, backstackEntry.entry.bindingContext);
}
public get animated(): boolean {
return this._animated;
}
public set animated(value: boolean) {
this._animated = value;
}
public get transition(): NavigationTransition {
return this._transition;
}
public set transition(value: NavigationTransition) {
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 _pushInFrameStack() {
if (this._isInFrameStack) {
return;
}
frameStack.push(this);
this._isInFrameStack = true;
}
public _popFromFrameStack() {
if (!this._isInFrameStack) {
return;
}
const top = topmost();
if (top !== this) {
throw new Error("Cannot pop a Frame which is not at the top of the navigation stack.");
}
frameStack.pop();
this._isInFrameStack = false;
}
get _childrenCount(): number {
if (this.currentPage) {
return 1;
}
return 0;
}
public eachChildView(callback: (child: View) => boolean) {
if (this.currentPage) {
callback(this.currentPage);
}
}
public _getIsAnimatedNavigation(entry: NavigationEntry): boolean {
if (entry && entry.animated !== undefined) {
return entry.animated;
}
if (this.animated !== undefined) {
return this.animated;
}
return FrameBase.defaultAnimatedNavigation;
}
public _getNavigationTransition(entry: NavigationEntry): NavigationTransition {
if (entry) {
if (isIOS && entry.transitioniOS !== undefined) {
return entry.transitioniOS;
}
if (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: Page): boolean {
throw new Error();
}
// We don't need to put Page as visual child. Don't call super.
public _addViewToNativeVisualTree(child: View): boolean {
return true;
}
// We don't need to put Page as visual child. Don't call super.
public _removeViewFromNativeVisualTree(child: View): void {
child._isAddedToNativeVisualTree = false;
}
public _printFrameBackStack() {
const length = this.backStack.length;
let i = length - 1;
console.log(`Frame Back Stack: `);
while (i >= 0) {
let backstackEntry = <BackstackEntry>this.backStack[i--];
console.log(`\t${backstackEntry.resolvedPage}`);
}
}
public _backstackEntryTrace(b: BackstackEntry): string {
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;
}
}
export function topmost(): FrameBase {
if (frameStack.length > 0) {
return frameStack[frameStack.length - 1];
}
return undefined;
}
export function goBack(): boolean {
const top = topmost();
if (top.canGoBack()) {
top.goBack();
return true;
}
if (frameStack.length > 1) {
top._popFromFrameStack();
}
return false;
}
export function stack(): Array<FrameBase> {
return frameStack;
}