Skip to content

Commit ffc5c65

Browse files
authored
Merge pull request #6911 from nextcloud-libraries/backport/6430/stable8
[stable8] refactor(useHotKey): migrate code to Typescript
2 parents 73b8659 + 21e506a commit ffc5c65

File tree

6 files changed

+182
-129
lines changed

6 files changed

+182
-129
lines changed

docs/composables/useHotKey.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ where:
5555
5656
<script>
5757
import { ref } from 'vue'
58-
import { useHotKey } from '../../src/composables/useHotKey/index.js'
58+
import { useHotKey } from '../../src/composables/useHotKey/index.ts'
5959
6060
export default {
6161
setup() {

src/components/NcAppNavigation/NcAppNavigation.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ import Vue from 'vue'
175175

176176
import { getTrapStack } from '../../utils/focusTrap.ts'
177177
import { logger } from '../../utils/logger.ts'
178-
import { useHotKey } from '../../composables/useHotKey/index.js'
178+
import { useHotKey } from '../../composables/useHotKey/index.ts'
179179
import { useIsMobile } from '../../composables/useIsMobile/index.js'
180180

181181
import NcAppNavigationList from '../NcAppNavigationList/index.js'

src/composables/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
export * from './useFormatDateTime.ts'
7-
export * from './useHotKey/index.js'
7+
export * from './useHotKey/index.ts'
88
export * from './useIsDarkTheme/index.ts'
99
export * from './useIsFullscreen/index.ts'
1010
export * from './useIsMobile/index.js'

src/composables/useHotKey/index.js

-125
This file was deleted.

src/composables/useHotKey/index.ts

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import { onKeyStroke } from '@vueuse/core'
6+
7+
const disableKeyboardShortcuts = window.OCP?.Accessibility?.disableKeyboardShortcuts?.()
8+
const isMac = /mac|ipad|iphone|darwin/i.test(navigator.userAgent)
9+
10+
export interface UseHotKeyOptions {
11+
/** Make key filter case sensitive */
12+
caseSensitive?: boolean
13+
14+
/** Prevent default behavior of key stroke */
15+
prevent?: boolean
16+
17+
/** Stop the event bubbling */
18+
stop?: boolean
19+
20+
/** Also listen for keyup event */
21+
push?: boolean
22+
23+
/**
24+
* If set then the callback is only called when the shift key is (not) pressed.
25+
* When left `undefined` a pressed shift key is ignored (callback is run with and without shift pressed).
26+
*/
27+
shift?: boolean
28+
29+
/**
30+
* Only run the callback if the control key is (not-)pressed.
31+
* Undefined will be handled the same as `false` and will only run the callback if the 'ctrl' key is NOT pressed.
32+
*/
33+
ctrl?: boolean
34+
35+
/**
36+
* If set the callback is only executed if the alt key is (not-)pressed
37+
* Undefined will be handled the same as `false` and will only run the callback if the 'alt' key is NOT pressed.
38+
*/
39+
alt?: boolean
40+
}
41+
42+
/**
43+
* Check if event target (active element) is editable (allows input from keyboard) or NcModal is open
44+
* If true, a hot key should not trigger the callback
45+
*
46+
* @todo Discuss if we should abort on another interactive elements (button, a, e.t.c)
47+
*
48+
* @param event keyboard event
49+
* @return Whether it should prevent callback
50+
*/
51+
function shouldIgnoreEvent(event: KeyboardEvent): boolean {
52+
if (!(event.target instanceof HTMLElement)
53+
|| event.target instanceof HTMLInputElement
54+
|| event.target instanceof HTMLTextAreaElement
55+
|| event.target instanceof HTMLSelectElement
56+
|| event.target.isContentEditable) {
57+
return true
58+
}
59+
/** Abort if any modal/dialog opened */
60+
return document.getElementsByClassName('modal-mask').length !== 0
61+
}
62+
63+
type KeyboardEventHandler = (event: KeyboardEvent) => void
64+
65+
/**
66+
* Implementation of the event handler.
67+
*
68+
* @param callback The callback to run
69+
* @param options hot key options
70+
*/
71+
function eventHandler(callback: KeyboardEventHandler, options: UseHotKeyOptions): KeyboardEventHandler {
72+
return (event: KeyboardEvent) => {
73+
const ctrlKeyPressed = isMac ? event.metaKey : event.ctrlKey
74+
if (ctrlKeyPressed !== Boolean(options.ctrl)) {
75+
/**
76+
* Ctrl is required and not pressed, or the opposite
77+
* As on macOS 'cmd' key is used instead of 'ctrl' key for most key combinations,
78+
* 'event.metaKey' should be checked
79+
*/
80+
return
81+
} else if (event.altKey !== Boolean(options.alt)) {
82+
// Alt is required and not pressed, or the opposite
83+
return
84+
} else if (options.shift !== undefined && event.shiftKey !== Boolean(options.shift)) {
85+
/**
86+
* Shift is required and not pressed, or the opposite
87+
* As shift key is used to type capital letters and alternate characters,
88+
* option should be explicitly defined
89+
*/
90+
return
91+
} else if (shouldIgnoreEvent(event)) {
92+
// Keyboard shortcuts are disabled, because active element assumes input
93+
return
94+
}
95+
96+
if (options.prevent) {
97+
event.preventDefault()
98+
}
99+
if (options.stop) {
100+
event.stopPropagation()
101+
}
102+
callback(event)
103+
}
104+
}
105+
106+
/**
107+
* Composable to use keyboard shortcuts in the application.
108+
* It respects the users accessibility configuration (opt-out shortcuts).
109+
*
110+
* @param keysOrFilter - keyboard key(s) to listen to, or filter function or pass `true` for listening to all keys
111+
* @param callback - callback function
112+
* @param options - composable options
113+
* @see docs/composables/usekeystroke.md
114+
*/
115+
export function useHotKey(
116+
keysOrFilter: true | string | string[] | ((e: KeyboardEvent) => boolean),
117+
callback = () => {},
118+
options: UseHotKeyOptions = {},
119+
) {
120+
if (disableKeyboardShortcuts) {
121+
// Keyboard shortcuts are disabled
122+
return () => {}
123+
}
124+
125+
/**
126+
* Validates event key to expected key
127+
* FIXME should support any languages / key codes
128+
*
129+
* @param event keyboard event
130+
* @param key expected key
131+
* @return whether it satisfies expected value or not
132+
*/
133+
const validateKeyEvent = (event: KeyboardEvent, key: string): boolean => {
134+
if (options.caseSensitive) {
135+
return event.key === key
136+
}
137+
return event.key.toLowerCase() === key.toLowerCase()
138+
}
139+
140+
/**
141+
* Filter function for the listener
142+
* see https://github.com/vueuse/vueuse/blob/v11.3.0/packages/core/onKeyStroke/index.ts#L21-L32
143+
*
144+
* @param event keyboard event
145+
* @return Whether it satisfies expected value or not
146+
*/
147+
const keyFilter = (event: KeyboardEvent): boolean => {
148+
if (typeof keysOrFilter === 'function') {
149+
return keysOrFilter(event)
150+
} else if (typeof keysOrFilter === 'string') {
151+
return validateKeyEvent(event, keysOrFilter)
152+
} else if (Array.isArray(keysOrFilter)) {
153+
return keysOrFilter.some(key => validateKeyEvent(event, key))
154+
} else {
155+
return true
156+
}
157+
}
158+
159+
const stopKeyDown = onKeyStroke(keyFilter, eventHandler(callback, options), {
160+
eventName: 'keydown',
161+
dedupe: true,
162+
passive: !options.prevent,
163+
})
164+
165+
const stopKeyUp = options.push
166+
? onKeyStroke(keyFilter, eventHandler(callback, options), {
167+
eventName: 'keyup',
168+
passive: !options.prevent,
169+
})
170+
: () => {}
171+
172+
return () => {
173+
stopKeyDown()
174+
stopKeyUp()
175+
}
176+
}

src/globals.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ declare module '*?raw' {
1919

2020
declare global {
2121
interface Window {
22-
_nc_contacts_menu_hooks: { [id: string]: ContactsMenuAction },
22+
_nc_contacts_menu_hooks: { [id: string]: ContactsMenuAction }
23+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
24+
OCP: any
2325
}
2426
}

0 commit comments

Comments
 (0)