Skip to content

Commit

Permalink
refactor(useHotKey): migrate code to Typescript
Browse files Browse the repository at this point in the history
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Jan 24, 2025
1 parent 8847493 commit 4d2b294
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 126 deletions.
2 changes: 1 addition & 1 deletion src/composables/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
export * from './useIsFullscreen/index.js'
export * from './useIsMobile/index.js'
export * from './useFormatDateTime.ts'
export * from './useHotKey/index.js'
export * from './useHotKey/index.ts'
export * from './useIsDarkTheme/index.ts'
125 changes: 0 additions & 125 deletions src/composables/useHotKey/index.js

This file was deleted.

176 changes: 176 additions & 0 deletions src/composables/useHotKey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { onKeyStroke } from '@vueuse/core'

const disableKeyboardShortcuts = window.OCP?.Accessibility?.disableKeyboardShortcuts?.()
const isMac = /mac|ipad|iphone|darwin/i.test(navigator.userAgent)

export interface UseHotKeyOptions {
/** Make key filter case sensitive */
caseSensitive?: boolean

/** Prevent default behavior of key stroke */
prevent?: boolean

/** Stop the event bubbling */
stop?: boolean

/** Also listen for keyup event */
push?: boolean

/**
* If set then the callback is only called when the shift key is (not) pressed.
* When left `undefined` a pressed shift key is ignored (callback is run with and without shift pressed).
*/
shift?: boolean

/**
* Only run the callback if the control key is (not-)pressed.
* Undefined will be handled the same as `false` and will only run the callback if the 'ctrl' key is NOT pressed.
*/
ctrl?: boolean

/**
* If set the callback is only executed if the alt key is (not-)pressed
* Undefined will be handled the same as `false` and will only run the callback if the 'alt' key is NOT pressed.
*/
alt?: boolean
}

/**
* Check if event target (active element) is editable (allows input from keyboard) or NcModal is open
* If true, a hot key should not trigger the callback
*
* @todo Discuss if we should abort on another interactive elements (button, a, e.t.c)
*
* @param event keyboard event
* @return Whether it should prevent callback
*/
function shouldIgnoreEvent(event: KeyboardEvent): boolean {
if (!(event.target instanceof HTMLElement)
|| event.target instanceof HTMLInputElement
|| event.target instanceof HTMLTextAreaElement
|| event.target instanceof HTMLSelectElement
|| event.target.isContentEditable) {
return true
}
/** Abort if any modal/dialog opened */
return document.getElementsByClassName('modal-mask').length !== 0
}

type KeyboardEventHandler = (event: KeyboardEvent) => void

/**
* Implementation of the event handler.
*
* @param callback The callback to run
* @param options hot key options
*/
function eventHandler(callback: KeyboardEventHandler, options: UseHotKeyOptions): KeyboardEventHandler {
return (event: KeyboardEvent) => {
const ctrlKeyPressed = isMac ? event.metaKey : event.ctrlKey
if (ctrlKeyPressed !== Boolean(options.ctrl)) {
/**
* Ctrl is required and not pressed, or the opposite
* As on macOS 'cmd' key is used instead of 'ctrl' key for most key combinations,
* 'event.metaKey' should be checked
*/
return
} else if (event.altKey !== Boolean(options.alt)) {
// Alt is required and not pressed, or the opposite
return
} else if (options.shift !== undefined && event.shiftKey !== Boolean(options.shift)) {
/**
* Shift is required and not pressed, or the opposite
* As shift key is used to type capital letters and alternate characters,
* option should be explicitly defined
*/
return
} else if (shouldIgnoreEvent(event)) {
// Keyboard shortcuts are disabled, because active element assumes input
return
}

if (options.prevent) {
event.preventDefault()
}
if (options.stop) {
event.stopPropagation()
}
callback(event)
}
}

/**
* Composable to use keyboard shortcuts in the application.
* It respects the users accessibility configuration (opt-out shortcuts).
*
* @param keysOrFilter - keyboard key(s) to listen to, or filter function or pass `true` for listening to all keys
* @param callback - callback function
* @param options - composable options
* @see docs/composables/usekeystroke.md
*/
export function useHotKey(
keysOrFilter: true | string | string[] | ((e: KeyboardEvent) => boolean),
callback = () => {},
options: UseHotKeyOptions = {},
) {
if (disableKeyboardShortcuts) {
// Keyboard shortcuts are disabled
return () => {}
}

/**
* Validates event key to expected key
* FIXME should support any languages / key codes
*
* @param event keyboard event
* @param key expected key
* @return whether it satisfies expected value or not
*/
const validateKeyEvent = (event: KeyboardEvent, key: string): boolean => {
if (options.caseSensitive) {
return event.key === key
}
return event.key.toLowerCase() === key.toLowerCase()
}

/**
* Filter function for the listener
* see https://github.com/vueuse/vueuse/blob/v11.3.0/packages/core/onKeyStroke/index.ts#L21-L32
*
* @param event keyboard event
* @return Whether it satisfies expected value or not
*/
const keyFilter = (event: KeyboardEvent): boolean => {
if (typeof keysOrFilter === 'function') {
return keysOrFilter(event)
} else if (typeof keysOrFilter === 'string') {
return validateKeyEvent(event, keysOrFilter)
} else if (Array.isArray(keysOrFilter)) {
return keysOrFilter.some(key => validateKeyEvent(event, key))
} else {
return true
}
}

const stopKeyDown = onKeyStroke(keyFilter, eventHandler(callback, options), {
eventName: 'keydown',
dedupe: true,
passive: !options.prevent,
})

const stopKeyUp = options.push
? onKeyStroke(keyFilter, eventHandler(callback, options), {
eventName: 'keyup',
passive: !options.prevent,
})
: () => {}

return () => {
stopKeyDown()
stopKeyUp()
}
}
8 changes: 8 additions & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ declare module '*?raw' {
const content: string
export default content
}

declare global {
interface Window {
OCP: any

Check failure on line 20 in src/globals.d.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Unexpected any. Specify a different type
}
}

export {}

0 comments on commit 4d2b294

Please sign in to comment.