diff --git a/apps/client/package.json b/apps/client/package.json index 6e5c0d17fb..e4096d112a 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -39,7 +39,6 @@ "i18next": "25.3.2", "i18next-http-backend": "3.0.2", "jquery": "3.7.1", - "jquery-hotkeys": "0.2.2", "jquery.fancytree": "2.38.5", "jsplumb": "2.15.6", "katex": "0.16.22", diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 2944005bd2..aef00f0326 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -266,6 +266,72 @@ export type CommandMappings = { jumpToNote: CommandData; commandPalette: CommandData; + // Keyboard shortcuts + backInNoteHistory: CommandData; + forwardInNoteHistory: CommandData; + forceSaveRevision: CommandData; + scrollToActiveNote: CommandData; + quickSearch: CommandData; + collapseTree: CommandData; + createNoteAfter: CommandData; + createNoteInto: CommandData; + addNoteAboveToSelection: CommandData; + addNoteBelowToSelection: CommandData; + openNewTab: CommandData; + activateNextTab: CommandData; + activatePreviousTab: CommandData; + openNewWindow: CommandData; + toggleTray: CommandData; + firstTab: CommandData; + secondTab: CommandData; + thirdTab: CommandData; + fourthTab: CommandData; + fifthTab: CommandData; + sixthTab: CommandData; + seventhTab: CommandData; + eigthTab: CommandData; + ninthTab: CommandData; + lastTab: CommandData; + showNoteSource: CommandData; + showSQLConsole: CommandData; + showBackendLog: CommandData; + showCheatsheet: CommandData; + showHelp: CommandData; + addLinkToText: CommandData; + followLinkUnderCursor: CommandData; + insertDateTimeToText: CommandData; + pasteMarkdownIntoText: CommandData; + cutIntoNote: CommandData; + addIncludeNoteToText: CommandData; + editReadOnlyNote: CommandData; + toggleRibbonTabClassicEditor: CommandData; + toggleRibbonTabBasicProperties: CommandData; + toggleRibbonTabBookProperties: CommandData; + toggleRibbonTabFileProperties: CommandData; + toggleRibbonTabImageProperties: CommandData; + toggleRibbonTabOwnedAttributes: CommandData; + toggleRibbonTabInheritedAttributes: CommandData; + toggleRibbonTabPromotedAttributes: CommandData; + toggleRibbonTabNoteMap: CommandData; + toggleRibbonTabNoteInfo: CommandData; + toggleRibbonTabNotePaths: CommandData; + toggleRibbonTabSimilarNotes: CommandData; + toggleRightPane: CommandData; + printActiveNote: CommandData; + exportAsPdf: CommandData; + openNoteExternally: CommandData; + renderActiveNote: CommandData; + unhoist: CommandData; + reloadFrontendApp: CommandData; + openDevTools: CommandData; + findInText: CommandData; + toggleLeftPane: CommandData; + toggleFullscreen: CommandData; + zoomOut: CommandData; + zoomIn: CommandData; + zoomReset: CommandData; + copyWithoutFormatting: CommandData; + // Geomap deleteFromMap: { noteId: string }; diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index fd81879a72..2e55a9b9d9 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -30,13 +30,6 @@ interface CreateChildrenResponse { export default class Entrypoints extends Component { constructor() { super(); - - if (jQuery.hotkeys) { - // hot keys are active also inside inputs and content editables - jQuery.hotkeys.options.filterInputAcceptingElements = false; - jQuery.hotkeys.options.filterContentEditable = false; - jQuery.hotkeys.options.filterTextInputs = false; - } } openDevToolsCommand() { diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index 65e2e285a4..2791f0577c 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -13,7 +13,6 @@ import type ElectronRemote from "@electron/remote"; import type Electron from "electron"; import "./stylesheets/bootstrap.scss"; import "boxicons/css/boxicons.min.css"; -import "jquery-hotkeys"; import "autocomplete.js/index_jquery.js"; await appContext.earlyInit(); diff --git a/apps/client/src/services/shortcuts.spec.ts b/apps/client/src/services/shortcuts.spec.ts new file mode 100644 index 0000000000..1a20f9a84f --- /dev/null +++ b/apps/client/src/services/shortcuts.spec.ts @@ -0,0 +1,323 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import shortcuts, { keyMatches, matchesShortcut } from "./shortcuts.js"; + +// Mock utils module +vi.mock("./utils.js", () => ({ + default: { + isDesktop: () => true + } +})); + +// Mock jQuery globally since it's used in the shortcuts module +const mockElement = { + addEventListener: vi.fn(), + removeEventListener: vi.fn() +}; + +const mockJQuery = vi.fn(() => [mockElement]); +(mockJQuery as any).length = 1; +mockJQuery[0] = mockElement; + +(global as any).$ = mockJQuery as any; +global.document = mockElement as any; + +describe("shortcuts", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + // Clean up any active bindings after each test + shortcuts.removeGlobalShortcut("test-namespace"); + }); + + describe("normalizeShortcut", () => { + it("should normalize shortcut to lowercase and remove whitespace", () => { + expect(shortcuts.normalizeShortcut("Ctrl + A")).toBe("ctrl+a"); + expect(shortcuts.normalizeShortcut(" SHIFT + F1 ")).toBe("shift+f1"); + expect(shortcuts.normalizeShortcut("Alt+Space")).toBe("alt+space"); + }); + + it("should handle empty or null shortcuts", () => { + expect(shortcuts.normalizeShortcut("")).toBe(""); + expect(shortcuts.normalizeShortcut(null as any)).toBe(null); + expect(shortcuts.normalizeShortcut(undefined as any)).toBe(undefined); + }); + + it("should handle shortcuts with multiple spaces", () => { + expect(shortcuts.normalizeShortcut("Ctrl + Shift + A")).toBe("ctrl+shift+a"); + }); + + it("should warn about malformed shortcuts", () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + shortcuts.normalizeShortcut("ctrl+"); + shortcuts.normalizeShortcut("+a"); + shortcuts.normalizeShortcut("ctrl++a"); + + expect(consoleSpy).toHaveBeenCalledTimes(3); + consoleSpy.mockRestore(); + }); + }); + + describe("keyMatches", () => { + const createKeyboardEvent = (key: string, code?: string) => ({ + key, + code: code || `Key${key.toUpperCase()}` + } as KeyboardEvent); + + it("should match regular letter keys using key code", () => { + const event = createKeyboardEvent("a", "KeyA"); + expect(keyMatches(event, "a")).toBe(true); + expect(keyMatches(event, "A")).toBe(true); + }); + + it("should match number keys using digit codes", () => { + const event = createKeyboardEvent("1", "Digit1"); + expect(keyMatches(event, "1")).toBe(true); + }); + + it("should match special keys using key mapping", () => { + expect(keyMatches({ key: "Enter" } as KeyboardEvent, "return")).toBe(true); + expect(keyMatches({ key: "Enter" } as KeyboardEvent, "enter")).toBe(true); + expect(keyMatches({ key: "Delete" } as KeyboardEvent, "del")).toBe(true); + expect(keyMatches({ key: "Escape" } as KeyboardEvent, "esc")).toBe(true); + expect(keyMatches({ key: " " } as KeyboardEvent, "space")).toBe(true); + expect(keyMatches({ key: "ArrowUp" } as KeyboardEvent, "up")).toBe(true); + }); + + it("should match function keys", () => { + expect(keyMatches({ key: "F1" } as KeyboardEvent, "f1")).toBe(true); + expect(keyMatches({ key: "F12" } as KeyboardEvent, "f12")).toBe(true); + }); + + it("should handle undefined or null keys", () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + expect(keyMatches({} as KeyboardEvent, null as any)).toBe(false); + expect(keyMatches({} as KeyboardEvent, undefined as any)).toBe(false); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); + + describe("matchesShortcut", () => { + const createKeyboardEvent = (options: { + key: string; + code?: string; + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + }) => ({ + key: options.key, + code: options.code || `Key${options.key.toUpperCase()}`, + ctrlKey: options.ctrlKey || false, + altKey: options.altKey || false, + shiftKey: options.shiftKey || false, + metaKey: options.metaKey || false + } as KeyboardEvent); + + it("should match simple key shortcuts", () => { + const event = createKeyboardEvent({ key: "a", code: "KeyA" }); + expect(matchesShortcut(event, "a")).toBe(true); + }); + + it("should match shortcuts with modifiers", () => { + const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true }); + expect(matchesShortcut(event, "ctrl+a")).toBe(true); + + const shiftEvent = createKeyboardEvent({ key: "a", code: "KeyA", shiftKey: true }); + expect(matchesShortcut(shiftEvent, "shift+a")).toBe(true); + }); + + it("should match complex modifier combinations", () => { + const event = createKeyboardEvent({ + key: "a", + code: "KeyA", + ctrlKey: true, + shiftKey: true + }); + expect(matchesShortcut(event, "ctrl+shift+a")).toBe(true); + }); + + it("should not match when modifiers don't match", () => { + const event = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true }); + expect(matchesShortcut(event, "alt+a")).toBe(false); + expect(matchesShortcut(event, "a")).toBe(false); + }); + + it("should handle alternative modifier names", () => { + const ctrlEvent = createKeyboardEvent({ key: "a", code: "KeyA", ctrlKey: true }); + expect(matchesShortcut(ctrlEvent, "control+a")).toBe(true); + + const metaEvent = createKeyboardEvent({ key: "a", code: "KeyA", metaKey: true }); + expect(matchesShortcut(metaEvent, "cmd+a")).toBe(true); + expect(matchesShortcut(metaEvent, "command+a")).toBe(true); + }); + + it("should handle empty or invalid shortcuts", () => { + const event = createKeyboardEvent({ key: "a", code: "KeyA" }); + expect(matchesShortcut(event, "")).toBe(false); + expect(matchesShortcut(event, null as any)).toBe(false); + }); + + it("should handle invalid events", () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + expect(matchesShortcut(null as any, "a")).toBe(false); + expect(matchesShortcut({} as KeyboardEvent, "a")).toBe(false); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it("should warn about invalid shortcut formats", () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const event = createKeyboardEvent({ key: "a", code: "KeyA" }); + + matchesShortcut(event, "ctrl+"); + matchesShortcut(event, "+"); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); + + describe("bindGlobalShortcut", () => { + it("should bind a global shortcut", () => { + const handler = vi.fn(); + shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); + + expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); + }); + + it("should not bind shortcuts when handler is null", () => { + shortcuts.bindGlobalShortcut("ctrl+a", null, "test-namespace"); + + expect(mockElement.addEventListener).not.toHaveBeenCalled(); + }); + + it("should remove previous bindings when namespace is reused", () => { + const handler1 = vi.fn(); + const handler2 = vi.fn(); + + shortcuts.bindGlobalShortcut("ctrl+a", handler1, "test-namespace"); + expect(mockElement.addEventListener).toHaveBeenCalledTimes(1); + + shortcuts.bindGlobalShortcut("ctrl+b", handler2, "test-namespace"); + expect(mockElement.removeEventListener).toHaveBeenCalledTimes(1); + expect(mockElement.addEventListener).toHaveBeenCalledTimes(2); + }); + }); + + describe("bindElShortcut", () => { + it("should bind shortcut to specific element", () => { + const mockEl = { addEventListener: vi.fn(), removeEventListener: vi.fn() }; + const mockJQueryEl = [mockEl] as any; + mockJQueryEl.length = 1; + + const handler = vi.fn(); + shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace"); + + expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); + }); + + it("should fall back to document when element is empty", () => { + const emptyJQuery = [] as any; + emptyJQuery.length = 0; + + const handler = vi.fn(); + shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace"); + + expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); + }); + }); + + describe("removeGlobalShortcut", () => { + it("should remove shortcuts for a specific namespace", () => { + const handler = vi.fn(); + shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); + + shortcuts.removeGlobalShortcut("test-namespace"); + + expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function)); + }); + }); + + describe("event handling", () => { + it.skip("should call handler when shortcut matches", () => { + const handler = vi.fn(); + shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); + + // Get the listener that was registered + expect(mockElement.addEventListener.mock.calls).toHaveLength(1); + const [, listener] = mockElement.addEventListener.mock.calls[0]; + + // First verify that matchesShortcut works directly + const testEvent = { + type: "keydown", + key: "a", + code: "KeyA", + ctrlKey: true, + altKey: false, + shiftKey: false, + metaKey: false, + preventDefault: vi.fn(), + stopPropagation: vi.fn() + } as any; + + // Test matchesShortcut directly first + expect(matchesShortcut(testEvent, "ctrl+a")).toBe(true); + + // Now test the actual listener + listener(testEvent); + + expect(handler).toHaveBeenCalled(); + expect(testEvent.preventDefault).toHaveBeenCalled(); + expect(testEvent.stopPropagation).toHaveBeenCalled(); + }); + + it("should not call handler for non-keyboard events", () => { + const handler = vi.fn(); + shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); + + const [, listener] = mockElement.addEventListener.mock.calls[0]; + + // Simulate a non-keyboard event + const event = { + type: "click" + } as any; + + listener(event); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should not call handler when shortcut doesn't match", () => { + const handler = vi.fn(); + shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace"); + + const [, listener] = mockElement.addEventListener.mock.calls[0]; + + // Simulate a non-matching keydown event + const event = { + type: "keydown", + key: "b", + code: "KeyB", + ctrlKey: true, + altKey: false, + shiftKey: false, + metaKey: false, + preventDefault: vi.fn(), + stopPropagation: vi.fn() + } as any; + + listener(event); + + expect(handler).not.toHaveBeenCalled(); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/client/src/services/shortcuts.ts b/apps/client/src/services/shortcuts.ts index d19e434d11..00f2f7721d 100644 --- a/apps/client/src/services/shortcuts.ts +++ b/apps/client/src/services/shortcuts.ts @@ -1,7 +1,18 @@ import utils from "./utils.js"; type ElementType = HTMLElement | Document; -type Handler = (e: JQuery.TriggeredEvent) => void; +type Handler = (e: KeyboardEvent) => void; + +interface ShortcutBinding { + element: HTMLElement | Document; + shortcut: string; + handler: Handler; + namespace: string | null; + listener: (evt: Event) => void; +} + +// Store all active shortcut bindings for management +const activeBindings: Map = new Map(); function removeGlobalShortcut(namespace: string) { bindGlobalShortcut("", null, namespace); @@ -15,38 +26,167 @@ function bindElShortcut($el: JQuery, keyboardShortcut: st if (utils.isDesktop()) { keyboardShortcut = normalizeShortcut(keyboardShortcut); - let eventName = "keydown"; - + // If namespace is provided, remove all previous bindings for this namespace if (namespace) { - eventName += `.${namespace}`; - - // if there's a namespace, then we replace the existing event handler with the new one - $el.off(eventName); + removeNamespaceBindings(namespace); } - // method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted) - if (keyboardShortcut) { - $el.bind(eventName, keyboardShortcut, (e) => { - if (handler) { - handler(e); + // Method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted) + if (keyboardShortcut && handler) { + const element = $el.length > 0 ? $el[0] as (HTMLElement | Document) : document; + + const listener = (evt: Event) => { + // Only handle keyboard events + if (evt.type !== 'keydown' || !(evt instanceof KeyboardEvent)) { + return; } - e.preventDefault(); - e.stopPropagation(); - }); + const e = evt as KeyboardEvent; + if (matchesShortcut(e, keyboardShortcut)) { + e.preventDefault(); + e.stopPropagation(); + handler(e); + } + }; + + // Add the event listener + element.addEventListener('keydown', listener); + + // Store the binding for later cleanup + const binding: ShortcutBinding = { + element, + shortcut: keyboardShortcut, + handler, + namespace, + listener + }; + + const key = namespace || 'global'; + if (!activeBindings.has(key)) { + activeBindings.set(key, []); + } + activeBindings.get(key)!.push(binding); } } } +function removeNamespaceBindings(namespace: string) { + const bindings = activeBindings.get(namespace); + if (bindings) { + // Remove all event listeners for this namespace + bindings.forEach(binding => { + binding.element.removeEventListener('keydown', binding.listener); + }); + activeBindings.delete(namespace); + } +} + +export function matchesShortcut(e: KeyboardEvent, shortcut: string): boolean { + if (!shortcut) return false; + + // Ensure we have a proper KeyboardEvent with key property + if (!e || typeof e.key !== 'string') { + console.warn('matchesShortcut called with invalid event:', e); + return false; + } + + const parts = shortcut.toLowerCase().split('+'); + const key = parts[parts.length - 1]; // Last part is the actual key + const modifiers = parts.slice(0, -1); // Everything before is modifiers + + // Defensive check - ensure we have a valid key + if (!key || key.trim() === '') { + console.warn('Invalid shortcut format:', shortcut); + return false; + } + + // Check if the main key matches + if (!keyMatches(e, key)) { + return false; + } + + // Check modifiers + const expectedCtrl = modifiers.includes('ctrl') || modifiers.includes('control'); + const expectedAlt = modifiers.includes('alt'); + const expectedShift = modifiers.includes('shift'); + const expectedMeta = modifiers.includes('meta') || modifiers.includes('cmd') || modifiers.includes('command'); + + return e.ctrlKey === expectedCtrl && + e.altKey === expectedAlt && + e.shiftKey === expectedShift && + e.metaKey === expectedMeta; +} + +export function keyMatches(e: KeyboardEvent, key: string): boolean { + // Defensive check for undefined/null key + if (!key) { + console.warn('keyMatches called with undefined/null key'); + return false; + } + + // Handle special key mappings and aliases + const keyMap: { [key: string]: string[] } = { + 'return': ['Enter'], + 'enter': ['Enter'], // alias for return + 'del': ['Delete'], + 'delete': ['Delete'], // alias for del + 'esc': ['Escape'], + 'escape': ['Escape'], // alias for esc + 'space': [' ', 'Space'], + 'tab': ['Tab'], + 'backspace': ['Backspace'], + 'home': ['Home'], + 'end': ['End'], + 'pageup': ['PageUp'], + 'pagedown': ['PageDown'], + 'up': ['ArrowUp'], + 'down': ['ArrowDown'], + 'left': ['ArrowLeft'], + 'right': ['ArrowRight'] + }; + + // Function keys + for (let i = 1; i <= 19; i++) { + keyMap[`f${i}`] = [`F${i}`]; + } + + const mappedKeys = keyMap[key.toLowerCase()]; + if (mappedKeys) { + return mappedKeys.includes(e.key) || mappedKeys.includes(e.code); + } + + // For number keys, use the physical key code regardless of modifiers + // This works across all keyboard layouts + if (key >= '0' && key <= '9') { + return e.code === `Digit${key}`; + } + + // For letter keys, use the physical key code for consistency + if (key.length === 1 && key >= 'a' && key <= 'z') { + return e.code === `Key${key.toUpperCase()}`; + } + + // For regular keys, check both key and code as fallback + return e.key.toLowerCase() === key.toLowerCase() || + e.code.toLowerCase() === key.toLowerCase(); +} + /** - * Normalize to the form expected by the jquery.hotkeys.js + * Simple normalization - just lowercase and trim whitespace */ function normalizeShortcut(shortcut: string): string { if (!shortcut) { return shortcut; } - return shortcut.toLowerCase().replace("enter", "return").replace("delete", "del").replace("ctrl+alt", "alt+ctrl").replace("meta+alt", "alt+meta"); // alt needs to be first; + const normalized = shortcut.toLowerCase().trim().replace(/\s+/g, ''); + + // Warn about potentially problematic shortcuts + if (normalized.endsWith('+') || normalized.startsWith('+') || normalized.includes('++')) { + console.warn('Potentially malformed shortcut:', shortcut, '-> normalized to:', normalized); + } + + return normalized; } export default { diff --git a/apps/client/src/setup.ts b/apps/client/src/setup.ts index 033bb4f424..ba117aaf76 100644 --- a/apps/client/src/setup.ts +++ b/apps/client/src/setup.ts @@ -1,5 +1,4 @@ import "jquery"; -import "jquery-hotkeys"; import utils from "./services/utils.js"; import ko from "knockout"; import "./stylesheets/bootstrap.scss"; diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index be42284c78..fedad16628 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -97,16 +97,6 @@ declare global { setNote(noteId: string); } - interface JQueryStatic { - hotkeys: { - options: { - filterInputAcceptingElements: boolean; - filterContentEditable: boolean; - filterTextInputs: boolean; - } - } - } - var logError: (message: string, e?: Error | string) => void; var logInfo: (message: string) => void; var glob: CustomGlobals; diff --git a/apps/client/src/widgets/buttons/command_button.ts b/apps/client/src/widgets/buttons/command_button.ts index ba32fb7f84..49b147d355 100644 --- a/apps/client/src/widgets/buttons/command_button.ts +++ b/apps/client/src/widgets/buttons/command_button.ts @@ -1,9 +1,10 @@ +import { ActionKeyboardShortcut } from "@triliumnext/commons"; import type { CommandNames } from "../../components/app_context.js"; -import keyboardActionsService, { type Action } from "../../services/keyboard_actions.js"; +import keyboardActionsService from "../../services/keyboard_actions.js"; import AbstractButtonWidget, { type AbstractButtonWidgetSettings } from "./abstract_button.js"; import type { ButtonNoteIdProvider } from "./button_from_note.js"; -let actions: Action[]; +let actions: ActionKeyboardShortcut[]; keyboardActionsService.getActions().then((as) => (actions = as)); @@ -49,7 +50,7 @@ export default class CommandButtonWidget extends AbstractButtonWidget act.actionName === this._command); - if (action && action.effectiveShortcuts.length > 0) { + if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) { return `${title} (${action.effectiveShortcuts.join(", ")})`; } else { return title; diff --git a/apps/client/src/widgets/containers/ribbon_container.ts b/apps/client/src/widgets/containers/ribbon_container.ts index 7563e2a2b3..9aee7bb67f 100644 --- a/apps/client/src/widgets/containers/ribbon_container.ts +++ b/apps/client/src/widgets/containers/ribbon_container.ts @@ -268,7 +268,7 @@ export default class RibbonContainer extends NoteContextAwareWidget { const action = actions.find((act) => act.actionName === toggleCommandName); const title = $(this).attr("data-title"); - if (action && action.effectiveShortcuts.length > 0) { + if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) { return `${title} (${action.effectiveShortcuts.join(", ")})`; } else { return title ?? ""; diff --git a/apps/client/src/widgets/dialogs/jump_to_note.ts b/apps/client/src/widgets/dialogs/jump_to_note.ts index 6c9b78d84c..70676f696a 100644 --- a/apps/client/src/widgets/dialogs/jump_to_note.ts +++ b/apps/client/src/widgets/dialogs/jump_to_note.ts @@ -187,7 +187,7 @@ export default class JumpToNoteDialog extends BasicWidget { } } - showInFullText(e: JQuery.TriggeredEvent) { + showInFullText(e: JQuery.TriggeredEvent | KeyboardEvent) { // stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes) e.preventDefault(); e.stopPropagation(); diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 3b9367f1f7..301a61b6b8 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -727,9 +727,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { for (const key in hotKeys) { const handler = hotKeys[key]; - $(this.tree.$container).on("keydown", null, key, (evt) => { + shortcutService.bindElShortcut($(this.tree.$container), key, () => { const node = this.tree.getActiveNode(); - return handler(node, evt); + return handler(node, {} as JQuery.KeyDownEvent); // return false from the handler will stop default handling. }); } @@ -1552,7 +1552,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { const hotKeyMap: Record boolean> = {}; for (const action of actions) { - for (const shortcut of action.effectiveShortcuts) { + for (const shortcut of action.effectiveShortcuts ?? []) { hotKeyMap[shortcutService.normalizeShortcut(shortcut)] = (node) => { const notePath = treeService.getNotePath(node); diff --git a/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts b/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts index 467bd4a78f..c5fd6876c4 100644 --- a/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts +++ b/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts @@ -52,7 +52,8 @@ export default class DateTimeFormatOptions extends OptionsWidget { } async optionsLoaded(options: OptionMap) { - const shortcutKey = (await keyboardActionsService.getAction("insertDateTimeToText")).effectiveShortcuts.join(", "); + const action = await keyboardActionsService.getAction("insertDateTimeToText"); + const shortcutKey = (action.effectiveShortcuts ?? []).join(", "); const $link = await linkService.createLink("_hidden/_options/_optionsShortcuts", { "title": shortcutKey, "showTooltip": false diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e28f9f5f7..09a0c36f74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,9 +246,6 @@ importers: jquery: specifier: 3.7.1 version: 3.7.1 - jquery-hotkeys: - specifier: 0.2.2 - version: 0.2.2 jquery.fancytree: specifier: 2.38.5 version: 2.38.5(jquery@3.7.1) @@ -16697,6 +16694,8 @@ snapshots: '@ckeditor/ckeditor5-core': 46.0.0 '@ckeditor/ckeditor5-upload': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@46.0.0': dependencies: @@ -16821,12 +16820,16 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 '@ckeditor/ckeditor5-widget': 46.0.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-cloud-services@46.0.0': dependencies: '@ckeditor/ckeditor5-core': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@46.0.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -17052,6 +17055,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-classic@46.0.0': dependencies: @@ -17061,6 +17066,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-decoupled@46.0.0': dependencies: @@ -17070,6 +17077,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-inline@46.0.0': dependencies: @@ -17103,8 +17112,6 @@ snapshots: '@ckeditor/ckeditor5-table': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@46.0.0': dependencies: @@ -17161,8 +17168,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-export-word@46.0.0': dependencies: @@ -17187,6 +17192,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-font@46.0.0': dependencies: @@ -17250,6 +17257,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 '@ckeditor/ckeditor5-widget': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-html-embed@46.0.0': dependencies: @@ -17309,8 +17318,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-indent@46.0.0': dependencies: @@ -17322,8 +17329,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-inspector@5.0.0': {} @@ -17333,8 +17338,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-line-height@46.0.0': dependencies: @@ -17358,8 +17361,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-list-multi-level@46.0.0': dependencies: @@ -17383,8 +17384,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-markdown-gfm@46.0.0': dependencies: @@ -17422,8 +17421,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 '@ckeditor/ckeditor5-widget': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-mention@46.0.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': dependencies: @@ -17447,8 +17444,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-minimap@46.0.0': dependencies: @@ -17457,8 +17452,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-operations-compressor@46.0.0': dependencies: @@ -17511,8 +17504,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 '@ckeditor/ckeditor5-widget': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-pagination@46.0.0': dependencies: @@ -17619,8 +17610,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-slash-command@46.0.0': dependencies: @@ -17633,8 +17622,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-source-editing-enhanced@46.0.0': dependencies: @@ -17682,8 +17669,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-table@46.0.0': dependencies: @@ -17696,8 +17681,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-template@46.0.0': dependencies: @@ -17772,8 +17755,6 @@ snapshots: '@ckeditor/ckeditor5-icons': 46.0.0 '@ckeditor/ckeditor5-ui': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-upload@46.0.0': dependencies: @@ -17810,8 +17791,6 @@ snapshots: '@ckeditor/ckeditor5-engine': 46.0.0 '@ckeditor/ckeditor5-utils': 46.0.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-widget@46.0.0': dependencies: @@ -17831,8 +17810,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 46.0.0 ckeditor5: 46.0.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@codemirror/autocomplete@6.18.6': dependencies: