From 24dfe0fa984d3144be5a646722b92f1e545f385e Mon Sep 17 00:00:00 2001 From: azivner Date: Tue, 6 Nov 2018 12:46:29 +0100 Subject: [PATCH] replace note tree context menu with bootstrap dropdown, #203 --- src/public/javascripts/services/tree.js | 45 +- .../javascripts/services/tree_context_menu.js | 248 +++---- src/public/libraries/jquery.ui-contextmenu.js | 631 ------------------ src/public/stylesheets/style.css | 21 +- src/views/index.ejs | 6 + 5 files changed, 188 insertions(+), 763 deletions(-) delete mode 100644 src/public/libraries/jquery.ui-contextmenu.js diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 05b21093e..4253dbd7b 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -4,7 +4,6 @@ import linkService from './link.js'; import messagingService from './messaging.js'; import noteDetailService from './note_detail.js'; import protectedSessionHolder from './protected_session_holder.js'; -import treeChangesService from './branches.js'; import treeUtils from './tree_utils.js'; import utils from './utils.js'; import server from './server.js'; @@ -16,6 +15,7 @@ import Branch from '../entities/branch.js'; import NoteShort from '../entities/note_short.js'; const $tree = $("#tree"); +const $treeContextMenu = $("#tree-context-menu"); const $createTopLevelNoteButton = $("#create-top-level-note-button"); const $collapseTreeButton = $("#collapse-tree-button"); const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button"); @@ -378,7 +378,48 @@ function initFancyTree(tree) { } }); - $tree.contextmenu(treeContextMenuService.contextMenuOptions); + $treeContextMenu.on('click', '.dropdown-item', function(e) { + const cmd = $(e.target).prop("data-cmd"); + + treeContextMenuService.selectContextMenuItem(e, cmd); + }); + + async function openContextMenu(e) { + $treeContextMenu.empty(); + + const contextMenuItems = await treeContextMenuService.getContextMenuItems(e); + + for (const item of contextMenuItems) { + if (item.title === '----') { + $treeContextMenu.append($("
").addClass("dropdown-divider")); + } else { + const $item = $("") + .addClass("dropdown-item") + .prop("data-cmd", item.cmd) + .append(item.title); + + if (item.enabled !== undefined && !item.enabled) { + $item.addClass("disabled"); + } + + $treeContextMenu.append($item); + } + } + + $treeContextMenu.css({ + display: "block", + top: e.pageY - 10, + left: e.pageX - 40 + }).addClass("show"); + } + + $(document).click(() => $(".context-menu").hide()); + + $tree.on('contextmenu', '.fancytree-node', function(e) { + openContextMenu(e); + + return false; // blocks default browser right click menu + }); } function getTree() { diff --git a/src/public/javascripts/services/tree_context_menu.js b/src/public/javascripts/services/tree_context_menu.js index a05c579f4..653f81119 100644 --- a/src/public/javascripts/services/tree_context_menu.js +++ b/src/public/javascripts/services/tree_context_menu.js @@ -76,135 +76,137 @@ function cut(nodes) { infoService.showMessage("Note(s) have been cut into clipboard."); } -const contextMenuOptions = { - delegate: "span.fancytree-title", - autoFocus: true, - menu: [ - {title: "Insert note here Ctrl+O", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"}, - {title: "Insert child note Ctrl+P", cmd: "insertChildNote", uiIcon: "ui-icon-plus"}, - {title: "Delete", cmd: "delete", uiIcon: "ui-icon-trash"}, - {title: "----"}, - {title: "Edit branch prefix F2", cmd: "editBranchPrefix", uiIcon: "ui-icon-pencil"}, - {title: "----"}, - {title: "Protect subtree", cmd: "protectSubtree", uiIcon: "ui-icon-locked"}, - {title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "ui-icon-unlocked"}, - {title: "----"}, - {title: "Copy / clone Ctrl+C", cmd: "copy", uiIcon: "ui-icon-copy"}, - {title: "Cut Ctrl+X", cmd: "cut", uiIcon: "ui-icon-scissors"}, - {title: "Paste into Ctrl+V", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, - {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, - {title: "----"}, - {title: "Export subtree", cmd: "exportSubtree", uiIcon: " ui-icon-arrowthick-1-ne", children: [ - {title: "Native Tar", cmd: "exportSubtreeToTar"}, - {title: "OPML", cmd: "exportSubtreeToOpml"}, - {title: "Markdown", cmd: "exportSubtreeToMarkdown"} - ]}, - {title: "Import into note (tar, opml, md, enex)", cmd: "importIntoNote", uiIcon: "ui-icon-arrowthick-1-sw"}, - {title: "----"}, - {title: "Collapse subtree Alt+-", cmd: "collapseSubtree", uiIcon: "ui-icon-minus"}, - {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, - {title: "Sort alphabetically Alt+S", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} - ], - beforeOpen: async (event, ui) => { - const node = $.ui.fancytree.getNode(ui.target); - 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 contextMenuItems = [ + {title: "Insert note here Ctrl+O", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"}, + {title: "Insert child note Ctrl+P", cmd: "insertChildNote", uiIcon: "ui-icon-plus"}, + {title: "Delete", cmd: "delete", uiIcon: "ui-icon-trash"}, + {title: "----"}, + {title: "Edit branch prefix F2", cmd: "editBranchPrefix", uiIcon: "ui-icon-pencil"}, + {title: "----"}, + {title: "Protect subtree", cmd: "protectSubtree", uiIcon: "ui-icon-locked"}, + {title: "Unprotect subtree", cmd: "unprotectSubtree", uiIcon: "ui-icon-unlocked"}, + {title: "----"}, + {title: "Copy / clone Ctrl+C", cmd: "copy", uiIcon: "ui-icon-copy"}, + {title: "Cut Ctrl+X", cmd: "cut", uiIcon: "ui-icon-scissors"}, + {title: "Paste into Ctrl+V", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, + {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, + {title: "----"}, + {title: "Export subtree", cmd: "exportSubtree", uiIcon: " ui-icon-arrowthick-1-ne"}, + {title: "Import into note (tar, opml, md, enex)", cmd: "importIntoNote", uiIcon: "ui-icon-arrowthick-1-sw"}, + {title: "----"}, + {title: "Collapse subtree Alt+-", cmd: "collapseSubtree", uiIcon: "ui-icon-minus"}, + {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, + {title: "Sort alphabetically Alt+S", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} +]; - // Modify menu entries depending on node status - $tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search'); - $tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search'); - $tree.contextmenu("enableEntry", "delete", isNotRoot && parentNote.type !== 'search'); - $tree.contextmenu("enableEntry", "copy", isNotRoot); - $tree.contextmenu("enableEntry", "cut", isNotRoot); - $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search'); - $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search'); - $tree.contextmenu("enableEntry", "importIntoNote", note.type !== 'search'); - $tree.contextmenu("enableEntry", "exportSubtree", note.type !== 'search'); - $tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search'); - - // Activate node on right-click - node.setActive(); - - // 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(); - - // Disable tree keyboard handling - ui.menu.prevKeyboard = node.tree.options.keyboard; - node.tree.options.keyboard = false; - }, - close: (event, ui) => {}, - select: (event, ui) => { - const node = $.ui.fancytree.getNode(ui.target); - - if (ui.cmd === "insertNoteHere") { - const parentNoteId = node.data.parentNoteId; - const isProtected = treeUtils.getParentProtectedStatus(node); - - treeService.createNote(node, parentNoteId, 'after', isProtected); - } - else if (ui.cmd === "insertChildNote") { - treeService.createNote(node, node.data.noteId, 'into'); - } - else if (ui.cmd === "editBranchPrefix") { - branchPrefixDialog.showDialog(node); - } - else if (ui.cmd === "protectSubtree") { - protectedSessionService.protectSubtree(node.data.noteId, true); - } - else if (ui.cmd === "unprotectSubtree") { - protectedSessionService.protectSubtree(node.data.noteId, false); - } - else if (ui.cmd === "copy") { - copy(treeService.getSelectedNodes()); - } - else if (ui.cmd === "cut") { - cut(treeService.getSelectedNodes()); - } - else if (ui.cmd === "pasteAfter") { - pasteAfter(node); - } - else if (ui.cmd === "pasteInto") { - pasteInto(node); - } - else if (ui.cmd === "delete") { - treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); - } - else if (ui.cmd === "exportSubtreeToTar") { - exportService.exportSubtree(node.data.branchId, 'tar'); - } - else if (ui.cmd === "exportSubtreeToOpml") { - exportService.exportSubtree(node.data.branchId, 'opml'); - } - else if (ui.cmd === "exportSubtreeToMarkdown") { - exportService.exportSubtree(node.data.branchId, 'markdown'); - } - else if (ui.cmd === "importIntoNote") { - exportService.importIntoNote(node.data.noteId); - } - else if (ui.cmd === "collapseSubtree") { - treeService.collapseTree(node); - } - else if (ui.cmd === "forceNoteSync") { - syncService.forceNoteSync(node.data.noteId); - } - else if (ui.cmd === "sortAlphabetically") { - treeService.sortAlphabetically(node.data.noteId); - } - else { - messagingService.logError("Unknown command: " + ui.cmd); - } +function enableItem(cmd, enabled) { + const item = contextMenuItems.find(item => item.cmd === cmd); + + if (!item) { + throw new Error(`Command ${cmd} has not been found!`); } -}; + + item.enabled = enabled; +} + +async function getContextMenuItems(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'; + + // Modify menu entries depending on node status + enableItem("insertNoteHere", isNotRoot && parentNote.type !== 'search'); + enableItem("insertChildNote", note.type !== 'search'); + enableItem("delete", isNotRoot && parentNote.type !== 'search'); + enableItem("copy", isNotRoot); + enableItem("cut", isNotRoot); + enableItem("pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search'); + enableItem("pasteInto", clipboardIds.length > 0 && note.type !== 'search'); + enableItem("importIntoNote", note.type !== 'search'); + enableItem("exportSubtree", note.type !== 'search'); + enableItem("editBranchPrefix", isNotRoot && parentNote.type !== 'search'); + + // Activate node on right-click + node.setActive(); + + // 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 contextMenuItems; +} + +function selectContextMenuItem(event, cmd) { + const node = $.ui.fancytree.getNode(event); + + if (cmd === "insertNoteHere") { + const parentNoteId = node.data.parentNoteId; + const isProtected = treeUtils.getParentProtectedStatus(node); + + treeService.createNote(node, parentNoteId, 'after', isProtected); + } + else if (cmd === "insertChildNote") { + treeService.createNote(node, node.data.noteId, 'into'); + } + 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 === "exportSubtreeToTar") { + exportService.exportSubtree(node.data.branchId, 'tar'); + } + else if (cmd === "exportSubtreeToOpml") { + exportService.exportSubtree(node.data.branchId, 'opml'); + } + else if (cmd === "exportSubtreeToMarkdown") { + exportService.exportSubtree(node.data.branchId, 'markdown'); + } + else if (cmd === "importIntoNote") { + exportService.importIntoNote(node.data.noteId); + } + 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 { + messagingService.logError("Unknown command: " + cmd); + } +} export default { pasteAfter, pasteInto, cut, copy, - contextMenuOptions + getContextMenuItems, + selectContextMenuItem }; \ No newline at end of file diff --git a/src/public/libraries/jquery.ui-contextmenu.js b/src/public/libraries/jquery.ui-contextmenu.js deleted file mode 100644 index 8b1102887..000000000 --- a/src/public/libraries/jquery.ui-contextmenu.js +++ /dev/null @@ -1,631 +0,0 @@ -/******************************************************************************* - * jquery.ui-contextmenu.js plugin. - * - * jQuery plugin that provides a context menu (based on the jQueryUI menu widget). - * - * @see https://github.com/mar10/jquery-ui-contextmenu - * - * Copyright (c) 2013-2017, Martin Wendt (http://wwWendt.de). Licensed MIT. - */ - -(function( factory ) { - "use strict"; - if ( typeof define === "function" && define.amd ) { - // AMD. Register as an anonymous module. - define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory ); - } else { - // Browser globals - factory( jQuery ); - } -}(function( $ ) { - -"use strict"; - -var supportSelectstart = "onselectstart" in document.createElement("div"), - match = $.ui.menu.version.match(/^(\d)\.(\d+)/), - uiVersion = { - major: parseInt(match[1], 10), - minor: parseInt(match[2], 10) - }, - isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ), - isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 ); - -$.widget("moogle.contextmenu", { - version: "@VERSION", - options: { - addClass: "ui-contextmenu", // Add this class to the outer
    - closeOnWindowBlur: true, // Close menu when window loses focus - autoFocus: false, // Set keyboard focus to first entry on open - autoTrigger: true, // open menu on browser's `contextmenu` event - delegate: null, // selector - hide: { effect: "fadeOut", duration: "fast" }, - ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents - menu: null, // selector or jQuery pointing to
      , or a definition hash - position: null, // popup positon - preventContextMenuForPopup: false, // prevent opening the browser's system - // context menu on menu entries - preventSelect: false, // disable text selection of target - show: { effect: "slideDown", duration: "fast" }, - taphold: false, // open menu on taphold events (requires external plugins) - uiMenuOptions: {}, // Additional options, used when UI Menu is created - // Events: - beforeOpen: $.noop, // menu about to open; return `false` to prevent opening - blur: $.noop, // menu option lost focus - close: $.noop, // menu was closed - create: $.noop, // menu was initialized - createMenu: $.noop, // menu was initialized (original UI Menu) - focus: $.noop, // menu option got focus - open: $.noop, // menu was opened - select: $.noop // menu option was selected; return `false` to prevent closing - }, - /** Constructor */ - _create: function() { - var cssText, eventNames, targetId, - opts = this.options; - - this.$headStyle = null; - this.$menu = null; - this.menuIsTemp = false; - this.currentTarget = null; - this.extraData = {}; - this.previousFocus = null; - - if (opts.delegate == null) { - $.error("ui-contextmenu: Missing required option `delegate`."); - } - if (opts.preventSelect) { - // Create a global style for all potential menu targets - // If the contextmenu was bound to `document`, we apply the - // selector relative to the tag instead - targetId = ($(this.element).is(document) ? $("body") - : this.element).uniqueId().attr("id"); - cssText = "#" + targetId + " " + opts.delegate + " { " + - "-webkit-user-select: none; " + - "-khtml-user-select: none; " + - "-moz-user-select: none; " + - "-ms-user-select: none; " + - "user-select: none; " + - "}"; - this.$headStyle = $("