feat: keyboard shortcuts for primary actions (#2030)
This commit is contained in:
203
packages/ui-services/src/Keyboard/KeyboardService.ts
Normal file
203
packages/ui-services/src/Keyboard/KeyboardService.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Environment, Platform } from '@standardnotes/snjs'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
|
||||
import { KeyboardCommand } from './KeyboardCommands'
|
||||
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
|
||||
import { KeyboardModifier } from './KeyboardModifier'
|
||||
import { KeyboardCommandHandler } from './KeyboardCommandHandler'
|
||||
import { KeyboardShortcut, PlatformedKeyboardShortcut } from './KeyboardShortcut'
|
||||
import { getKeyboardShortcuts } from './getKeyboardShortcuts'
|
||||
|
||||
export class KeyboardService {
|
||||
readonly activeModifiers = new Set<KeyboardModifier>()
|
||||
private commandHandlers: KeyboardCommandHandler[] = []
|
||||
private commandMap = new Map<KeyboardCommand, KeyboardShortcut>()
|
||||
|
||||
constructor(private platform: Platform, environment: Environment) {
|
||||
window.addEventListener('keydown', this.handleKeyDown)
|
||||
window.addEventListener('keyup', this.handleKeyUp)
|
||||
window.addEventListener('blur', this.handleWindowBlur)
|
||||
|
||||
const shortcuts = getKeyboardShortcuts(platform, environment)
|
||||
for (const shortcut of shortcuts) {
|
||||
this.registerShortcut(shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
get isMac() {
|
||||
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.commandHandlers.length = 0
|
||||
window.removeEventListener('keydown', this.handleKeyDown)
|
||||
window.removeEventListener('keyup', this.handleKeyUp)
|
||||
window.removeEventListener('blur', this.handleWindowBlur)
|
||||
;(this.handleKeyDown as unknown) = undefined
|
||||
;(this.handleKeyUp as unknown) = undefined
|
||||
;(this.handleWindowBlur as unknown) = undefined
|
||||
}
|
||||
|
||||
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (modifier) {
|
||||
case KeyboardModifier.Meta: {
|
||||
if (this.isMac) {
|
||||
this.activeModifiers.add(modifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
case KeyboardModifier.Ctrl: {
|
||||
if (!this.isMac) {
|
||||
this.activeModifiers.add(modifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
this.activeModifiers.add(modifier)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
if (!modifier) {
|
||||
return
|
||||
}
|
||||
|
||||
this.activeModifiers.delete(modifier)
|
||||
}
|
||||
|
||||
public cancelAllKeyboardModifiers = (): void => {
|
||||
this.activeModifiers.clear()
|
||||
}
|
||||
|
||||
public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.addActiveModifier(modifier)
|
||||
}
|
||||
|
||||
public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.removeActiveModifier(modifier)
|
||||
}
|
||||
|
||||
private handleKeyDown = (event: KeyboardEvent): void => {
|
||||
this.updateAllModifiersFromEvent(event)
|
||||
|
||||
this.handleKeyboardEvent(event, KeyboardKeyEvent.Down)
|
||||
}
|
||||
|
||||
private handleKeyUp = (event: KeyboardEvent): void => {
|
||||
this.updateAllModifiersFromEvent(event)
|
||||
|
||||
this.handleKeyboardEvent(event, KeyboardKeyEvent.Up)
|
||||
}
|
||||
|
||||
private updateAllModifiersFromEvent(event: KeyboardEvent): void {
|
||||
for (const modifier of Object.values(KeyboardModifier)) {
|
||||
if (event.getModifierState(modifier)) {
|
||||
this.addActiveModifier(modifier)
|
||||
} else {
|
||||
this.removeActiveModifier(modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowBlur = (): void => {
|
||||
for (const modifier of this.activeModifiers) {
|
||||
this.activeModifiers.delete(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
for (const command of this.commandMap.keys()) {
|
||||
const shortcut = this.commandMap.get(command)
|
||||
if (!shortcut) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (eventMatchesKeyAndModifiers(event, shortcut)) {
|
||||
if (shortcut.preventDefault) {
|
||||
event.preventDefault()
|
||||
}
|
||||
this.handleCommand(command, event, keyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleCommand(command: KeyboardCommand, event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
for (const observer of this.commandHandlers) {
|
||||
if (observer.command !== command) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.element && event.target !== observer.element) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.elements && !observer.elements.includes(target)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notElement && observer.notElement === event.target) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notElementIds && observer.notElementIds.includes(target.id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (observer.notTags && observer.notTags.includes(target.tagName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp
|
||||
if (callback) {
|
||||
const exclusive = callback(event)
|
||||
if (exclusive) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerShortcut(shortcut: KeyboardShortcut): void {
|
||||
this.commandMap.set(shortcut.command, shortcut)
|
||||
}
|
||||
|
||||
addCommandHandler(observer: KeyboardCommandHandler): () => void {
|
||||
this.commandHandlers.push(observer)
|
||||
|
||||
const thislessObservers = this.commandHandlers
|
||||
return () => {
|
||||
observer.onKeyDown = undefined
|
||||
observer.onKeyDown = undefined
|
||||
removeFromArray(thislessObservers, observer)
|
||||
}
|
||||
}
|
||||
|
||||
addCommandHandlers(handlers: KeyboardCommandHandler[]): () => void {
|
||||
const disposers = handlers.map((handler) => this.addCommandHandler(handler))
|
||||
return () => {
|
||||
for (const disposer of disposers) {
|
||||
disposer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keyboardShortcutForCommand(command: KeyboardCommand): PlatformedKeyboardShortcut | undefined {
|
||||
const shortcut = this.commandMap.get(command)
|
||||
if (!shortcut) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
platform: this.platform,
|
||||
...shortcut,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user