From 465c3b87a76f5396d0e2fd3dc69330b8887be847 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 21 Nov 2019 22:24:07 +0100 Subject: [PATCH] tree keyboard shortcuts --- .../javascripts/services/keyboard_actions.js | 23 +- src/public/javascripts/services/tree.js | 10 +- .../javascripts/services/tree_keybindings.js | 308 ++++++++++-------- src/services/keyboard_actions.js | 88 ++++- 4 files changed, 258 insertions(+), 171 deletions(-) diff --git a/src/public/javascripts/services/keyboard_actions.js b/src/public/javascripts/services/keyboard_actions.js index 1cfd49244..1498788f6 100644 --- a/src/public/javascripts/services/keyboard_actions.js +++ b/src/public/javascripts/services/keyboard_actions.js @@ -52,13 +52,7 @@ function setActionHandler(actionName, handler) { } async function triggerAction(actionName) { - await keyboardActionsLoaded; - - const action = keyboardActionRepo[actionName]; - - if (!action) { - throw new Error(`Cannot find action ${actionName}`); - } + const action = getAction(actionName); if (!action.handler) { throw new Error(`Action ${actionName} has no handler`); @@ -67,7 +61,20 @@ async function triggerAction(actionName) { await action.handler(); } +async function getAction(actionName) { + await keyboardActionsLoaded; + + const action = keyboardActionRepo[actionName]; + + if (!action) { + throw new Error(`Cannot find action ${actionName}`); + } + + return action; +} + export default { setActionHandler, - triggerAction + triggerAction, + getAction }; \ No newline at end of file diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index d56f79108..de8f8f67d 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -9,9 +9,7 @@ import server from './server.js'; import treeCache from './tree_cache.js'; import toastService from "./toast.js"; import treeBuilder from "./tree_builder.js"; -import treeKeyBindings from "./tree_keybindings.js"; -import Branch from '../entities/branch.js'; -import NoteShort from '../entities/note_short.js'; +import treeKeyBindingService from "./tree_keybindings.js"; import hoistedNoteService from '../services/hoisted_note.js'; import optionsService from "../services/options.js"; import TreeContextMenu from "./tree_context_menu.js"; @@ -430,7 +428,7 @@ async function treeInitialized() { setFrontendAsLoaded(); } -function initFancyTree(tree) { +async function initFancyTree(tree) { utils.assertArguments(tree); $tree.fancytree({ @@ -473,7 +471,7 @@ function initFancyTree(tree) { collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false), init: (event, data) => treeInitialized(), // don't collapse to short form hotkeys: { - keydown: treeKeyBindings + keydown: await treeKeyBindingService.getKeyboardBindings() }, dnd5: dragAndDropSetup, lazyLoad: function(event, data) { @@ -754,7 +752,7 @@ async function sortAlphabetically(noteId) { async function showTree() { const tree = await loadTree(); - initFancyTree(tree); + await initFancyTree(tree); } ws.subscribeToMessages(message => { diff --git a/src/public/javascripts/services/tree_keybindings.js b/src/public/javascripts/services/tree_keybindings.js index 1ce0b43c9..4d3ecb14a 100644 --- a/src/public/javascripts/services/tree_keybindings.js +++ b/src/public/javascripts/services/tree_keybindings.js @@ -5,148 +5,9 @@ import hoistedNoteService from "./hoisted_note.js"; import clipboard from "./clipboard.js"; import treeCache from "./tree_cache.js"; import searchNoteService from "./search_notes.js"; +import keyboardActionService from "./keyboard_actions.js"; -const keyBindings = { - "del": node => { - treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node)); - }, - "ctrl+up": node => { - const beforeNode = node.getPrevSibling(); - - if (beforeNode !== null) { - treeChangesService.moveBeforeNode([node], beforeNode); - } - - return false; - }, - "ctrl+down": node => { - const afterNode = node.getNextSibling(); - if (afterNode !== null) { - treeChangesService.moveAfterNode([node], afterNode); - } - - return false; - }, - "ctrl+left": node => { - treeChangesService.moveNodeUpInHierarchy(node); - - return false; - }, - "ctrl+right": node => { - const toNode = node.getPrevSibling(); - - if (toNode !== null) { - treeChangesService.moveToNode([node], toNode); - } - - return false; - }, - "shift+up": () => { - const node = treeService.getFocusedNode(); - - if (!node) { - return; - } - - if (node.isActive()) { - node.setSelected(true); - } - - node.navigate($.ui.keyCode.UP, false).then(() => { - const currentNode = treeService.getFocusedNode(); - - if (currentNode.isSelected()) { - node.setSelected(false); - } - - currentNode.setSelected(true); - }); - - return false; - }, - "shift+down": () => { - const node = treeService.getFocusedNode(); - - if (!node) { - return; - } - - if (node.isActive()) { - node.setSelected(true); - } - - node.navigate($.ui.keyCode.DOWN, false).then(() => { - const currentNode = treeService.getFocusedNode(); - - if (currentNode.isSelected()) { - node.setSelected(false); - } - - currentNode.setSelected(true); - }); - - return false; - }, - "f2": async node => { - const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js"); - editBranchPrefixDialog.showDialog(node); - }, - "alt+-": node => { - treeService.collapseTree(node); - }, - "alt+s": node => { - treeService.sortAlphabetically(node.data.noteId); - - return false; - }, - "ctrl+a": node => { - for (const child of node.getParent().getChildren()) { - child.setSelected(true); - } - - return false; - }, - "ctrl+c": node => { - clipboard.copy(treeService.getSelectedOrActiveNodes(node)); - - return false; - }, - "ctrl+x": node => { - clipboard.cut(treeService.getSelectedOrActiveNodes(node)); - - return false; - }, - "ctrl+v": node => { - clipboard.pasteInto(node); - - return false; - }, - "return": node => { - noteDetailService.focusOnTitle(); - - return false; - }, - "backspace": async node => { - if (!await hoistedNoteService.isRootNode(node)) { - node.getParent().setActive().then(treeService.clearSelectedNodes); - } - }, - "ctrl+h": node => { - hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => { - if (node.data.noteId === hoistedNoteId) { - hoistedNoteService.unhoist(); - } - else { - const note = await treeCache.getNote(node.data.noteId); - - if (note.type !== 'search') { - hoistedNoteService.setHoistedNoteId(node.data.noteId); - } - } - }); - - return false; - }, +const fixedKeyBindings = { // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin // after opening context menu, standard shortcuts don't work, but they are detected here // so we essentially takeover the standard handling with our implementation. @@ -169,12 +30,173 @@ const keyBindings = { node.navigate($.ui.keyCode.DOWN, true).then(treeService.clearSelectedNodes); return false; + } +}; + +const templates = { + "DeleteNote": node => { + treeChangesService.deleteNodes(treeService.getSelectedOrActiveNodes(node)); }, - "ctrl+shift+s": node => { + "MoveNoteUp": node => { + const beforeNode = node.getPrevSibling(); + + if (beforeNode !== null) { + treeChangesService.moveBeforeNode([node], beforeNode); + } + + return false; + }, + "MoveNoteDown": node => { + const afterNode = node.getNextSibling(); + if (afterNode !== null) { + treeChangesService.moveAfterNode([node], afterNode); + } + + return false; + }, + "MoveNoteUpInHierarchy": node => { + treeChangesService.moveNodeUpInHierarchy(node); + + return false; + }, + "MoveNoteDownInHierarchy": node => { + const toNode = node.getPrevSibling(); + + if (toNode !== null) { + treeChangesService.moveToNode([node], toNode); + } + + return false; + }, + "AddNoteAboveToSelection": () => { + const node = treeService.getFocusedNode(); + + if (!node) { + return; + } + + if (node.isActive()) { + node.setSelected(true); + } + + node.navigate($.ui.keyCode.UP, false).then(() => { + const currentNode = treeService.getFocusedNode(); + + if (currentNode.isSelected()) { + node.setSelected(false); + } + + currentNode.setSelected(true); + }); + + return false; + }, + "AddNoteBelowToSelection": () => { + const node = treeService.getFocusedNode(); + + if (!node) { + return; + } + + if (node.isActive()) { + node.setSelected(true); + } + + node.navigate($.ui.keyCode.DOWN, false).then(() => { + const currentNode = treeService.getFocusedNode(); + + if (currentNode.isSelected()) { + node.setSelected(false); + } + + currentNode.setSelected(true); + }); + + return false; + }, + // TODO: this shouldn't be tree specific shortcut + "EditBranchPrefix": async node => { + const editBranchPrefixDialog = await import("../dialogs/branch_prefix.js"); + editBranchPrefixDialog.showDialog(node); + }, + "CollapseSubtree": node => { + treeService.collapseTree(node); + }, + "SortChildNotes": node => { + treeService.sortAlphabetically(node.data.noteId); + + return false; + }, + "SelectAllNotesInParent": node => { + for (const child of node.getParent().getChildren()) { + child.setSelected(true); + } + + return false; + }, + "CopyNotesToClipboard": node => { + clipboard.copy(treeService.getSelectedOrActiveNodes(node)); + + return false; + }, + "CutNotesToClipboard": node => { + clipboard.cut(treeService.getSelectedOrActiveNodes(node)); + + return false; + }, + "PasteNotesFromClipboard": node => { + clipboard.pasteInto(node); + + return false; + }, + "EditNoteTitle": node => { + noteDetailService.focusOnTitle(); + + return false; + }, + "ActivateParentNote": async node => { + if (!await hoistedNoteService.isRootNode(node)) { + node.getParent().setActive().then(treeService.clearSelectedNodes); + } + }, + // TODO: this should probably be app-global shortcut + "ToggleNoteHoisting": node => { + hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => { + if (node.data.noteId === hoistedNoteId) { + hoistedNoteService.unhoist(); + } + else { + const note = await treeCache.getNote(node.data.noteId); + + if (note.type !== 'search') { + hoistedNoteService.setHoistedNoteId(node.data.noteId); + } + } + }); + + return false; + }, + "SearchInSubtree": node => { searchNoteService.searchInSubtree(node.data.noteId); return false; } }; -export default keyBindings; \ No newline at end of file +async function getKeyboardBindings() { + const bindings = Object.assign({}, fixedKeyBindings); + + for (const actionName in templates) { + const action = await keyboardActionService.getAction(actionName); + + for (const shortcut of action.effectiveShortcuts || []) { + bindings[shortcut] = templates[actionName]; + } + } + + return bindings; +} + +export default { + getKeyboardBindings +}; \ No newline at end of file diff --git a/src/services/keyboard_actions.js b/src/services/keyboard_actions.js index b757406ba..18a65a706 100644 --- a/src/services/keyboard_actions.js +++ b/src/services/keyboard_actions.js @@ -114,19 +114,89 @@ const DEFAULT_KEYBOARD_ACTIONS = [ defaultShortcuts: ["CommandOrControl+return"] }, { - actionName: "ClipboardCopy", + actionName: "DeleteNote", + defaultShortcuts: ["Delete"], + description: "Delete note" + }, + { + actionName: "MoveNoteUp", + defaultShortcuts: ["Ctrl+Up"], + description: "Move note up" + }, + { + actionName: "MoveNoteDown", + defaultShortcuts: ["Ctrl+Down"], + description: "Move note down" + }, + { + actionName: "MoveNoteUpInHierarchy", + defaultShortcuts: ["Ctrl+Left"], + description: "Move note up in hierarchy" + }, + { + actionName: "MoveNoteDownInHierarchy", + defaultShortcuts: ["Ctrl+Right"], + description: "Move note down in hierarchy" + }, + { + actionName: "AddNoteAboveToSelection", + defaultShortcuts: ["Shift+Up"], + description: "Add note above to the selection" + }, + { + actionName: "AddNoteBelowToSelection", + defaultShortcuts: ["Shift+Down"], + description: "Add note above to the selection" + }, + { + actionName: "CopyNotesToClipboard", defaultShortcuts: ["CommandOrControl+C"], description: "Copy selected notes to the clipboard" }, { - actionName: "ClipboardPaste", + actionName: "PasteNotesFromClipboard", defaultShortcuts: ["CommandOrControl+V"], description: "Paste notes from the clipboard into active note" }, { - actionName: "ClipboardCut", + actionName: "CutNotesToClipboard", defaultShortcuts: ["CommandOrControl+X"], - description: "Copy selected notes to the clipboard" + description: "Cut selected notes to the clipboard" + }, + { + actionName: "EditBranchPrefix", + defaultShortcuts: ["F2"], + description: "Show Edit branch prefix dialog" + }, + { + actionName: "CollapseSubtree", + defaultShortcuts: ["Alt+-"], + description: "Collapses subtree of current note" + }, + { + actionName: "SortChildNotes", + defaultShortcuts: ["Alt+s"], + description: "Sort child notes" + }, + { + actionName: "ActivateParentNote", + defaultShortcuts: ["Backspace"], + description: "Activates parent note of currently active note" + }, + { + actionName: "ToggleNoteHoisting", + defaultShortcuts: ["Alt+h"], + description: "Toggles note hoisting of active note" + }, + { + actionName: "SearchInSubtree", + defaultShortcuts: ["CommandOrControl+Shift+S"], + description: "Search for notes in the active note's subtree" + }, + { + actionName: "EditNoteTitle", + defaultShortcuts: ["return"], + description: "Edit active note title" }, { actionName: "SelectAllNotesInParent", @@ -136,16 +206,6 @@ const DEFAULT_KEYBOARD_ACTIONS = [ { separator: "Text note operations" }, - { - actionName: "Undo", - defaultShortcuts: ["CommandOrControl+Z"], - description: "Undo last text operation (applicable on MacOS only)" - }, - { - actionName: "Redo", - defaultShortcuts: ["CommandOrControl+Y"], - description: "Undo last text operation (applicable on MacOS only)" - }, { actionName: "AddLinkToText", defaultShortcuts: ["CommandOrControl+L"],