From c7b57841231c0bae50578d654e27db609d02c84f Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 3 May 2019 20:27:38 +0200 Subject: [PATCH] context menu refactoring --- package-lock.json | 2 +- src/public/javascripts/services/clipboard.js | 85 +++++ .../javascripts/services/context_menu.js | 6 +- src/public/javascripts/services/tree.js | 13 +- .../javascripts/services/tree_context_menu.js | 351 +++++++----------- .../javascripts/services/tree_keybindings.js | 9 +- 6 files changed, 234 insertions(+), 232 deletions(-) create mode 100644 src/public/javascripts/services/clipboard.js diff --git a/package-lock.json b/package-lock.json index aefb16d33..e0d150f90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "trilium", - "version": "0.31.3", + "version": "0.31.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/public/javascripts/services/clipboard.js b/src/public/javascripts/services/clipboard.js new file mode 100644 index 000000000..245bca80a --- /dev/null +++ b/src/public/javascripts/services/clipboard.js @@ -0,0 +1,85 @@ +import treeUtils from "./tree_utils.js"; +import treeChangesService from "./branches.js"; +import cloningService from "./cloning.js"; +import infoService from "./info.js"; + +let clipboardIds = []; +let clipboardMode = null; + +async function pasteAfter(node) { + if (clipboardMode === 'cut') { + const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); + + await treeChangesService.moveAfterNode(nodes, node); + + clipboardIds = []; + clipboardMode = null; + } + else if (clipboardMode === 'copy') { + for (const noteId of clipboardIds) { + await cloningService.cloneNoteAfter(noteId, node.data.branchId); + } + + // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places + } + else if (clipboardIds.length === 0) { + // just do nothing + } + else { + infoService.throwError("Unrecognized clipboard mode=" + clipboardMode); + } +} + +async function pasteInto(node) { + if (clipboardMode === 'cut') { + const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); + + await treeChangesService.moveToNode(nodes, node); + + await node.setExpanded(true); + + clipboardIds = []; + clipboardMode = null; + } + else if (clipboardMode === 'copy') { + for (const noteId of clipboardIds) { + await cloningService.cloneNoteTo(noteId, node.data.noteId); + } + + await node.setExpanded(true); + + // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places + } + else if (clipboardIds.length === 0) { + // just do nothing + } + else { + infoService.throwError("Unrecognized clipboard mode=" + mode); + } +} + +function copy(nodes) { + clipboardIds = nodes.map(node => node.data.noteId); + clipboardMode = 'copy'; + + infoService.showMessage("Note(s) have been copied into clipboard."); +} + +function cut(nodes) { + clipboardIds = nodes.map(node => node.key); + clipboardMode = 'cut'; + + infoService.showMessage("Note(s) have been cut into clipboard."); +} + +function isEmpty() { + return clipboardIds.length === 0; +} + +export default { + pasteAfter, + pasteInto, + cut, + copy, + isEmpty +} \ No newline at end of file diff --git a/src/public/javascripts/services/context_menu.js b/src/public/javascripts/services/context_menu.js index 16510027f..17f09590a 100644 --- a/src/public/javascripts/services/context_menu.js +++ b/src/public/javascripts/services/context_menu.js @@ -2,7 +2,7 @@ const $contextMenuContainer = $("#context-menu-container"); let dateContextMenuOpenedMs = 0; -function initContextMenu(event, contextMenuItems, selectContextMenuItem) { +async function initContextMenu(event, contextMenu) { event.stopPropagation(); $contextMenuContainer.empty(); @@ -34,7 +34,7 @@ function initContextMenu(event, contextMenuItems, selectContextMenuItem) { e.originalTarget = event.target; - selectContextMenuItem(e, cmd); + contextMenu.selectContextMenuItem(e, cmd); // it's important to stop the propagation especially for sub-menus, otherwise the event // might be handled again by top-level menu @@ -61,7 +61,7 @@ function initContextMenu(event, contextMenuItems, selectContextMenuItem) { } } - addItems($contextMenuContainer, contextMenuItems); + addItems($contextMenuContainer, await contextMenu.getContextMenuItems()); // code below tries to detect when dropdown would overflow from page // in such case we'll position it above click coordinates so it will fit into client diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index e9775470e..4a473bcd4 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -16,6 +16,7 @@ import Branch from '../entities/branch.js'; import NoteShort from '../entities/note_short.js'; import hoistedNoteService from '../services/hoisted_note.js'; import confirmDialog from "../dialogs/confirm.js"; +import TreeContextMenu from "./tree_context_menu.js"; const $tree = $("#tree"); const $createTopLevelNoteButton = $("#create-top-level-note-button"); @@ -485,9 +486,15 @@ function initFancyTree(tree) { }); $tree.on('contextmenu', '.fancytree-node', function(e) { - treeContextMenuService.getContextMenuItems(e).then(([node, contextMenuItems]) => { - contextMenuWidget.initContextMenu(e, contextMenuItems, treeContextMenuService.selectContextMenuItem); - }); + const node = $.ui.fancytree.getNode(e); + + // right click resets selection to just this node + // this is important when e.g. you right click on a note while having different note active + // and then click on delete - obviously you want to delete only that one right-clicked + node.setSelected(true); + clearSelectedNodes(); + + contextMenuWidget.initContextMenu(e, new TreeContextMenu(node)); return false; // blocks default browser right click menu }); diff --git a/src/public/javascripts/services/tree_context_menu.js b/src/public/javascripts/services/tree_context_menu.js index 06a0dad4b..3740f4836 100644 --- a/src/public/javascripts/services/tree_context_menu.js +++ b/src/public/javascripts/services/tree_context_menu.js @@ -1,5 +1,4 @@ import treeService from './tree.js'; -import cloningService from './cloning.js'; import messagingService from './messaging.js'; import protectedSessionService from './protected_session.js'; import treeChangesService from './branches.js'; @@ -7,235 +6,147 @@ import treeUtils from './tree_utils.js'; import branchPrefixDialog from '../dialogs/branch_prefix.js'; import exportDialog from '../dialogs/export.js'; import importDialog from '../dialogs/import.js'; -import infoService from "./info.js"; import treeCache from "./tree_cache.js"; import syncService from "./sync.js"; import hoistedNoteService from './hoisted_note.js'; import noteDetailService from './note_detail.js'; +import clipboard from './clipboard.js'; -let clipboardIds = []; -let clipboardMode = null; - -async function pasteAfter(node) { - if (clipboardMode === 'cut') { - const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); - - await treeChangesService.moveAfterNode(nodes, node); - - clipboardIds = []; - clipboardMode = null; +class TreeContextMenu { + constructor(node) { + this.node = node; } - else if (clipboardMode === 'copy') { - for (const noteId of clipboardIds) { - await cloningService.cloneNoteAfter(noteId, node.data.branchId); + + getNoteTypeItems(baseCmd) { + return [ + { title: "Text", cmd: baseCmd + "_text", uiIcon: "file" }, + { title: "Code", cmd: baseCmd + "_code", uiIcon: "terminal" }, + { title: "Saved search", cmd: baseCmd + "_search", uiIcon: "search-folder" }, + { title: "Relation Map", cmd: baseCmd + "_relation-map", uiIcon: "map" }, + { title: "Render HTML note", cmd: baseCmd + "_render", uiIcon: "play" } + ]; + } + + async getContextMenuItems() { + const branch = await treeCache.getBranch(this.node.data.branchId); + const note = await treeCache.getNote(this.node.data.noteId); + const parentNote = await treeCache.getNote(branch.parentNoteId); + const isNotRoot = note.noteId !== 'root'; + const isHoisted = note.noteId === await hoistedNoteService.getHoistedNoteId(); + + const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNote.type !== 'search'; + const insertChildNoteEnabled = note.type !== 'search'; + + return [ + { title: "Open in new tab", cmd: "openInTab", uiIcon: "empty" }, + { title: "Insert note after Ctrl+O", cmd: "insertNoteAfter", uiIcon: "plus", + items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null, + enabled: insertNoteAfterEnabled }, + { title: "Insert child note Ctrl+P", cmd: "insertChildNote", uiIcon: "plus", + items: insertChildNoteEnabled ? this.getNoteTypeItems("insertChildNote") : null, + enabled: insertChildNoteEnabled }, + { title: "Delete Delete", cmd: "delete", uiIcon: "trash", + enabled: isNotRoot && !isHoisted && parentNote.type !== 'search' }, + { title: "----" }, + isHoisted ? null : { title: "Hoist note Ctrl-H", cmd: "hoist", uiIcon: "empty" }, + !isHoisted || !isNotRoot ? null : { title: "Unhoist note Ctrl-H", cmd: "unhoist", uiIcon: "arrow-up" }, + { title: "Edit branch prefix F2", cmd: "editBranchPrefix", uiIcon: "empty", + enabled: isNotRoot && parentNote.type !== 'search'}, + { title: "----" }, + { title: "Protect subtree", cmd: "protectSubtree", uiIcon: "shield-check" }, + { title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield-close" }, + { title: "----" }, + { title: "Copy / clone Ctrl+C", cmd: "copy", uiIcon: "files", + enabled: isNotRoot }, + { title: "Cut Ctrl+X", cmd: "cut", uiIcon: "scissors", + enabled: isNotRoot }, + { title: "Paste into Ctrl+V", cmd: "pasteInto", uiIcon: "clipboard", + enabled: !clipboard.isEmpty() && note.type !== 'search' }, + { title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard", + enabled: !clipboard.isEmpty() && isNotRoot && parentNote.type !== 'search' }, + { title: "----" }, + { title: "Export", cmd: "export", uiIcon: "empty", + enabled: note.type !== 'search' }, + { title: "Import into note", cmd: "importIntoNote", uiIcon: "empty", + enabled: note.type !== 'search' }, + { title: "----" }, + { title: "Collapse subtree Alt+-", cmd: "collapseSubtree", uiIcon: "align-justify" }, + { title: "Force note sync", cmd: "forceNoteSync", uiIcon: "refresh" }, + { title: "Sort alphabetically Alt+S", cmd: "sortAlphabetically", uiIcon: "empty" } + ].filter(row => row !== null); + } + + async selectContextMenuItem(event, cmd) { + if (cmd === 'openInTab') { + noteDetailService.openInTab(this.node.data.noteId); } + else if (cmd.startsWith("insertNoteAfter")) { + const parentNoteId = this.node.data.parentNoteId; + const isProtected = await treeUtils.getParentProtectedStatus(this.node); + const type = cmd.split("_")[1]; - // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places - } - else if (clipboardIds.length === 0) { - // just do nothing - } - else { - infoService.throwError("Unrecognized clipboard mode=" + clipboardMode); - } -} - -async function pasteInto(node) { - if (clipboardMode === 'cut') { - const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey)); - - await treeChangesService.moveToNode(nodes, node); - - await node.setExpanded(true); - - clipboardIds = []; - clipboardMode = null; - } - else if (clipboardMode === 'copy') { - for (const noteId of clipboardIds) { - await cloningService.cloneNoteTo(noteId, node.data.noteId); + treeService.createNote(this.node, parentNoteId, 'after', { + type: type, + isProtected: isProtected + }); } + else if (cmd.startsWith("insertChildNote")) { + const type = cmd.split("_")[1]; - await node.setExpanded(true); - - // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places - } - else if (clipboardIds.length === 0) { - // just do nothing - } - else { - infoService.throwError("Unrecognized clipboard mode=" + mode); + treeService.createNote(this.node, this.node.data.noteId, 'into', { + type: type, + isProtected: this.node.data.isProtected + }); + } + else if (cmd === "editBranchPrefix") { + branchPrefixDialog.showDialog(this.node); + } + else if (cmd === "protectSubtree") { + protectedSessionService.protectSubtree(this.node.data.noteId, true); + } + else if (cmd === "unprotectSubtree") { + protectedSessionService.protectSubtree(this.node.data.noteId, false); + } + else if (cmd === "copy") { + clipboard.copy(treeService.getSelectedNodes()); + } + else if (cmd === "cut") { + clipboard.cut(treeService.getSelectedNodes()); + } + else if (cmd === "pasteAfter") { + clipboard.pasteAfter(this.node); + } + else if (cmd === "pasteInto") { + clipboard.pasteInto(this.node); + } + else if (cmd === "delete") { + treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); + } + else if (cmd === "export") { + exportDialog.showDialog("subtree"); + } + else if (cmd === "importIntoNote") { + importDialog.showDialog(); + } + else if (cmd === "collapseSubtree") { + treeService.collapseTree(this.node); + } + else if (cmd === "forceNoteSync") { + syncService.forceNoteSync(this.node.data.noteId); + } + else if (cmd === "sortAlphabetically") { + treeService.sortAlphabetically(this.node.data.noteId); + } + else if (cmd === "hoist") { + hoistedNoteService.setHoistedNoteId(this.node.data.noteId); + } + else if (cmd === "unhoist") { + hoistedNoteService.unhoist(); + } + else { + messagingService.logError("Unknown command: " + cmd); + } } } -function copy(nodes) { - clipboardIds = nodes.map(node => node.data.noteId); - clipboardMode = 'copy'; - - infoService.showMessage("Note(s) have been copied into clipboard."); -} - -function cut(nodes) { - clipboardIds = nodes.map(node => node.key); - clipboardMode = 'cut'; - - infoService.showMessage("Note(s) have been cut into clipboard."); -} - -function getNoteTypeItems(baseCmd) { - return [ - { title: "Text", cmd: baseCmd + "_text", uiIcon: "file" }, - { title: "Code", cmd: baseCmd + "_code", uiIcon: "terminal" }, - { title: "Saved search", cmd: baseCmd + "_search", uiIcon: "search-folder" }, - { title: "Relation Map", cmd: baseCmd + "_relation-map", uiIcon: "map" }, - { title: "Render HTML note", cmd: baseCmd + "_render", uiIcon: "play" } - ]; -} - -async function getTopLevelItems(event) { - const node = $.ui.fancytree.getNode(event); - const branch = await treeCache.getBranch(node.data.branchId); - const note = await treeCache.getNote(node.data.noteId); - const parentNote = await treeCache.getNote(branch.parentNoteId); - const isNotRoot = note.noteId !== 'root'; - const isHoisted = note.noteId === await hoistedNoteService.getHoistedNoteId(); - - const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNote.type !== 'search'; - const insertChildNoteEnabled = note.type !== 'search'; - - return [ - { title: "Open in new tab", cmd: "openInTab", uiIcon: "empty" }, - { title: "Insert note after Ctrl+O", cmd: "insertNoteAfter", uiIcon: "plus", - items: insertNoteAfterEnabled ? getNoteTypeItems("insertNoteAfter") : null, - enabled: insertNoteAfterEnabled }, - { title: "Insert child note Ctrl+P", cmd: "insertChildNote", uiIcon: "plus", - items: insertChildNoteEnabled ? getNoteTypeItems("insertChildNote") : null, - enabled: insertChildNoteEnabled }, - { title: "Delete Delete", cmd: "delete", uiIcon: "trash", - enabled: isNotRoot && !isHoisted && parentNote.type !== 'search' }, - { title: "----" }, - isHoisted ? null : { title: "Hoist note Ctrl-H", cmd: "hoist", uiIcon: "empty" }, - !isHoisted || !isNotRoot ? null : { title: "Unhoist note Ctrl-H", cmd: "unhoist", uiIcon: "arrow-up" }, - { title: "Edit branch prefix F2", cmd: "editBranchPrefix", uiIcon: "empty", - enabled: isNotRoot && parentNote.type !== 'search'}, - { title: "----" }, - { title: "Protect subtree", cmd: "protectSubtree", uiIcon: "shield-check" }, - { title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "shield-close" }, - { title: "----" }, - { title: "Copy / clone Ctrl+C", cmd: "copy", uiIcon: "files", - enabled: isNotRoot }, - { title: "Cut Ctrl+X", cmd: "cut", uiIcon: "scissors", - enabled: isNotRoot }, - { title: "Paste into Ctrl+V", cmd: "pasteInto", uiIcon: "clipboard", - enabled: clipboardIds.length > 0 && note.type !== 'search' }, - { title: "Paste after", cmd: "pasteAfter", uiIcon: "clipboard", - enabled: clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search' }, - { title: "----" }, - { title: "Export", cmd: "export", uiIcon: "empty", - enabled: note.type !== 'search' }, - { title: "Import into note", cmd: "importIntoNote", uiIcon: "empty", - enabled: note.type !== 'search' }, - { title: "----" }, - { title: "Collapse subtree Alt+-", cmd: "collapseSubtree", uiIcon: "align-justify" }, - { title: "Force note sync", cmd: "forceNoteSync", uiIcon: "refresh" }, - { title: "Sort alphabetically Alt+S", cmd: "sortAlphabetically", uiIcon: "empty" } - ].filter(row => row !== null); -} - -async function getContextMenuItems(event) { - const items = await getTopLevelItems(event); - - const node = $.ui.fancytree.getNode(event); - - // right click resets selection to just this node - // this is important when e.g. you right click on a note while having different note active - // and then click on delete - obviously you want to delete only that one right-clicked - node.setSelected(true); - treeService.clearSelectedNodes(); - - return [node, items]; -} - -async function selectContextMenuItem(event, cmd) { - // context menu is always triggered on current node - const node = treeService.getActiveNode(); - - if (cmd === 'openInTab') { - noteDetailService.openInTab(node.data.noteId); - } - else if (cmd.startsWith("insertNoteAfter")) { - const parentNoteId = node.data.parentNoteId; - const isProtected = await treeUtils.getParentProtectedStatus(node); - const type = cmd.split("_")[1]; - - treeService.createNote(node, parentNoteId, 'after', { - type: type, - isProtected: isProtected - }); - } - else if (cmd.startsWith("insertChildNote")) { - const type = cmd.split("_")[1]; - - treeService.createNote(node, node.data.noteId, 'into', { - type: type, - isProtected: node.data.isProtected - }); - } - else if (cmd === "editBranchPrefix") { - branchPrefixDialog.showDialog(node); - } - else if (cmd === "protectSubtree") { - protectedSessionService.protectSubtree(node.data.noteId, true); - } - else if (cmd === "unprotectSubtree") { - protectedSessionService.protectSubtree(node.data.noteId, false); - } - else if (cmd === "copy") { - copy(treeService.getSelectedNodes()); - } - else if (cmd === "cut") { - cut(treeService.getSelectedNodes()); - } - else if (cmd === "pasteAfter") { - pasteAfter(node); - } - else if (cmd === "pasteInto") { - pasteInto(node); - } - else if (cmd === "delete") { - treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); - } - else if (cmd === "export") { - exportDialog.showDialog("subtree"); - } - else if (cmd === "importIntoNote") { - importDialog.showDialog(); - } - else if (cmd === "collapseSubtree") { - treeService.collapseTree(node); - } - else if (cmd === "forceNoteSync") { - syncService.forceNoteSync(node.data.noteId); - } - else if (cmd === "sortAlphabetically") { - treeService.sortAlphabetically(node.data.noteId); - } - else if (cmd === "hoist") { - hoistedNoteService.setHoistedNoteId(node.data.noteId); - } - else if (cmd === "unhoist") { - hoistedNoteService.unhoist(); - } - else { - messagingService.logError("Unknown command: " + cmd); - } -} - -export default { - pasteAfter, - pasteInto, - cut, - copy, - getContextMenuItems, - selectContextMenuItem -}; \ No newline at end of file +export default TreeContextMenu; \ No newline at end of file diff --git a/src/public/javascripts/services/tree_keybindings.js b/src/public/javascripts/services/tree_keybindings.js index 647ab306c..6f288d632 100644 --- a/src/public/javascripts/services/tree_keybindings.js +++ b/src/public/javascripts/services/tree_keybindings.js @@ -1,10 +1,9 @@ import noteDetailService from "./note_detail.js"; -import utils from "./utils.js"; import treeChangesService from "./branches.js"; -import contextMenuService from "./tree_context_menu.js"; import treeService from "./tree.js"; import editBranchPrefixDialog from "../dialogs/branch_prefix.js"; import hoistedNoteService from "./hoisted_note.js"; +import clipboard from "./clipboard.js"; const keyBindings = { "del": node => { @@ -90,17 +89,17 @@ const keyBindings = { return false; }, "ctrl+c": () => { - contextMenuService.copy(treeService.getSelectedNodes()); + clipboard.copy(treeService.getSelectedNodes()); return false; }, "ctrl+x": () => { - contextMenuService.cut(treeService.getSelectedNodes()); + clipboard.cut(treeService.getSelectedNodes()); return false; }, "ctrl+v": node => { - contextMenuService.pasteInto(node); + clipboard.pasteInto(node); return false; },