import {Component, Input, OnInit, ViewChild} from '@angular/core'; import {IonInput, ModalController} from '@ionic/angular'; import {ApiService} from '../../service/api.service'; import {BehaviorSubject} from 'rxjs'; import {Router} from '@angular/router'; import {NodeTypeIcons} from '../../structures/node-types'; import {debounce, debug} from '../../utility'; import {OpenerService} from '../../service/opener.service'; export interface SearchResult { title: string; short_title: string; type: string; id: string; boxId?: string; associated?: { title: string, type: 'page', id: string }; } @Component({ selector: 'noded-search-modal', templateUrl: './Search.component.html', styleUrls: ['./Search.component.scss'], }) export class SearchComponent implements OnInit { @ViewChild('ionInput') ionInput: IonInput; @Input() query = ''; public results: BehaviorSubject = new BehaviorSubject([]); public allResults: BehaviorSubject = new BehaviorSubject([]); public loading = false; public typeIcons = NodeTypeIcons; public allResultTypes = ['all', 'page', 'node', 'db', 'code', 'files']; public resultTypes = ['all']; public typeTitles = { all: 'All', page: 'Pages', node: 'Nodes', db: 'Databases', code: 'Code', files: 'Files', }; public selectedType = 'all'; protected searchChangeDebounce = debounce(async ($event) => { const query = $event.detail.value; const results = await this.search(query); this.allResults.next(results); if ( !this.allResults.getValue() ) { this.resultTypes = ['all']; } else { const existentTypes = this.allResults.getValue().map(x => this.classifyResult(x)); this.resultTypes = this.allResultTypes.filter((x: any) => existentTypes.includes(x) || x === 'all'); } this.filterView(this.selectedType); this.loading = false; }, 1000); constructor( protected ionModalController: ModalController, protected api: ApiService, protected router: Router, protected opener: OpenerService, ) { } async dismiss() { await this.ionModalController.dismiss(); } public isDark() { return document.body.classList.contains('dark'); } filterView(name: string) { this.selectedType = name; this.results.next((this.allResults.getValue() || []).filter(x => { if ( this.selectedType === 'all' ) { return true; } return this.classifyResult(x) === this.selectedType; })); } classifyResult(result: SearchResult) { if ( ['page', 'form'].includes(result.type) ) { return 'page'; } else if ( ['node', 'markdown'].includes(result.type) ) { return 'node'; } else if ( result.type === 'code' ) { return 'code'; } else if ( result.type === 'db' ) { return 'db'; } else if ( ['file_box', 'files'].includes(result.type) || result.type.startsWith('file_box') ) { return 'files'; } } ngOnInit() { setTimeout(() => { this.ionInput.setFocus(); }, 400); } async onSearchChange($event) { this.loading = true; this.searchChangeDebounce($event); } async search(query): Promise { return new Promise(resolve => { this.api.get(`/search?query=${query}`).subscribe(res => { resolve(res.data.results as SearchResult[]); }); }); } async openResult(result: SearchResult) { debug('Search result:', result); const nodeTypes = [ 'node', 'code', 'files', 'markdown', ]; if ( result.type === 'page' ) { await this.opener.openTarget(result.id); await this.dismiss(); } else if ( nodeTypes.includes(result.type) ) { await this.opener.openTarget(result.associated.id, result.id); await this.dismiss(); } else if ( result.type === 'db' ) { await this.opener.openDatabase(result.associated.id, result.id); } else if ( result.type.startsWith('file_box') ) { await this.opener.openFileBox(result.associated.id, result.id, result.boxId); } else { debug('No open handler for search result!'); } } async openRelated(event, result: SearchResult) { event.preventDefault(); event.stopPropagation(); if ( result.associated ) { if ( result.associated.type === 'page' ) { await this.opener.openTarget(result.associated.id); await this.dismiss(); } } } }