diff --git a/docs/composables.md b/docs/composables.md new file mode 100644 index 0000000000..07980cfcdc --- /dev/null +++ b/docs/composables.md @@ -0,0 +1,27 @@ + + +### Registration + +To use any composable, import and use it according to documentation or Vue guidelines, for example: + +```js static +import { useIsMobile } from '@nextcloud/vue/dist/composables/useIsMobile.js' + +export default { + setup() { + return { + isMobile: useIsMobile(), + } + }, +} +``` +or in ` + + +``` diff --git a/src/composables/index.js b/src/composables/index.js index 4ef76388f5..016a4d0496 100644 --- a/src/composables/index.js +++ b/src/composables/index.js @@ -6,3 +6,4 @@ export * from './useIsFullscreen/index.js' export * from './useIsMobile/index.js' export * from './useFormatDateTime.ts' +export * from './useHotKey/index.js' diff --git a/src/composables/useHotKey/index.js b/src/composables/useHotKey/index.js new file mode 100644 index 0000000000..0f78ee32d3 --- /dev/null +++ b/src/composables/useHotKey/index.js @@ -0,0 +1,81 @@ +/** + * 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?.() + +/** + * 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 {KeyboardEvent} event keyboard event + * @return {boolean} whether it should prevent callback + */ +function shouldIgnoreEvent(event) { + if (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 +} + +const eventHandler = (callback, options) => (event) => { + if (!!options.ctrl !== event.ctrlKey) { + // Ctrl is required and not pressed, or the opposite + return + } else if (!!options.alt !== event.altKey) { + // Alt is required and not pressed, or the opposite + return + } else if (!!options.shift !== event.shiftKey) { + // Shift is required and not pressed, or the opposite + 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) +} + +/** + * @param {string} key - keyboard key or keys to listen to + * @param {Function} callback - callback function + * @param {object} options - composable options + * @see docs/composables/usekeystroke.md + */ +export function useHotKey(key, callback = () => {}, options = {}) { + if (disableKeyboardShortcuts) { + // Keyboard shortcuts are disabled + return () => {} + } + + const stopKeyDown = onKeyStroke(key, eventHandler(callback, options), { + eventName: 'keydown', + dedupe: true, + passive: !options.prevent, + }) + + const stopKeyUp = options.push + ? onKeyStroke(key, eventHandler(callback, options), { + eventName: 'keyup', + passive: !options.prevent, + }) + : () => {} + + return () => { + stopKeyDown() + stopKeyUp() + } +} diff --git a/styleguide.config.js b/styleguide.config.js index 728cc409cd..4cdd215928 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -139,6 +139,17 @@ module.exports = async () => { }, ], }, + { + name: 'Composables', + content: 'docs/composables.md', + sectionDepth: 1, + sections: [ + { + name: 'useHotKey', + content: 'docs/composables/useHotKey.md', + }, + ], + }, { name: 'Components', content: 'docs/components.md',