import {AfterViewInit, Component, ElementRef, OnInit, ViewChild, HostListener, Host} from '@angular/core'; import {AlertController, ModalController, Platform, PopoverController, LoadingController} from '@ionic/angular'; import { SplashScreen } from '@ionic-native/splash-screen/ngx'; import { StatusBar } from '@ionic-native/status-bar/ngx'; import { ApiService } from './service/api.service'; import { Router } from '@angular/router'; import {TREE_ACTIONS, TreeComponent} from '@circlon/angular-tree-component'; import {BehaviorSubject, Observable} from 'rxjs'; import {OptionPickerComponent} from './components/option-picker/option-picker.component'; import {OptionMenuComponent} from './components/option-menu/option-menu.component'; import {SelectorComponent} from './components/sharing/selector/selector.component'; import {SessionService} from './service/session.service'; import {SearchComponent} from './components/search/Search.component'; import {NodeTypeIcons} from './structures/node-types'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'] }) export class AppComponent implements OnInit { @ViewChild('menuTree') menuTree: TreeComponent; public readonly ready$: BehaviorSubject = new BehaviorSubject(false); public addChildTarget: any = false; public deleteTarget: any = false; public menuTarget: any = false; public refreshingMenu = false; public lastClickEvent: Array = []; public nodes = []; public currentPageId: string; public options = { isExpandedField: 'expanded', animateExpand: true, actionMapping: { mouse: { dblClick: (tree, node, $event) => { const id = node.data.id; const nodeId = node.data.node_id; if ( !node.data.virtual ) { this.currentPageId = id; this.router.navigate(['/editor', { id, ...(nodeId ? { node_id: nodeId } : {}) }]); } }, click: (tree, node, $event) => { TREE_ACTIONS.FOCUS(tree, node, $event); TREE_ACTIONS.EXPAND(tree, node, $event); this.addChildTarget = false; this.deleteTarget = false; this.menuTarget = false; if ( !node.data.noChildren && (!node.data.level || node.data.level === 'manage') ) { this.addChildTarget = node; } if ( !node.data.noDelete && (!node.data.level || node.data.level === 'manage') ) { this.deleteTarget = node; } this.menuTarget = node; this.lastClickEvent = [tree, node, $event]; } } } }; public typeIcons = NodeTypeIcons; public get appName(): string { return this.session.appName || 'Noded'; } public darkMode = false; protected loader?: any; protected hasSearchOpen = false; protected initialized$: BehaviorSubject = new BehaviorSubject(false); constructor( private platform: Platform, private splashScreen: SplashScreen, private statusBar: StatusBar, private api: ApiService, protected router: Router, protected alerts: AlertController, protected popover: PopoverController, protected modal: ModalController, protected session: SessionService, protected loading: LoadingController, ) { this.initializeApp(); } _doInit() { this.reloadMenuItems().subscribe(() => { this.ready$.next(true); setTimeout(() => { this.loader.dismiss(); this.menuTree.treeModel.expandAll(); }, 10); }); } ngOnInit() { if ( !this.initialized$.getValue() ) { const sub = this.initialized$.subscribe((didInit) => { if (didInit) { this._doInit(); sub.unsubscribe(); } }); } else { this._doInit(); } } showOptions($event) { this.popover.create({ event: $event, component: OptionPickerComponent, componentProps: { toggleDark: () => this.toggleDark(), isDark: () => this.isDark(), showSearch: () => this.handleKeyboardEvent(), } }).then(popover => popover.present()); } onFilterChange($event) { const query = $event.detail.value.toLowerCase(); this.menuTree.treeModel.clearFilter(); if ( query ) { this.menuTree.treeModel.filterNodes(node => { if ( node.data.virtual ) { // "Virtual" tree nodes should always be shown return true; } return node.data.name.toLowerCase().indexOf(query) >= 0; }); } } @HostListener('document:keyup.control./', ['$event']) async handleKeyboardEvent() { if ( this.hasSearchOpen ) { return; } const modal = await this.modal.create({ component: SearchComponent, }); this.hasSearchOpen = true; await modal.present(); await modal.onDidDismiss(); this.hasSearchOpen = false; } async onNodeMenuClick($event) { let canManage = this.menuTarget.data.level === 'manage'; if ( !canManage ) { if ( !this.menuTarget.data.level ) { canManage = true; } } if ( !this.menuTarget.data.id ) { return; } const options = [ {name: 'Export to HTML', icon: 'fa fa-file-export', value: 'export_html'}, ]; const manageOptions = [ {name: 'Share Sub-Tree', icon: 'fa fa-share-alt', value: 'share'}, ]; const popover = await this.popover.create({ component: OptionMenuComponent, componentProps: { menuItems: [ ...(!canManage ? options : [...options, ...manageOptions]), ], }, event: $event, }); popover.onDidDismiss().then((result) => { if ( result.data === 'share' ) { this.modal.create({ component: SelectorComponent, componentProps: { node: this.menuTarget.data, } }).then(modal => { modal.present(); }); } else if ( result.data === 'export_html' ) { this.exportTargetAsHTML(); } }); await popover.present(); } async exportTargetAsHTML() { const exportRecord: any = await new Promise((res, rej) => { const reqData = { format: 'html', PageId: this.menuTarget.data.id, }; this.api.post(`/exports/subtree`, reqData).subscribe({ next: (result) => { res(result.data); }, error: rej }); }); const dlUrl = this.api._build_url(`/exports/${exportRecord.UUID}/download`); window.open(dlUrl, '_blank'); } async onTopLevelCreate() { const alert = await this.alerts.create({ header: 'Create Page', message: 'Please enter a new name for the page:', cssClass: 'page-prompt', inputs: [ { name: 'name', type: 'text', placeholder: 'My Awesome Page' } ], buttons: [ { text: 'Cancel', role: 'cancel', cssClass: 'secondary' }, { text: 'Create', handler: async args => { this.api.post('/page/create', args).subscribe(res => { this.router.navigate(['/editor', { id: res.data.UUID }]); this.reloadMenuItems().subscribe(); }); } } ] }); await alert.present(); } async onChildCreate() { const alert = await this.alerts.create({ header: 'Create Sub-Page', message: 'Please enter a new name for the page:', cssClass: 'page-prompt', inputs: [ { name: 'name', type: 'text', placeholder: 'My Awesome Page' } ], buttons: [ { text: 'Cancel', role: 'cancel', cssClass: 'secondary' }, { text: 'Create', handler: async args => { args = { name: args.name, parentId: this.addChildTarget.data.id }; this.api.post('/page/create-child', args).subscribe(res => { this.reloadMenuItems().subscribe(() => { TREE_ACTIONS.EXPAND( this.lastClickEvent[0], this.lastClickEvent[1], this.lastClickEvent[2] ); this.router.navigate(['/editor', { id: res.data.UUID }]); }); }); } } ] }); await alert.present(); } async onDeleteClick() { const alert = await this.alerts.create({ header: 'Delete page?', message: 'Deleting this page will make its contents and all of its children inaccessible. Are you sure you want to continue?', buttons: [ { text: 'Keep It', role: 'cancel' }, { text: 'Delete It', handler: async () => { this.api .post(`/page/delete/${this.deleteTarget.data.id}`) .subscribe(res => { if ( this.currentPageId === this.deleteTarget.data.id ) { this.router.navigate(['/home']); } this.reloadMenuItems().subscribe(); this.deleteTarget = false; this.addChildTarget = false; this.menuTarget = false; }); } } ] }); await alert.present(); } onMenuRefresh() { this.refreshingMenu = true; this.reloadMenuItems().subscribe(); setTimeout(() => { this.refreshingMenu = false; }, 2000); } reloadMenuItems() { return new Observable(sub => { this.api.get('/menu/items').subscribe(result => { this.nodes = result.data; setTimeout(() => { sub.next(); sub.complete(); }, 0); }); }); } async initializeApp() { this.loader = await this.loading.create({ message: 'Starting up...', cssClass: 'noded-loading-mask', showBackdrop: true, }); await this.loader.present(); await this.platform.ready(); const stat: any = await this.session.stat(); if ( !stat.authenticated_user ) { window.location.href = `${stat.system_base}start`; return; } this.session.appName = stat.app_name; this.session.systemBase = stat.system_base; await this.session.initialize(); await this.statusBar.styleDefault(); await this.splashScreen.hide(); this.initialized$.next(true); } toggleDark() { // const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); this.darkMode = !this.darkMode; document.body.classList.toggle('dark', this.darkMode); } isDark() { return !!this.darkMode; } }