From 28d6986eead63d82f6c709d193c627f9288a97b7 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 13 Oct 2020 07:54:14 -0500 Subject: [PATCH] Add support for full-text search (#7) --- angular.json | 3 + package-lock.json | 5 ++ package.json | 1 + src/app/app.component.ts | 23 ++++- src/app/components/components.module.ts | 4 + .../option-picker.component.html | 10 ++- .../option-picker/option-picker.component.ts | 6 ++ .../components/search/Search.component.html | 36 ++++++++ .../components/search/Search.component.scss | 54 ++++++++++++ src/app/components/search/Search.component.ts | 85 +++++++++++++++++++ src/app/home/home.page.html | 7 +- src/global.scss | 15 ++++ 12 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/app/components/search/Search.component.html create mode 100644 src/app/components/search/Search.component.scss create mode 100644 src/app/components/search/Search.component.ts diff --git a/angular.json b/angular.json index ce52da4..fe4034c 100644 --- a/angular.json +++ b/angular.json @@ -37,6 +37,9 @@ } ], "styles": [ + { + "input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css" + }, { "input": "src/theme/variables.scss" }, diff --git a/package-lock.json b/package-lock.json index ef4d533..915c0ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1847,6 +1847,11 @@ } } }, + "@fortawesome/fontawesome-free": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz", + "integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ==" + }, "@ionic-native/core": { "version": "5.28.0", "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.28.0.tgz", diff --git a/package.json b/package.json index f15acce..a646a21 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser-dynamic": "~10.1.5", "@angular/router": "~10.1.5", "@circlon/angular-tree-component": "^10.0.0", + "@fortawesome/fontawesome-free": "^5.15.1", "@ionic-native/core": "^5.0.0", "@ionic-native/splash-screen": "^5.0.0", "@ionic-native/status-bar": "^5.0.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 81ccd5f..5a6095d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; +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'; @@ -11,6 +11,7 @@ import {OptionPickerComponent} from './components/option-picker/option-picker.co 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"; @Component({ selector: 'app-root', @@ -67,6 +68,7 @@ export class AppComponent implements OnInit { public darkMode = false; protected loader?: any; + protected hasSearchOpen = false; protected initialized$: BehaviorSubject = new BehaviorSubject(false); @@ -115,6 +117,7 @@ export class AppComponent implements OnInit { componentProps: { toggleDark: () => this.toggleDark(), isDark: () => this.isDark(), + showSearch: () => this.handleKeyboardEvent(), } }).then(popover => popover.present()); } @@ -134,6 +137,24 @@ export class AppComponent implements OnInit { } } + @HostListener('document:keyup.control./', ['$event']) + async handleKeyboardEvent() { + if ( this.hasSearchOpen ) { + return; + } + + console.log('firing search'); + 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 ) { diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 951c9bf..b79e22a 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -24,6 +24,7 @@ import {DatetimeEditorComponent} from './editor/database/editors/datetime/dateti import {DatetimeRendererComponent} from './editor/database/renderers/datetime-renderer.component'; import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component'; import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component'; +import {SearchComponent} from './search/Search.component'; @NgModule({ declarations: [ @@ -47,6 +48,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend DatetimeRendererComponent, CurrencyRendererComponent, BooleanRendererComponent, + SearchComponent, ], imports: [ CommonModule, @@ -76,6 +78,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend DatetimeRendererComponent, CurrencyRendererComponent, BooleanRendererComponent, + SearchComponent, ], exports: [ HostComponent, @@ -98,6 +101,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend DatetimeRendererComponent, CurrencyRendererComponent, BooleanRendererComponent, + SearchComponent, ] }) export class ComponentsModule {} diff --git a/src/app/components/option-picker/option-picker.component.html b/src/app/components/option-picker/option-picker.component.html index 3857248..a6dea0b 100644 --- a/src/app/components/option-picker/option-picker.component.html +++ b/src/app/components/option-picker/option-picker.component.html @@ -1,14 +1,18 @@ + + + Search Everywhere + - + Export to HTML Site - + {{ isDark() ? 'To The Light!' : 'Go Dark...' }} - + Logout diff --git a/src/app/components/option-picker/option-picker.component.ts b/src/app/components/option-picker/option-picker.component.ts index 701b6a2..0a072d4 100644 --- a/src/app/components/option-picker/option-picker.component.ts +++ b/src/app/components/option-picker/option-picker.component.ts @@ -1,6 +1,7 @@ import {Component, Input, OnInit} from '@angular/core'; import {Router} from '@angular/router'; import {ApiService} from '../../service/api.service'; +import {PopoverController} from '@ionic/angular'; @Component({ selector: 'app-option-picker', @@ -10,10 +11,12 @@ import {ApiService} from '../../service/api.service'; export class OptionPickerComponent implements OnInit { @Input() toggleDark: () => void; @Input() isDark: () => boolean; + @Input() showSearch: () => void | Promise; constructor( protected api: ApiService, protected router: Router, + protected popover: PopoverController, ) { } ngOnInit() {} @@ -25,6 +28,9 @@ export class OptionPickerComponent implements OnInit { window.location.href = '/auth/logout'; } else if ( key === 'toggle_darkmode' ) { this.toggleDark(); + } else if ( key === 'search_everywhere' ) { + this.showSearch(); + this.popover.dismiss(); } } } diff --git a/src/app/components/search/Search.component.html b/src/app/components/search/Search.component.html new file mode 100644 index 0000000..e8a667d --- /dev/null +++ b/src/app/components/search/Search.component.html @@ -0,0 +1,36 @@ +
+ + + +
+ + + + +
{{ result.short_title }}
+
+
+ +
{{ result.associated.title }}
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/components/search/Search.component.scss b/src/app/components/search/Search.component.scss new file mode 100644 index 0000000..7b8c7e4 --- /dev/null +++ b/src/app/components/search/Search.component.scss @@ -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); + } + } +} \ No newline at end of file diff --git a/src/app/components/search/Search.component.ts b/src/app/components/search/Search.component.ts new file mode 100644 index 0000000..0cc6dae --- /dev/null +++ b/src/app/components/search/Search.component.ts @@ -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 = new BehaviorSubject([]); + + 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 { + 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(); + } + } + } +} diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index c62fc53..0a232f0 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -9,8 +9,11 @@ - + Hi, there! Select or create a page to get started. - \ No newline at end of file + + (You can press Ctrl + / to search everywhere.) + + diff --git a/src/global.scss b/src/global.scss index 9713d2e..395b1c2 100644 --- a/src/global.scss +++ b/src/global.scss @@ -28,6 +28,21 @@ @import '~@circlon/angular-tree-component/css/angular-tree-component.css'; @import "~ag-grid-community/dist/styles/ag-grid.css"; @import "~ag-grid-community/dist/styles/ag-theme-balham.css"; +@import "~@fortawesome/fontawesome-free/css/all.min.css"; + +:root { + --noded-background-note: #3A86FF; + --noded-color-note: white; + --noded-background-note-hover: #66a1ff; + + --noded-background-db: #8338EC; + --noded-color-db: white; + --noded-background-db-hover: #a873f2; + + --noded-background-node: #FB5607; + --noded-color-node: white; + --noded-background-node-hover: #fc864f; +} div.picker-wrapper { border: 2px solid lightgrey !important;