mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 13:39:01 +01:00 
			
		
		
		
	Remove unmaintained hotkeys dependency (#6507)
This commit is contained in:
		
						commit
						133c9c5a7b
					
				@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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 };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										323
									
								
								apps/client/src/services/shortcuts.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								apps/client/src/services/shortcuts.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -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();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -1,7 +1,18 @@
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
 | 
			
		||||
type ElementType = HTMLElement | Document;
 | 
			
		||||
type Handler = (e: JQuery.TriggeredEvent<ElementType | Element, string, ElementType | Element, ElementType | Element>) => 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<string, ShortcutBinding[]> = new Map();
 | 
			
		||||
 | 
			
		||||
function removeGlobalShortcut(namespace: string) {
 | 
			
		||||
    bindGlobalShortcut("", null, namespace);
 | 
			
		||||
@ -15,38 +26,167 @@ function bindElShortcut($el: JQuery<ElementType | Element>, 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 {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import "jquery";
 | 
			
		||||
import "jquery-hotkeys";
 | 
			
		||||
import utils from "./services/utils.js";
 | 
			
		||||
import ko from "knockout";
 | 
			
		||||
import "./stylesheets/bootstrap.scss";
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								apps/client/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								apps/client/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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<CommandBut
 | 
			
		||||
 | 
			
		||||
        const action = actions.find((act) => 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;
 | 
			
		||||
 | 
			
		||||
@ -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 ?? "";
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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<string, (node: Fancytree.FancytreeNode, e: JQuery.KeyDownEvent) => 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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										55
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										55
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@ -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:
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user