import { App } from "../../App"; import { AtomBinder } from "../../core/AtomBinder"; import { AtomDisposableList } from "../../core/AtomDisposableList"; import { AtomList } from "../../core/AtomList"; import { AtomLoader } from "../../core/AtomLoader"; import { AtomOnce } from "../../core/AtomOnce"; import { AtomUri } from "../../core/AtomUri"; import Bind from "../../core/Bind"; import { BindableProperty } from "../../core/BindableProperty"; import { StringHelper } from "../../core/StringHelper"; import { IClassOf, IDisposable, INotifyPropertyChanged } from "../../core/types"; import XNode from "../../core/XNode"; import { Inject } from "../../di/Inject"; import { NavigationService } from "../../services/NavigationService"; import { AtomViewModel, Watch } from "../../view-model/AtomViewModel"; import { AtomWindowViewModel } from "../../view-model/AtomWindowViewModel"; import bindProperty from "../../view-model/bindProperty"; import bindUrlParameter from "../../view-model/bindUrlParameter"; import { AtomUI } from "../core/AtomUI"; import { WindowService } from "../services/WindowService"; import { AtomTabbedPageStyle } from "../styles/AtomTabbedPageStyle"; import { AtomControl } from "./AtomControl"; import { AtomGridView } from "./AtomGridView"; import { AtomItemsControl } from "./AtomItemsControl"; import { AtomPage } from "./AtomPage"; const BindPage = Bind.forData(); export class AtomTabbedPage extends AtomControl implements INotifyPropertyChanged { public static titleTemplate = XNode.prepare("titleTemplate", true, true); public localViewModel: AtomTabViewModel; public tabChannelName: string; public titleTemplate: IClassOf; public presenter: HTMLElement; private mWindowService: WindowService; protected get windowService(): WindowService { return this.mWindowService || (this.mWindowService = this.resolve(WindowService)); } private mSelectedPage: AtomPage; public get selectedPage(): AtomPage { return this.mSelectedPage; } public set selectedPage(value: AtomPage) { this.mSelectedPage = value; if (value && value.element) { const pe = value.element.parentElement; if (pe !== this.presenter) { // const p = document.createElement("div"); // const s = p.style; // p.className = "page-host"; // p.appendChild(value.element); this.presenter.appendChild(value.element); const ve = value; value.bind(value.element, "styleDisplay", [["this", "selectedPage"]], false, (v) => v === ve ? "" : "none", this); } } this.invalidate(); AtomBinder.refreshValue(this, "selectedPage"); } constructor(app: App, e?: HTMLElement) { super(app, e ?? document.createElement("section")); } protected preCreate(): void { super.preCreate(); this.defaultControlStyle = AtomTabbedPageStyle; this.tabChannelName = "app"; this.titleTemplate = null; this.mSelectedPage = null; this.runAfterInit(() => { this.setPrimitiveValue(this.element, "styleClass", `atom-tabbed-page ${this.controlStyle.name}`); }); this.localViewModel = this.resolve(AtomTabViewModel, () => ({ owner: this })); this.render(
this.localViewModel.selectedPage)}>
({ "tab-item": 1, "selected-tab-item": x.data === this.localViewModel.selectedPage }))}>
this.localViewModel.selectedPage = x.data)} text={BindPage.oneWay((x) => (x.data as any).viewModelTitle || x.data.title)}>
this.localViewModel.closePage(x.data))}/>
this.localViewModel.pages)} selectedItem={Bind.twoWays(() => this.localViewModel.selectedPage)} itemTemplate={Bind.oneWay(() => this.titleTemplate)}>
); this.registerDisposable(this.windowService.registerHostForWindow((e) => this.getParentHost(e))); // this.windowService.currentTarget = this.presenter; } private getParentHost(e: HTMLElement): HTMLElement { while (e) { const pe = e._logicalParent || e.parentElement; if (pe === this.presenter) { return e.parentElement; } if (!pe) { return null; } e = pe; } } } interface ITabState { urls: string[]; selectedUrl: string; } class AtomTabViewModel extends AtomViewModel { @BindableProperty public pages: AtomList; @BindableProperty public selectedPage: AtomPage; @BindableProperty public selectedUrl: string; public channel: string; public storageKey: string; @BindableProperty public tabState: ITabState; private pageUpdater = new AtomOnce(); @Inject private navigationService: NavigationService; constructor(@Inject app: App, private owner: AtomTabbedPage) { super(app); this.pages = new AtomList(); bindProperty( this, this, "selectedUrl", this, [["selectedPage"]], { fromSource: (v: any): any => { return v.tag; }, fromTarget: (v: any): any => { if (!this.pages) { return null; } return this.pages.find((p) => p.tag === v); } }); bindUrlParameter(this, "selectedUrl", "url"); } public async init(): Promise { const ch = this.owner.tabChannelName; this.storageKey = `${this.app.contextId}_${ch}`; const d = this.navigationService.registerNavigationHook( (uri, { target }) => { if ( target === this.owner.tabChannelName || (uri.protocol === "tab:" && uri.host === this.owner.tabChannelName)) { return this.loadPageForReturn(uri); } } ); this.registerDisposable(d); const urls = sessionStorage.getItem(this.storageKey) || "null"; const urlState: ITabState = JSON.parse(urls) || { name, urls: [], selectedUrl: null }; for (const iterator of urlState.urls) { const page = await this.loadPage(new AtomUri(iterator), true); if (page.tag === urlState.selectedUrl) { this.pageUpdater.run(() => { this.selectedPage = page; }); } } if (!this.selectedPage) { this.selectedPage = this.pages[0]; } } @Watch public watchSelectedPage(): void { this.saveState(this.selectedPage); } public closePage(page: AtomPage): void { this.app.runAsync(() => this.navigationService.remove(page)); } protected saveState(a?: any): void { const state: ITabState = { urls: this.pages.map((p) => p.tag), selectedUrl: this.selectedUrl, }; sessionStorage.setItem(this.storageKey, JSON.stringify(state)); } protected async loadPageForReturn(url: AtomUri): Promise { const p = await this.loadPage(url, false); try { return await (p 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 ex; } } protected async loadPage( url: AtomUri, doNotSetSelected: boolean): Promise { const uriString = url.toString(); const ws = this.navigationService as WindowService; const existing = this.pages.find((x) => x.tag === uriString); if (existing) { if (!doNotSetSelected) { if (this.selectedPage !== existing) { this.selectedPage = existing; // ws.currentTarget = existing.element; } } return existing; } // const popupType = await UMD.resolveViewClassAsync(url.path); // const page: AtomPage = (new (popupType)(this.app)) as AtomPage; const { view: page, disposables } = await AtomLoader.loadView(url, this.app, true, () => new AtomWindowViewModel(this.app)); page.title ||= StringHelper.fromPascalToTitleCase(page.constructor.name); page.tag = uriString; if (url.query && url.query.title) { page.title ||= url.query.title.toString(); } (page as any).viewModelTitle = null; page.bind(page.element, "viewModelTitle", [["viewModel", "title"]]); page.bind(page.element, "styleDisplay", [["this", "selectedPage"]], false, (v) => v === page ? "" : "none", this); this.pages.add(page); if (!doNotSetSelected) { this.selectedPage = page; } const e = page.element; // ws.currentTarget = e; disposables.add(() => { const index = this.pages.indexOf(page); if (this.pages.length <= 1 && index <= 0) { return; } this.pages.remove(page); e.remove(); // ws.currentTarget = null; if (this.selectedPage === page) { this.selectedPage = this.pages[index - 1]; } }); this.saveState(); return page; } }