From 77b5589ef7ed30797df8101134c07f55c797cc20 Mon Sep 17 00:00:00 2001 From: xhon-pelushi Date: Thu, 13 Aug 2026 23:41:45 -0400 Subject: [PATCH] fix(editor): use useHotKey for editor shortcuts Signed-off-by: SebastianKrupinski --- src/mixins/EditorMixin.js | 39 ++++++---- src/views/EditFull.vue | 14 ---- src/views/EditSimple.vue | 8 -- .../unit/mixins/EditorMixin.test.js | 75 +++++++++++++++++-- 4 files changed, 94 insertions(+), 42 deletions(-) diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index 636d787636..63f6556b66 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -6,6 +6,7 @@ import { showError, showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' +import { useHotKey } from '@nextcloud/vue/composables/useHotKey' import { mapState, mapStores } from 'pinia' import { getRFCProperties } from '@/models/rfcProps.js' import { containsRoomUrl } from '@/services/talkService.ts' @@ -59,6 +60,8 @@ export default { isRecurrenceException: false, // Whether or not the Talk modal is open isTalkModalOpen: false, + // Cleanup functions for hotkeys + hotKeysRegister: [], } }, computed: { @@ -518,6 +521,19 @@ export default { } }, + mounted() { + this.hotKeysRegister = [ + useHotKey('Escape', () => this.keyboardCloseEditor(), { allowInModal: true }), + useHotKey('Enter', () => this.keyboardSaveEvent(), { ctrl: true, allowInModal: true }), + useHotKey('Delete', () => this.keyboardDeleteEvent(), { ctrl: true, allowInModal: true }), + useHotKey('d', () => this.keyboardDuplicateEvent(), { ctrl: true, prevent: true, allowInModal: true }), + ] + }, + + beforeUnmount() { + this.hotKeysRegister.forEach((stop) => stop()) + }, + methods: { /** * Opens the Talk modal for selecting or creating a Talk room @@ -642,27 +658,22 @@ export default { this.closeEditor() } }, - keyboardCloseEditor(event) { - if (event.key === 'Escape') { - this.cancel(false) - } + keyboardCloseEditor() { + this.cancel(false) }, - keyboardSaveEvent(event) { - if (event.key === 'Enter' && event.ctrlKey === true && !this.isReadOnly && !this.canCreateRecurrenceException) { + keyboardSaveEvent() { + if (!this.isReadOnly && !this.canCreateRecurrenceException) { this.saveAndLeave(false) } }, - keyboardDeleteEvent(event) { - if (event.key === 'Delete' && event.ctrlKey === true && this.canDelete && !this.canCreateRecurrenceException) { + keyboardDeleteEvent() { + if (this.canDelete && !this.canCreateRecurrenceException) { this.deleteAndLeave(false) } }, - keyboardDuplicateEvent(event) { - if (event.key === 'd' && event.ctrlKey === true) { - event.preventDefault() - if (!this.isNew && this.canDuplicate) { - this.duplicateEvent() - } + keyboardDuplicateEvent() { + if (!this.isNew && this.canDuplicate) { + this.duplicateEvent() } }, /** diff --git a/src/views/EditFull.vue b/src/views/EditFull.vue index 8684c47df2..9f3e3bc132 100644 --- a/src/views/EditFull.vue +++ b/src/views/EditFull.vue @@ -537,20 +537,6 @@ export default { }, }, - mounted() { - window.addEventListener('keydown', this.keyboardCloseEditor) - window.addEventListener('keydown', this.keyboardSaveEvent) - window.addEventListener('keydown', this.keyboardDeleteEvent) - window.addEventListener('keydown', this.keyboardDuplicateEvent) - }, - - beforeUnmount() { - window.removeEventListener('keydown', this.keyboardCloseEditor) - window.removeEventListener('keydown', this.keyboardSaveEvent) - window.removeEventListener('keydown', this.keyboardDeleteEvent) - window.removeEventListener('keydown', this.keyboardDuplicateEvent) - }, - methods: { updateLocation(location) { this.calendarObjectInstanceStore.changeLocation({ diff --git a/src/views/EditSimple.vue b/src/views/EditSimple.vue index 8e62ddb395..c3811c8e5d 100644 --- a/src/views/EditSimple.vue +++ b/src/views/EditSimple.vue @@ -518,10 +518,6 @@ export default { this.isLoading = false } this.boundaryElement = document.querySelector('.calendar-wrapper') - window.addEventListener('keydown', this.keyboardCloseEditor) - window.addEventListener('keydown', this.keyboardSaveEvent) - window.addEventListener('keydown', this.keyboardDeleteEvent) - window.addEventListener('keydown', this.keyboardDuplicateEvent) window.addEventListener('resize', this.handleResize) this.$nextTick(() => { @@ -548,10 +544,6 @@ export default { }, beforeUnmount() { - window.removeEventListener('keydown', this.keyboardCloseEditor) - window.removeEventListener('keydown', this.keyboardSaveEvent) - window.removeEventListener('keydown', this.keyboardDeleteEvent) - window.removeEventListener('keydown', this.keyboardDuplicateEvent) window.removeEventListener('resize', this.handleResize) // Clean up resize timeout diff --git a/tests/javascript/unit/mixins/EditorMixin.test.js b/tests/javascript/unit/mixins/EditorMixin.test.js index 5886dd07be..48b3b53e17 100644 --- a/tests/javascript/unit/mixins/EditorMixin.test.js +++ b/tests/javascript/unit/mixins/EditorMixin.test.js @@ -72,18 +72,59 @@ describe('mixins/EditorMixin test suite', () => { }) }) + describe('keyboardCloseEditor', () => { + it('cancels the editor', () => { + const cancel = vi.fn() + EditorMixin.methods.keyboardCloseEditor.call({ cancel }) + + expect(cancel).toHaveBeenCalledWith(false) + }) + }) + + describe('keyboardSaveEvent', () => { + it('does not save when read-only or editing a recurrence exception', () => { + const saveAndLeave = vi.fn() + EditorMixin.methods.keyboardSaveEvent.call({ isReadOnly: true, canCreateRecurrenceException: false, saveAndLeave }) + EditorMixin.methods.keyboardSaveEvent.call({ isReadOnly: false, canCreateRecurrenceException: true, saveAndLeave }) + + expect(saveAndLeave).not.toHaveBeenCalled() + }) + + it('saves when allowed', () => { + const saveAndLeave = vi.fn() + EditorMixin.methods.keyboardSaveEvent.call({ isReadOnly: false, canCreateRecurrenceException: false, saveAndLeave }) + + expect(saveAndLeave).toHaveBeenCalledWith(false) + }) + }) + + describe('keyboardDeleteEvent', () => { + it('does not delete when not allowed or editing a recurrence exception', () => { + const deleteAndLeave = vi.fn() + EditorMixin.methods.keyboardDeleteEvent.call({ canDelete: false, canCreateRecurrenceException: false, deleteAndLeave }) + EditorMixin.methods.keyboardDeleteEvent.call({ canDelete: true, canCreateRecurrenceException: true, deleteAndLeave }) + + expect(deleteAndLeave).not.toHaveBeenCalled() + }) + + it('deletes when allowed', () => { + const deleteAndLeave = vi.fn() + EditorMixin.methods.keyboardDeleteEvent.call({ canDelete: true, canCreateRecurrenceException: false, deleteAndLeave }) + + expect(deleteAndLeave).toHaveBeenCalledWith(false) + }) + }) + describe('keyboardDuplicateEvent', () => { it('does not trigger a duplication when it is not allowed in the current view', () => { const duplicateEvent = vi.fn() const vm = { isNew: false, - canCreateRecurrenceException: false, canDuplicate: false, duplicateEvent, } - const event = { key: 'd', ctrlKey: true, preventDefault: vi.fn() } - EditorMixin.methods.keyboardDuplicateEvent.call(vm, event) + EditorMixin.methods.keyboardDuplicateEvent.call(vm) expect(duplicateEvent).not.toHaveBeenCalled() }) @@ -92,15 +133,37 @@ describe('mixins/EditorMixin test suite', () => { const duplicateEvent = vi.fn() const vm = { isNew: false, - canCreateRecurrenceException: false, canDuplicate: true, duplicateEvent, } - const event = { key: 'd', ctrlKey: true, preventDefault: vi.fn() } - EditorMixin.methods.keyboardDuplicateEvent.call(vm, event) + EditorMixin.methods.keyboardDuplicateEvent.call(vm) expect(duplicateEvent).toHaveBeenCalled() }) }) + + describe('mounted/beforeUnmount hotkey wiring', () => { + it('registers hotkeys on mount and removes them on unmount', () => { + const duplicateEvent = vi.fn() + const vm = { + ...EditorMixin.methods, + isNew: false, + canDuplicate: true, + duplicateEvent, + hotKeysRegister: [], + } + + EditorMixin.mounted.call(vm) + expect(vm.hotKeysRegister).toHaveLength(4) + + document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', ctrlKey: true, bubbles: true })) + expect(duplicateEvent).toHaveBeenCalledTimes(1) + + EditorMixin.beforeUnmount.call(vm) + + document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'd', ctrlKey: true, bubbles: true })) + expect(duplicateEvent).toHaveBeenCalledTimes(1) + }) + }) })