Add support for full-text search (#7)
parent
6297f9d0f0
commit
28d6986eea
@ -1,14 +1,18 @@
|
||||
<ion-list>
|
||||
<ion-item button (click)="onSelect('search_everywhere')">
|
||||
<i slot="start" class="fa fa-search"></i>
|
||||
<ion-label>Search Everywhere</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('html_export')">
|
||||
<ion-icon slot="start" name="menu"></ion-icon>
|
||||
<i slot="start" class="fa fa-code"></i>
|
||||
<ion-label>Export to HTML Site</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('toggle_darkmode')">
|
||||
<ion-icon slot="start" [name]="isDark() ? 'sun' : 'moon'"></ion-icon>
|
||||
<i slot="start" class="fa" [ngClass]="isDark() ? 'fa-sun' : 'fa-moon'"></i>
|
||||
<ion-label>{{ isDark() ? 'To The Light!' : 'Go Dark...' }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="onSelect('logout')">
|
||||
<ion-icon slot="start" name="exit"></ion-icon>
|
||||
<i slot="start" class="fa fa-sign-out-alt"></i>
|
||||
<ion-label>Logout</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
@ -0,0 +1,36 @@
|
||||
<div class="container">
|
||||
<ion-header class="search-header">
|
||||
<ion-input
|
||||
placeholder="Start typing to search..."
|
||||
autofocus
|
||||
#ionInput
|
||||
class="search-input"
|
||||
(ionChange)="onSearchChange($event)"
|
||||
></ion-input>
|
||||
</ion-header>
|
||||
<div class="search-results">
|
||||
<ion-list>
|
||||
<ion-item
|
||||
*ngFor="let result of (results | async)"
|
||||
button
|
||||
[title]="'Open '+result.type"
|
||||
(click)="openResult(result)"
|
||||
>
|
||||
<ion-label class="search-label" [ngClass]="result.type">
|
||||
<i class="search-icon" [ngClass]="typeIcons[result.type]"></i>
|
||||
<div class="search-title">{{ result.short_title }}</div>
|
||||
</ion-label>
|
||||
<div
|
||||
class="search-assoc"
|
||||
[ngClass]="result.associated.type"
|
||||
*ngIf="result.associated"
|
||||
[title]="'Open associated '+result.associated.type"
|
||||
(click)="openRelated($event, result)"
|
||||
>
|
||||
<i class="assoc-icon" [ngClass]="typeIcons[result.associated.type]"></i>
|
||||
<div class="assoc-title">{{ result.associated.title }}</div>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,54 @@
|
||||
.search-header {
|
||||
padding: 7px;
|
||||
|
||||
.search-input {
|
||||
font-size: 16pt;
|
||||
}
|
||||
}
|
||||
|
||||
.search-label {
|
||||
display: flex;
|
||||
|
||||
.search-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.page {
|
||||
.search-icon {
|
||||
color: var(--noded-background-note);
|
||||
}
|
||||
}
|
||||
|
||||
&.node {
|
||||
.search-icon {
|
||||
color: var(--noded-background-node);
|
||||
content: '\f10d';
|
||||
}
|
||||
}
|
||||
|
||||
&.db {
|
||||
.search-icon {
|
||||
color: var(--noded-background-db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-assoc {
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
||||
.assoc-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.page {
|
||||
background: var(--noded-background-note);
|
||||
color: var(--noded-color-note);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: var(--noded-background-note-hover);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
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';
|
||||
|
||||
export interface SearchResult {
|
||||
title: string;
|
||||
short_title: string;
|
||||
type: 'page' | 'node';
|
||||
id: string;
|
||||
associated?: {
|
||||
title: string,
|
||||
type: 'page' | 'node',
|
||||
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<SearchResult[]> = new BehaviorSubject<SearchResult[]>([]);
|
||||
|
||||
public typeIcons = {
|
||||
node: 'fa fa-quote-left',
|
||||
page: 'fa fa-sticky-note',
|
||||
db: 'fa fa-database',
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected modal: ModalController,
|
||||
protected api: ApiService,
|
||||
protected router: Router,
|
||||
) { }
|
||||
|
||||
async dismiss() {
|
||||
await this.modal.dismiss();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
setTimeout(() => {
|
||||
this.ionInput.setFocus();
|
||||
}, 750);
|
||||
}
|
||||
|
||||
async onSearchChange($event) {
|
||||
const query = $event.detail.value;
|
||||
this.results.next(await this.search(query));
|
||||
}
|
||||
|
||||
async search(query): Promise<SearchResult[]> {
|
||||
return new Promise(resolve => {
|
||||
this.api.get(`/search?query=${query}`).subscribe(res => {
|
||||
resolve(res.data.results as SearchResult[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async openResult(result: SearchResult) {
|
||||
if ( result.type === 'page' ) {
|
||||
await this.router.navigate(['/editor', { id: result.id }]);
|
||||
await this.dismiss();
|
||||
} else if ( result.type === 'node' ) {
|
||||
await this.router.navigate(['/editor', { id: result.associated.id, node_id: result.id }]);
|
||||
await this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
async openRelated(event, result: SearchResult) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if ( result.associated ) {
|
||||
if ( result.associated.type === 'page' ) {
|
||||
await this.router.navigate(['/editor', { id: result.associated.id }]);
|
||||
await this.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue