Add some app keyboard shortcuts
This commit is contained in:
parent
2a818162d7
commit
9d28183e76
@ -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'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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`
|
||||||
|
@ -5,6 +5,10 @@ const SYMBOLS = {
|
|||||||
meta: '⌘', // ⌘
|
meta: '⌘', // ⌘
|
||||||
control: '⌃', // ⌃
|
control: '⌃', // ⌃
|
||||||
shift: '⇧', // ⇧
|
shift: '⇧', // ⇧
|
||||||
|
left: '←', // ←
|
||||||
|
right: '→', // →
|
||||||
|
up: '↑', // ↑
|
||||||
|
down: '↓', // ↓
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -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')
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user