@web-atoms/core
Version:
296 lines (254 loc) • 9.39 kB
text/typescript
import { App } from "../../App";
import { Atom } from "../../Atom";
import { AtomBinder } from "../../core/AtomBinder";
import { AtomDispatcher } from "../../core/AtomDispatcher";
import { AtomLoader } from "../../core/AtomLoader";
import { AtomUri } from "../../core/AtomUri";
import { BindableProperty } from "../../core/BindableProperty";
import { IClassOf, IDisposable, INotifyPropertyChanged } from "../../core/types";
import { NavigationService } from "../../services/NavigationService";
import { AtomWindowViewModel } from "../../view-model/AtomWindowViewModel";
import { AtomUI } from "../core/AtomUI";
import { WindowService } from "../services/WindowService";
import AtomFrameStyle from "../styles/AtomFrameStyle";
import { AtomControl } from "./AtomControl";
export interface IPageItem {
url: string;
page: AtomControl;
scrollY: number;
}
/**
* Creates and hosts an instance of AtomControl available at given URL. Query string parameters
* from the url will be passed to view model as initial property values.
*
* By default stack is turned off, so elements and controls will be destroyed when new control is hosted.
*/
export class AtomFrame
extends AtomControl
implements INotifyPropertyChanged {
public stack: IPageItem[];
public get canGoBack(): boolean {
return this.stack.length ? true : false;
}
public name: string;
public keepStack: boolean;
public current: AtomControl;
public pagePresenter: HTMLElement;
public currentDisposable: IDisposable;
public backCommand: () => void;
public saveScrollPosition: boolean;
private mUrl: string;
public get url(): string {
return this.mUrl;
}
public set url(value: string) {
if (this.mUrl === value) {
return;
}
if (value === undefined) {
return;
}
this.mUrl = value;
this.runAfterInit(() => {
this.app.runAsync(() => this.loadForReturn(value === null ? null : new AtomUri(value), true));
});
}
private navigationService: WindowService;
constructor(app: App, e?: HTMLElement) {
super(app, e || document.createElement("section"));
}
public async onBackCommand(): Promise<void> {
if (!this.stack.length) {
// tslint:disable-next-line: no-console
console.warn(`FrameStack is empty !!`);
return;
}
const ctrl: AtomControl = this.current;
if (ctrl) {
await this.navigationService.remove(ctrl);
}
// this.popStack();
}
/**
* This will pop page from the stack and set as current
* @param windowClosed true if current page was closed by User Action
*/
public popStack(windowClosed?: boolean): void {
if (!this.stack.length) {
// tslint:disable-next-line: no-console
console.warn(`FrameStack is empty !!`);
return;
}
const last = this.stack.pop();
AtomBinder.refreshItems(this.stack);
const old = this.current;
this.current = last.page;
(this.current.element as HTMLElement).style.display = "";
if (old) {
this.navigationService.remove(old).catch((e) =>
// tslint:disable-next-line: no-console
console.log(e));
}
this.setUrl(last.url);
if (this.saveScrollPosition) {
setTimeout(() => {
window.scrollTo(0, last.scrollY);
}, 200);
}
}
public canChange(): Promise<boolean> {
const c = this.current;
if (!c) {
return Promise.resolve(true);
}
return this.navigationService.remove(c);
}
public push(ctrl: AtomControl): void {
if (this.current) {
if (this.keepStack) {
(this.current.element as HTMLElement).style.display = "none";
this.stack.add({
url: (this.current as any)._$_url ,
page: this.current,
scrollY: this.navigationService.screen.scrollTop
});
} else {
if (this.current === ctrl) {
return;
}
const c1: AtomControl = this.current;
const e1: HTMLElement = c1.element as HTMLElement;
if (e1) {
this.navigationService.remove(c1);
}
}
}
const element: HTMLElement = ctrl.element as HTMLElement;
const e = this.pagePresenter || this.element;
(e).appendChild(element);
this.current = ctrl;
if (this.saveScrollPosition) {
window.scrollTo(0, 0);
}
}
public async load(url: AtomUri, clearHistory?: boolean): Promise<AtomControl> {
// we will not worry if we cannot close the page or not
// as we are moving in detail view, we will come back to page
// without loosing anything
if (clearHistory) {
if (! await this.canChange()) {
return;
}
}
const { view, disposables } =
await AtomLoader.loadView<AtomControl>(url, this.app, true, () => new AtomWindowViewModel(this.app));
const urlString = url.host ? url.toString() : url.pathAndQuery;
(view as any)._$_url = urlString;
this.push(view);
const e = view.element;
// this.navigationService.currentTarget = e;
this.setUrl(urlString);
disposables.add({
dispose: () => {
const closed = this.current === view;
e.innerHTML = "";
e.remove();
// this.navigationService.currentTarget = null;
this.popStack(closed);
}
});
return view;
}
public toUpperCase(s: string): string {
return s.split("-")
.filter((t) => t)
.map((t) => t.substr(0, 1).toUpperCase() + t.substr(1))
.join("");
}
protected setUrl(urlString: string) {
this.mUrl = urlString;
AtomBinder.refreshValue(this, "url");
AtomBinder.refreshValue(this, "canGoBack");
}
protected async loadForReturn(url: AtomUri, clearHistory?: boolean): Promise<any> {
const hasHistory = this.keepStack;
this.keepStack = !clearHistory;
if (url === null) {
if (hasHistory && clearHistory) {
this.clearStack();
}
return;
}
const page = await this.load(url, clearHistory);
if (hasHistory) {
if (clearHistory) {
this.clearStack();
}
}
try {
return await (page as any).returnPromise;
} catch (ex) {
// this will prevent warning in chrome for unhandled exception
if ((ex.message ? ex.message : ex) === "cancelled") {
// tslint:disable-next-line: no-console
console.warn(ex);
return;
}
// throw new Error( ex.stack ? (ex + "\r\n" + ex.stack ) : ex);
throw ex;
}
}
protected clearStack(): void {
// clear stack... irrespective of cancellation !!
for (const iterator of this.stack) {
const e = iterator.page.element;
if (e) {
iterator.page.dispose();
e.innerHTML = "";
e.remove();
}
}
this.stack.clear();
}
protected preCreate(): void {
this.name = null;
this.stack = [];
this.keepStack = false;
this.current = null;
this.currentDisposable = null;
this.saveScrollPosition = false;
this.navigationService = this.app.resolve(NavigationService) as WindowService;
this.defaultControlStyle = AtomFrameStyle;
this.pagePresenter = null;
this.mUrl = null;
AtomUI.assignID(this.element);
this.runAfterInit(() => {
this.setPrimitiveValue(this.element, "styleClass", this.controlStyle.name);
});
this.backCommand = () => this.app.runAsync(() => this.onBackCommand());
// hook navigation...
const d = this.navigationService.registerNavigationHook((url, {
target,
clearHistory,
cancelToken }) => {
if (this.name) {
if (target !== this.name) {
return undefined;
}
} else {
if (
target !== "frame"
&& url.protocol !== "frame:") {
return undefined;
}
}
if (cancelToken) {
cancelToken.registerForCancel(() => {
this.backCommand();
});
}
return this.loadForReturn(url, clearHistory);
});
this.registerDisposable(d);
}
}