Add support for full-text search (#7)
parent
6297f9d0f0
commit
28d6986eea
@ -1,14 +1,18 @@
|
|||||||
<ion-list>
|
<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-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-label>Export to HTML Site</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button (click)="onSelect('toggle_darkmode')">
|
<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-label>{{ isDark() ? 'To The Light!' : 'Go Dark...' }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button (click)="onSelect('logout')">
|
<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-label>Logout</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</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