import { AtomLoader } from "@web-atoms/core/dist/core/AtomLoader";
import { AtomUri } from "@web-atoms/core/dist/core/AtomUri";
import sleep from "@web-atoms/core/dist/core/sleep";
import XNode from "@web-atoms/core/dist/core/XNode";
import { NavigationService } from "@web-atoms/core/dist/services/NavigationService";
import { AtomWindowViewModel } from "@web-atoms/core/dist/view-model/AtomWindowViewModel";
import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
import PopupService, { IDialogOptions, PopupWindow } from "@web-atoms/core/dist/web/services/PopupService";
import PageNavigator from "../PageNavigator";
import { StringHelper } from "@web-atoms/core/dist/core/StringHelper";
import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty";
import { AtomDisposableList } from "@web-atoms/core/dist/core/AtomDisposableList";
import Bind from "@web-atoms/core/dist/core/Bind";
import { CancelToken, IDisposable } from "@web-atoms/core/dist/core/types";
import { AncestorEnumerator, ChildEnumerator } from "@web-atoms/core/dist/web/core/AtomUI";
import { displayRouteSymbol, routeSymbol } from "@web-atoms/core/dist/core/Command";
import Route from "@web-atoms/core/dist/core/Route";
import "./MobileApp.global.css";
export function PullToRefresh() {
return
this.viewModelTitle || this.title)} />;
this.iconRenderer = () => this.iconClass)}
eventClick={(e1) => this.dispatchIconClickEvent(e1)} />;
this.actionBarRenderer = () => ;
this.runAfterInit(() => {
// we will not keep in the dom
// this is to prevent any heavy animation classes slowing down performance
this.pullToRefreshElement?.remove?.();
this.initialized = true;
this.enablePullToRefreshEvents();
try {
document.body.dispatchEvent(new CustomEvent("pageNavigated", {
detail: {
title: this.title,
route: this.route || Object.getPrototypeOf(this)?.constructor?.name
},
bubbles: true,
cancelable: true
} ));
} catch {}
});
}
protected render(node: XNode, e?: any, creator?: any): void {
if (e || node?.attributes?.["data-page-element"]) {
super.render(node, e, creator);
return;
}
this.render = super.render;
const na = (node.attributes ??= {});
let extracted = {};
if (!na["data-page-element"]) {
na["data-page-element"] = "content";
extracted = this.extractControlProperties(node);
}
super.render( this.viewModel.title)}
{...extracted}>
{node}
, e, creator);
this.contentElement = this.element.querySelector("[data-page-element='content']");
if (document.body.hasAttribute("ios-keyboard")) {
return;
}
setTimeout(() => {
// this.contentElement.scrollTo(0, 0);
this.contentElement.scrollTop = 0;
this.contentElement.scrollLeft = 0;
}, 100);
}
protected enablePullToRefreshEvents() {
const e = this.pullToRefreshElement;
if (!e) {
this.pullToRefreshDisposable?.dispose();
this.pullToRefreshDisposable = void 0;
return e;
}
if (this.pullToRefreshDisposable) {
return;
}
let elementAdded = false;
const touchStart = (te: Event) => {
if (!this.pullToRefreshElement) {
this.pullToRefreshDisposable?.dispose();
this.pullToRefreshDisposable = null;
return;
}
if (this.contentElement.scrollTop > 0) {
return;
}
const isMouseEvent = te.type === "mousedown";
const moveEventName = isMouseEvent ? "mousemove" : "touchmove";
const endEventName = isMouseEvent ? "mouseup" : "touchend";
const startY = isMouseEvent ? (te as MouseEvent).screenY : (te as TouchEvent).touches[0].screenY;
this.pullToRefreshElement.dataset.mode = "down";
if (isMouseEvent) {
te.stopPropagation();
te.preventDefault();
}
const d = new AtomDisposableList();
d.add(() => {
this.contentElement.style.removeProperty("touch-action");
});
d.add(this.bindEvent(this.contentElement, moveEventName, (me: Event) => {
const screenY = isMouseEvent ? (me as MouseEvent).screenY : (me as TouchEvent).touches[0].screenY;
const diffX = Math.min(75, screenY - startY);
if (diffX > 5) {
if (!elementAdded) {
this.contentElement.style.touchAction = "none";
elementAdded = true;
this.element.appendChild(this.pullToRefreshElement);
}
} else {
return;
}
this.contentElement.style.transform = `translateY(${diffX}px)`;
if (diffX > 50) {
this.pullToRefreshElement.dataset.mode = "up";
} else {
this.pullToRefreshElement.dataset.mode = "down";
}
}, null, { passive: true }));
d.add(this.bindEvent(this.contentElement, endEventName, (ue: MouseEvent) => {
ue.stopPropagation();
if (isMouseEvent) {
ue.stopImmediatePropagation();
ue.preventDefault();
}
d.dispose();
const done = () => {
delete this.pullToRefreshElement.dataset.mode;
this.contentElement.style.transform = ``;
this.pullToRefreshElement.style.transform = "";
this.pullToRefreshElement.remove();
elementAdded = false;
};
const diffX = ue.screenY - startY;
if (diffX <= 50) {
done();
return;
}
const ce = new CustomEvent("reloadPage", { detail: this, bubbles: true, cancelable: true });
this.contentElement.dispatchEvent(ce);
if (ce.defaultPrevented) {
done();
return;
}
this.pullToRefreshElement.dataset.mode = "loading";
const promise = (ce as any).promise as PromiseLike;
if (!promise) {
done();
return;
}
promise.then(done, done);
}, null, { passive: !isMouseEvent }));
};
const ed = new AtomDisposableList();
ed.add(this.bindEvent(this.contentElement, "mousedown", touchStart));
ed.add(this.bindEvent(this.contentElement, "touchstart", touchStart, null, { passive: true }));
this.pullToRefreshDisposable = ed;
}
protected dispatchIconClickEvent(e: Event) {
const ce = new CustomEvent("iconClick", { bubbles: true });
e.target.dispatchEvent(ce);
}
protected hide() {
const { element } = this;
if (!element) {
return;
}
element.setAttribute("data-page-state", "hidden");
if (this.keep) {
setTimeout(() =>
element.setAttribute("data-page-removed", "true"), 400);
return;
}
element._logicalParent = element.parentElement;
this.scrollTop = this.contentElement?.scrollTop;
setTimeout(() => {
element?.remove();
}, 400);
}
protected show() {
if (this.keep) {
this.element.removeAttribute("data-page-removed");
} else {
this.element._logicalParent.appendChild(this.element);
}
setTimeout(() => {
if (this.scrollTop) {
this.contentElement.scrollTop = this.scrollTop;
}
this.element.setAttribute("data-page-state", "ready");
}, 1);
}
}
delete (BasePage.prototype as any).init;
export class ContentPage extends BasePage {
public parameters: T;
public close: (result: TResult) => any;
}
export type InputOf = T extends ContentPage ? T : any;
export type OutputOf = T extends ContentPage ? T : any;
export class TabbedPage extends BasePage {
}
export default class MobileApp extends AtomControl {
public static current: MobileApp;
public static drawer = XNode.prepare("drawer", true, true);
public static async pushPage(
pageUri: string | any,
parameters: {[key: string]: any} = {},
clearHistory: boolean = false) {
const mobileApp = this.current;
const page = await AtomLoader.loadClass(pageUri, parameters ?? {}, mobileApp.app);
const result = await mobileApp.loadPage(page, clearHistory);
return result as T;
}
public drawer: typeof Drawer;
public hideDrawer: () => void;
public pages: BasePage[];
public selectedPage: BasePage;
/**
* Set this to class or url to load the page when user
* hits back button when there is nothing in history stack
*/
public defaultPage: any;
private container: HTMLDivElement;
public popTo(w: any) {
this.app.runAsync(async () => {
while (this.selectedPage !== w) {
this.selectedPage.cancel();
await sleep(500);
}
});
}
public async back() {
if (this.pages.length === 0) {
const drawer = this.drawer;
if (drawer && !this.hideDrawer) {
const drawerPage = new drawer(this.app);
// (drawerPage as any).init?.()?.catch((error) => {
// if (!CancelToken.isCancelled(error)) {
// // tslint:disable-next-line: no-console
// console.error(error);
// }
// });
const modalClose = (ce: Event) => {
let start = ce.target as HTMLElement;
const de = drawerPage.element;
while (start) {
if (start === de) {
return;
}
start = start.parentElement;
}
ce.preventDefault();
ce.stopImmediatePropagation?.();
ce.target.dispatchEvent(new CustomEvent("closeDrawer", { bubbles: true }));
};
// const da = drawerNode.attributes ??= {};
const dispatchCloseDrawer = (de: Event) => {
if (de.defaultPrevented) {
return;
}
const target = de.target as HTMLElement;
if(AncestorEnumerator.find(target, (x) => x === drawerPage.element)) {
return;
}
de.target.dispatchEvent(new CustomEvent("closeDrawer", { bubbles: true }));
};
this.element.appendChild(drawerPage.element);
setTimeout(() => {
this.element.dataset.drawer = "visible";
drawerPage.bindEvent(this.element, "click", dispatchCloseDrawer);
drawerPage.bindEvent(document.body, "click", modalClose, null, true);
}, 10);
this.hideDrawer = () => {
this.element.dataset.drawer = "";
setTimeout(() => {
const de = drawerPage.element;
drawerPage.dispose();
de.remove();
}, 400);
this.hideDrawer = undefined;
};
return false;
}
return true;
}
this.selectedPage.cancel("cancelled");
return false;
}
protected async init() {
}
protected preCreate(): void {
MobileApp.current = this;
// tslint:disable-next-line: ban-types
window.addEventListener("backButton", (ce: CustomEvent) => {
const { detail } = ce;
ce.preventDefault();
this.app.runAsync(async () => {
if (await this.back()) {
detail();
}
});
});
// disable top level scroll
// document.body.style.overflow = "hidden";
this.drawer = null;
this.element.dataset.pageApp = "page-app";
const container = this.container = document.createElement("div");
container.dataset.container = "true";
this.element.appendChild(container);
this.pages = [];
this.selectedPage = null;
this.bindEvent(this.element, "iconClick", (e) => this.back());
this.bindEvent(this.element, "closeDrawer", (e) => {
this.hideDrawer?.();
});
const navigationService = this.app.resolve(NavigationService);
navigationService.registerNavigationHook(
(uri, { target, clearHistory }) => {
if (/^(app|root)$/.test(target)) {
return this.loadPageForReturn(uri, clearHistory);
}
}
);
this.runAfterInit(() => this.app.runAsync(() => this.init()));
}
protected async loadPageForReturn(url: AtomUri, clearHistory: boolean): Promise {
this.defaultPage ??= url;
const page = await AtomLoader.loadControl(url, this.app);
if (url.query && url.query.title) {
page.title ||= url.query.title.toString();
}
const p = await this.loadPage(page, clearHistory);
try {
return await (p as any);
} 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 ex;
}
}
protected async loadPage(page: BasePage, clearHistory: boolean) {
page.title ||= StringHelper.fromPascalToTitleCase(Object.getPrototypeOf(page).constructor.name);
const { parameters } = page as any;
const route = parameters?.[displayRouteSymbol];
if (route) {
page.route = route;
}
const selectedPage = this.selectedPage;
if (selectedPage) {
(selectedPage as any).hide();
this.pages.add(selectedPage);
}
const hasPages = !clearHistory && this.pages.length;
if (clearHistory) {
for (const iterator of this.pages) {
const e = iterator.element;
iterator.dispose();
e.remove();
}
this.pages.length = 0;
this.selectedPage = null;
}
if (!hasPages) {
page.element.dataset.pageState = "ready";
page.iconClass = "fas fa-bars";
} else {
page.iconClass = "fas fa-arrow-left";
}
this.container.appendChild(page.element);
this.selectedPage = page;
const vm = page.viewModel as AtomWindowViewModel;
const element = page.element;
return new Promise((resolve, reject) => {
const closeFactory = (callback, result?) =>
(r) => {
if (clearHistory) {
this.app.runAsync(() => this.loadPageForReturn(this.defaultPage, true));
return;
}
// element.dataset.pageState = "";
element.removeAttribute("data-page-state");
const last = this.pages.pop();
(last as any).show();
setTimeout(() => {
page.dispose();
element.remove();
callback(r ?? result);
this.selectedPage = last;
}, 300);
};
const cancel = closeFactory(reject, "cancelled") as any;
const close = closeFactory(resolve);
if (vm) {
vm.cancel = cancel;
vm.close = close;
}
page.cancel = cancel;
page.close = close;
});
}
}
export const isMobileView = /Android|iPhone/i.test(navigator.userAgent)
|| (window.visualViewport.width < 500)
|| (window as any).forceMobileLayout;
const isPopupPage = Symbol("isPopupPage");
class PopupWindowEx extends PopupWindow {
static [isPopupPage] = true;
public static dialogOptions: IDialogOptions;
}
const root = (isMobileView ? ContentPage : PopupWindowEx) as typeof AtomControl;
export class PopupWindowPage extends (root as any as typeof ContentPage) {
public parameters: TIn;
public static dialogOptions: IDialogOptions;
public close: (r: TOut) => void;
public cancel: (error?: any) => void;
public title: string;
public headerRenderer: () => XNode;
public footerRenderer: () => XNode;
public titleRenderer: () => XNode;
}
PageNavigator.pushPageForResult =
(page, parameters, clearHistory) => {
if (!isMobileView
&& page[isPopupPage]
) {
const popupPage = page as any as typeof PopupWindowEx;
const options: IDialogOptions = {
... popupPage.dialogOptions ?? {},
parameters: {
parameters
}
};
// we make window modal if not set...
options.modal ??= true;
if (options.modal) {
return (page as any as typeof PopupWindow).showModal(options);
}
return (page as any as typeof PopupWindow).showWindow(options);
}
return MobileApp.pushPage(page, parameters, clearHistory)
};