Add some app keyboard shortcuts

This commit is contained in:
shamoon 2024-04-04 23:31:16 -07:00
parent 2a818162d7
commit 9d28183e76
9 changed files with 132 additions and 6 deletions

View File

@ -22,6 +22,7 @@ import { SettingsService } from './services/settings.service'
import { FileDropComponent } from './components/file-drop/file-drop.component' import { FileDropComponent } from './components/file-drop/file-drop.component'
import { NgxFileDropModule } from 'ngx-file-drop' import { NgxFileDropModule } from 'ngx-file-drop'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { HotKeyService } from './services/hot-key.service'
describe('AppComponent', () => { describe('AppComponent', () => {
let component: AppComponent let component: AppComponent
@ -32,6 +33,7 @@ describe('AppComponent', () => {
let toastService: ToastService let toastService: ToastService
let router: Router let router: Router
let settingsService: SettingsService let settingsService: SettingsService
let hotKeyService: HotKeyService
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -52,6 +54,7 @@ describe('AppComponent', () => {
settingsService = TestBed.inject(SettingsService) settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
router = TestBed.inject(Router) router = TestBed.inject(Router)
hotKeyService = TestBed.inject(HotKeyService)
fixture = TestBed.createComponent(AppComponent) fixture = TestBed.createComponent(AppComponent)
component = fixture.componentInstance component = fixture.componentInstance
}) })
@ -141,4 +144,15 @@ describe('AppComponent', () => {
fileStatusSubject.next(new FileStatus()) fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
}) })
it('should support hotkeys', () => {
const addShortcutSpy = jest.spyOn(hotKeyService, 'addShortcut')
const routerSpy = jest.spyOn(router, 'navigate')
component.ngOnInit()
expect(addShortcutSpy).toHaveBeenCalled()
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 'h', ctrlKey: true })
)
expect(routerSpy).toHaveBeenCalledWith(['/dashboard'])
})
}) })

View File

@ -12,6 +12,7 @@ import {
PermissionsService, PermissionsService,
PermissionType, PermissionType,
} from './services/permissions.service' } from './services/permissions.service'
import { HotKeyService } from './services/hot-key.service'
@Component({ @Component({
selector: 'pngx-root', selector: 'pngx-root',
@ -31,7 +32,8 @@ export class AppComponent implements OnInit, OnDestroy {
private tasksService: TasksService, private tasksService: TasksService,
public tourService: TourService, public tourService: TourService,
private renderer: Renderer2, private renderer: Renderer2,
private permissionsService: PermissionsService private permissionsService: PermissionsService,
private hotKeyService: HotKeyService
) { ) {
this.settings.updateAppearanceSettings() this.settings.updateAppearanceSettings()
} }
@ -123,6 +125,17 @@ export class AppComponent implements OnInit, OnDestroy {
} }
}) })
this.hotKeyService
.addShortcut({ keys: 'control.h', description: $localize`Dashboard` })
.subscribe(() => {
this.router.navigate(['/dashboard'])
})
this.hotKeyService
.addShortcut({ keys: 'control.d', description: $localize`Documents` })
.subscribe(() => {
this.router.navigate(['/documents'])
})
const prevBtnTitle = $localize`Prev` const prevBtnTitle = $localize`Prev`
const nextBtnTitle = $localize`Next` const nextBtnTitle = $localize`Next`
const endBtnTitle = $localize`End` const endBtnTitle = $localize`End`

View File

@ -5,6 +5,10 @@ const SYMBOLS = {
meta: '⌘', // ⌘ meta: '⌘', // ⌘
control: '⌃', // ⌃ control: '⌃', // ⌃
shift: '⇧', // ⇧ shift: '⇧', // ⇧
left: '←', // ←
right: '→', // →
up: '↑', // ↑
down: '↓', // ↓
} }
@Component({ @Component({

View File

@ -12,8 +12,12 @@ import {
} from '@angular/core/testing' } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { Router, ActivatedRoute, convertToParamMap } from '@angular/router' import {
import { RouterTestingModule } from '@angular/router/testing' Router,
ActivatedRoute,
convertToParamMap,
RouterModule,
} from '@angular/router'
import { import {
NgbModal, NgbModal,
NgbModule, NgbModule,
@ -253,7 +257,7 @@ describe('DocumentDetailComponent', () => {
DatePipe, DatePipe,
], ],
imports: [ imports: [
RouterTestingModule.withRoutes(routes), RouterModule.forRoot(routes),
HttpClientTestingModule, HttpClientTestingModule,
NgbModule, NgbModule,
NgSelectModule, NgSelectModule,
@ -1126,6 +1130,35 @@ describe('DocumentDetailComponent', () => {
req.flush(true) req.flush(true)
}) })
it('should support keyboard shortcuts', () => {
initNormally()
jest.spyOn(component, 'hasNext').mockReturnValue(true)
const nextSpy = jest.spyOn(component, 'nextDoc')
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 'arrowright', ctrlKey: true })
)
expect(nextSpy).toHaveBeenCalled()
jest.spyOn(component, 'hasPrevious').mockReturnValue(true)
const prevSpy = jest.spyOn(component, 'previousDoc')
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 'arrowleft', ctrlKey: true })
)
expect(prevSpy).toHaveBeenCalled()
jest.spyOn(openDocumentsService, 'isDirty').mockReturnValue(true)
const saveSpy = jest.spyOn(component, 'save')
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
)
expect(saveSpy).toHaveBeenCalled()
const closeSpy = jest.spyOn(component, 'close')
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' }))
expect(closeSpy).toHaveBeenCalled()
})
function initNormally() { function initNormally() {
jest jest
.spyOn(activatedRoute, 'paramMap', 'get') .spyOn(activatedRoute, 'paramMap', 'get')

View File

@ -69,6 +69,7 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
import { PDFDocumentProxy } from '../common/pdf-viewer/typings' import { PDFDocumentProxy } from '../common/pdf-viewer/typings'
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { HotKeyService } from 'src/app/services/hot-key.service'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@ -201,7 +202,8 @@ export class DocumentDetailComponent
private permissionsService: PermissionsService, private permissionsService: PermissionsService,
private userService: UserService, private userService: UserService,
private customFieldsService: CustomFieldsService, private customFieldsService: CustomFieldsService,
private http: HttpClient private http: HttpClient,
private hotKeyService: HotKeyService
) { ) {
super() super()
} }
@ -455,6 +457,40 @@ export class DocumentDetailComponent
}) })
} }
}) })
this.hotKeyService
.addShortcut({
keys: 'control.arrowright',
description: $localize`Next document`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.hasNext()) this.nextDoc()
})
this.hotKeyService
.addShortcut({
keys: 'control.arrowleft',
description: $localize`Previous document`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.hasPrevious()) this.previousDoc()
})
this.hotKeyService
.addShortcut({ keys: 'escape', description: $localize`Close document` })
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.close()
})
this.hotKeyService
.addShortcut({ keys: 'control.s', description: $localize`Save document` })
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.openDocumentService.isDirty(this.document)) this.save()
})
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@ -68,4 +68,20 @@ describe('HotKeyService', () => {
document.dispatchEvent(event) document.dispatchEvent(event)
expect(modalSpy).not.toHaveBeenCalled() expect(modalSpy).not.toHaveBeenCalled()
}) })
it('should dismiss all modals on escape but not fire event', () => {
const callback = jest.fn()
service
.addShortcut({ keys: 'escape', description: 'Escape' })
.subscribe(callback)
const modalSpy = jest.spyOn(modalService, 'open')
document.dispatchEvent(
new KeyboardEvent('keydown', { key: '?', shiftKey: true })
)
expect(modalSpy).toHaveBeenCalled()
const dismissAllSpy = jest.spyOn(modalService, 'dismissAll')
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
expect(dismissAllSpy).toHaveBeenCalled()
expect(callback).not.toHaveBeenCalled()
})
}) })

View File

@ -53,8 +53,13 @@ export class HotKeyService {
return return
} }
e.preventDefault()
this.modalService.dismissAll() this.modalService.dismissAll()
if (e.key === 'Escape' && this.modalService.hasOpenModals()) {
// If there is a modal open, just dismiss
return
}
e.preventDefault()
observer.next(e) observer.next(e)
} }

View File

@ -135,6 +135,7 @@ describe('OpenDocumentsService', () => {
expect(openDocumentsService.hasDirty()).toBeFalsy() expect(openDocumentsService.hasDirty()).toBeFalsy()
openDocumentsService.setDirty(documents[0], true) openDocumentsService.setDirty(documents[0], true)
expect(openDocumentsService.hasDirty()).toBeTruthy() expect(openDocumentsService.hasDirty()).toBeTruthy()
expect(openDocumentsService.isDirty(documents[0])).toBeTruthy()
let openModal let openModal
modalService.activeInstances.subscribe((instances) => { modalService.activeInstances.subscribe((instances) => {
openModal = instances[0] openModal = instances[0]

View File

@ -90,6 +90,10 @@ export class OpenDocumentsService {
return this.dirtyDocuments.size > 0 return this.dirtyDocuments.size > 0
} }
isDirty(doc: Document): boolean {
return this.dirtyDocuments.has(doc.id)
}
closeDocument(doc: Document): Observable<boolean> { closeDocument(doc: Document): Observable<boolean> {
let index = this.openDocuments.findIndex((d) => d.id == doc.id) let index = this.openDocuments.findIndex((d) => d.id == doc.id)
if (index == -1) return of(true) if (index == -1) return of(true)