From 5bb577ab20a37c75add50172db3ce259bbcf52fc Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:30:04 -0700 Subject: [PATCH] Hotkey service --- .../global-search.component.spec.ts | 4 +- .../global-search/global-search.component.ts | 23 ++++++---- .../src/app/services/hot-key.service.spec.ts | 41 +++++++++++++++++ src-ui/src/app/services/hot-key.service.ts | 45 +++++++++++++++++++ 4 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 src-ui/src/app/services/hot-key.service.spec.ts create mode 100644 src-ui/src/app/services/hot-key.service.ts diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts index fed135818..0755d9900 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts @@ -150,12 +150,12 @@ describe('GlobalSearchComponent', () => { it('should handle keyboard nav', () => { const focusSpy = jest.spyOn(component.searchInput.nativeElement, 'focus') - component.handleKeyboardEvent( + document.dispatchEvent( new KeyboardEvent('keydown', { key: 'k', ctrlKey: true }) ) expect(focusSpy).toHaveBeenCalled() // coverage - component.handleKeyboardEvent( + document.dispatchEvent( new KeyboardEvent('keydown', { key: 'k', metaKey: true }) ) diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts index 9ee32c906..f7d87d8e8 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts @@ -5,6 +5,7 @@ import { ViewChildren, QueryList, HostListener, + OnInit, } from '@angular/core' import { Router } from '@angular/router' import { NgbDropdown, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' @@ -39,13 +40,14 @@ import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component' import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component' import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component' +import { HotKeyService } from 'src/app/services/hot-key.service' @Component({ selector: 'pngx-global-search', templateUrl: './global-search.component.html', styleUrl: './global-search.component.scss', }) -export class GlobalSearchComponent { +export class GlobalSearchComponent implements OnInit { public DataType = DataType public query: string public queryDebounce: Subject @@ -60,13 +62,6 @@ export class GlobalSearchComponent { @ViewChildren('primaryButton') primaryButtons: QueryList @ViewChildren('secondaryButton') secondaryButtons: QueryList - @HostListener('document:keydown', ['$event']) - handleKeyboardEvent(event: KeyboardEvent) { - if (event.key === 'k' && (event.ctrlKey || event.metaKey)) { - this.searchInput.nativeElement.focus() - } - } - constructor( private searchService: SearchService, private router: Router, @@ -74,7 +69,8 @@ export class GlobalSearchComponent { private documentService: DocumentService, private documentListViewService: DocumentListViewService, private permissionsService: PermissionsService, - private toastService: ToastService + private toastService: ToastService, + private hotkeyService: HotKeyService ) { this.queryDebounce = new Subject() @@ -90,6 +86,15 @@ export class GlobalSearchComponent { }) } + ngOnInit() { + this.hotkeyService.addShortcut({ keys: 'meta.k' }).subscribe(() => { + this.searchInput.nativeElement.focus() + }) + this.hotkeyService.addShortcut({ keys: 'control.k' }).subscribe(() => { + this.searchInput.nativeElement.focus() + }) + } + private search(query: string) { this.loading = true this.searchService.globalSearch(query).subscribe((results) => { diff --git a/src-ui/src/app/services/hot-key.service.spec.ts b/src-ui/src/app/services/hot-key.service.spec.ts new file mode 100644 index 000000000..0162d860e --- /dev/null +++ b/src-ui/src/app/services/hot-key.service.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing' +import { EventManager } from '@angular/platform-browser' +import { DOCUMENT } from '@angular/common' + +import { HotKeyService } from './hot-key.service' + +describe('HotKeyService', () => { + let service: HotKeyService + let eventManager: EventManager + let document: Document + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [HotKeyService, EventManager], + }) + service = TestBed.inject(HotKeyService) + eventManager = TestBed.inject(EventManager) + document = TestBed.inject(DOCUMENT) + }) + + it('should support adding a shortcut', () => { + const callback = jest.fn() + const addEventListenerSpy = jest.spyOn(eventManager, 'addEventListener') + + const observable = service + .addShortcut({ keys: 'control.a' }) + .subscribe(() => { + callback() + }) + + expect(addEventListenerSpy).toHaveBeenCalled() + + document.dispatchEvent( + new KeyboardEvent('keydown', { key: 'a', ctrlKey: true }) + ) + expect(callback).toHaveBeenCalled() + + //coverage + observable.unsubscribe() + }) +}) diff --git a/src-ui/src/app/services/hot-key.service.ts b/src-ui/src/app/services/hot-key.service.ts new file mode 100644 index 000000000..a08556b43 --- /dev/null +++ b/src-ui/src/app/services/hot-key.service.ts @@ -0,0 +1,45 @@ +import { DOCUMENT } from '@angular/common' +import { Inject, Injectable } from '@angular/core' +import { EventManager } from '@angular/platform-browser' +import { Observable } from 'rxjs' + +export interface ShortcutOptions { + element?: any + keys: string +} + +@Injectable({ + providedIn: 'root', +}) +export class HotKeyService { + defaults: Partial = { + element: this.document, + } + + constructor( + private eventManager: EventManager, + @Inject(DOCUMENT) private document: Document + ) {} + + addShortcut(options: ShortcutOptions) { + const merged = { ...this.defaults, ...options } + const event = `keydown.${merged.keys}` + + return new Observable((observer) => { + const handler = (e) => { + e.preventDefault() + observer.next(e) + } + + const dispose = this.eventManager.addEventListener( + merged.element, + event, + handler + ) + + return () => { + dispose() + } + }) + } +}