mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	replace note tree context menu with bootstrap dropdown, #203
This commit is contained in:
		| @@ -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($("<div>").addClass("dropdown-divider")); | ||||
|             } else { | ||||
|                 const $item = $("<a>") | ||||
|                     .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() { | ||||
|   | ||||
| @@ -76,10 +76,7 @@ function cut(nodes) { | ||||
|     infoService.showMessage("Note(s) have been cut into clipboard."); | ||||
| } | ||||
|  | ||||
| const contextMenuOptions = { | ||||
|     delegate: "span.fancytree-title", | ||||
|     autoFocus: true, | ||||
|     menu: [ | ||||
| const contextMenuItems = [ | ||||
|     {title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"}, | ||||
|     {title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"}, | ||||
|     {title: "Delete", cmd: "delete", uiIcon: "ui-icon-trash"}, | ||||
| @@ -94,35 +91,42 @@ const contextMenuOptions = { | ||||
|     {title: "Paste into <kbd>Ctrl+V</kbd>", 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: "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 <kbd>Alt+-</kbd>", cmd: "collapseSubtree", uiIcon: "ui-icon-minus"}, | ||||
|     {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, | ||||
|     {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} | ||||
|     ], | ||||
|     beforeOpen: async (event, ui) => { | ||||
|         const node = $.ui.fancytree.getNode(ui.target); | ||||
| ]; | ||||
|  | ||||
| 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 | ||||
|         $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'); | ||||
|     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(); | ||||
| @@ -133,78 +137,76 @@ const contextMenuOptions = { | ||||
|     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); | ||||
|     return contextMenuItems; | ||||
| } | ||||
|  | ||||
|         if (ui.cmd === "insertNoteHere") { | ||||
| 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 (ui.cmd === "insertChildNote") { | ||||
|     else if (cmd === "insertChildNote") { | ||||
|         treeService.createNote(node, node.data.noteId, 'into'); | ||||
|     } | ||||
|         else if (ui.cmd === "editBranchPrefix") { | ||||
|     else if (cmd === "editBranchPrefix") { | ||||
|         branchPrefixDialog.showDialog(node); | ||||
|     } | ||||
|         else if (ui.cmd === "protectSubtree") { | ||||
|     else if (cmd === "protectSubtree") { | ||||
|         protectedSessionService.protectSubtree(node.data.noteId, true); | ||||
|     } | ||||
|         else if (ui.cmd === "unprotectSubtree") { | ||||
|     else if (cmd === "unprotectSubtree") { | ||||
|         protectedSessionService.protectSubtree(node.data.noteId, false); | ||||
|     } | ||||
|         else if (ui.cmd === "copy") { | ||||
|     else if (cmd === "copy") { | ||||
|         copy(treeService.getSelectedNodes()); | ||||
|     } | ||||
|         else if (ui.cmd === "cut") { | ||||
|     else if (cmd === "cut") { | ||||
|         cut(treeService.getSelectedNodes()); | ||||
|     } | ||||
|         else if (ui.cmd === "pasteAfter") { | ||||
|     else if (cmd === "pasteAfter") { | ||||
|         pasteAfter(node); | ||||
|     } | ||||
|         else if (ui.cmd === "pasteInto") { | ||||
|     else if (cmd === "pasteInto") { | ||||
|         pasteInto(node); | ||||
|     } | ||||
|         else if (ui.cmd === "delete") { | ||||
|     else if (cmd === "delete") { | ||||
|         treeChangesService.deleteNodes(treeService.getSelectedNodes(true)); | ||||
|     } | ||||
|         else if (ui.cmd === "exportSubtreeToTar") { | ||||
|     else if (cmd === "exportSubtreeToTar") { | ||||
|         exportService.exportSubtree(node.data.branchId, 'tar'); | ||||
|     } | ||||
|         else if (ui.cmd === "exportSubtreeToOpml") { | ||||
|     else if (cmd === "exportSubtreeToOpml") { | ||||
|         exportService.exportSubtree(node.data.branchId, 'opml'); | ||||
|     } | ||||
|         else if (ui.cmd === "exportSubtreeToMarkdown") { | ||||
|     else if (cmd === "exportSubtreeToMarkdown") { | ||||
|         exportService.exportSubtree(node.data.branchId, 'markdown'); | ||||
|     } | ||||
|         else if (ui.cmd === "importIntoNote") { | ||||
|     else if (cmd === "importIntoNote") { | ||||
|         exportService.importIntoNote(node.data.noteId); | ||||
|     } | ||||
|         else if (ui.cmd === "collapseSubtree") { | ||||
|     else if (cmd === "collapseSubtree") { | ||||
|         treeService.collapseTree(node); | ||||
|     } | ||||
|         else if (ui.cmd === "forceNoteSync") { | ||||
|     else if (cmd === "forceNoteSync") { | ||||
|         syncService.forceNoteSync(node.data.noteId); | ||||
|     } | ||||
|         else if (ui.cmd === "sortAlphabetically") { | ||||
|     else if (cmd === "sortAlphabetically") { | ||||
|         treeService.sortAlphabetically(node.data.noteId); | ||||
|     } | ||||
|     else { | ||||
|             messagingService.logError("Unknown command: " + ui.cmd); | ||||
|         messagingService.logError("Unknown command: " + cmd); | ||||
|     } | ||||
| } | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     pasteAfter, | ||||
|     pasteInto, | ||||
|     cut, | ||||
|     copy, | ||||
|     contextMenuOptions | ||||
|     getContextMenuItems, | ||||
|     selectContextMenuItem | ||||
| }; | ||||
| @@ -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 <ul> | ||||
| 		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 <UL>, 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 <body> 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 = $("<style class='moogle-contextmenu-style' />") | ||||
| 				.prop("type", "text/css") | ||||
| 				.appendTo("head"); | ||||
|  | ||||
| 			try { | ||||
| 				this.$headStyle.html(cssText); | ||||
| 			} catch ( e ) { | ||||
| 				// issue #47: fix for IE 6-8 | ||||
| 				this.$headStyle[0].styleSheet.cssText = cssText; | ||||
| 			} | ||||
| 			// TODO: the selectstart is not supported by FF? | ||||
| 			if (supportSelectstart) { | ||||
| 				this.element.on("selectstart" + this.eventNamespace, opts.delegate, | ||||
| 									  function(event) { | ||||
| 					event.preventDefault(); | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 		this._createUiMenu(opts.menu); | ||||
|  | ||||
| 		eventNames = "contextmenu" + this.eventNamespace; | ||||
| 		if (opts.taphold) { | ||||
| 			eventNames += " taphold" + this.eventNamespace; | ||||
| 		} | ||||
| 		this.element.on(eventNames, opts.delegate, $.proxy(this._openMenu, this)); | ||||
| 	}, | ||||
| 	/** Destructor, called on $().contextmenu("destroy"). */ | ||||
| 	_destroy: function() { | ||||
| 		this.element.off(this.eventNamespace); | ||||
|  | ||||
| 		this._createUiMenu(null); | ||||
|  | ||||
| 		if (this.$headStyle) { | ||||
| 			this.$headStyle.remove(); | ||||
| 			this.$headStyle = null; | ||||
| 		} | ||||
| 	}, | ||||
| 	/** (Re)Create jQuery UI Menu. */ | ||||
| 	_createUiMenu: function(menuDef) { | ||||
| 		var ct, ed, | ||||
| 			opts = this.options; | ||||
|  | ||||
| 		// Remove temporary <ul> if any | ||||
| 		if (this.isOpen()) { | ||||
| 			// #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target | ||||
| 			ct = this.currentTarget; | ||||
| 			ed = this.extraData; | ||||
| 			// close without animation, to force async mode | ||||
| 			this._closeMenu(true); | ||||
| 			this.currentTarget = ct; | ||||
| 			this.extraData = ed; | ||||
| 		} | ||||
| 		if (this.menuIsTemp) { | ||||
| 			this.$menu.remove(); // this will also destroy ui.menu | ||||
| 		} else if (this.$menu) { | ||||
| 			this.$menu | ||||
| 				.menu("destroy") | ||||
| 				.removeClass(this.options.addClass) | ||||
| 				.hide(); | ||||
| 		} | ||||
| 		this.$menu = null; | ||||
| 		this.menuIsTemp = false; | ||||
| 		// If a menu definition array was passed, create a hidden <ul> | ||||
| 		// and generate the structure now | ||||
| 		if ( !menuDef ) { | ||||
| 			return; | ||||
| 		} else if ($.isArray(menuDef)) { | ||||
| 			this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef); | ||||
| 			this.menuIsTemp = true; | ||||
| 		}else if ( typeof menuDef === "string" ) { | ||||
| 			this.$menu = $(menuDef); | ||||
| 		} else { | ||||
| 			this.$menu = menuDef; | ||||
| 		} | ||||
| 		// Create - but hide - the jQuery UI Menu widget | ||||
| 		this.$menu | ||||
| 			.hide() | ||||
| 			.addClass(opts.addClass) | ||||
| 			// Create a menu instance that delegates events to our widget | ||||
| 			.menu($.extend(true, {}, opts.uiMenuOptions, { | ||||
| 				items: "> :not(.ui-widget-header)", | ||||
| 				blur: $.proxy(opts.blur, this), | ||||
| 				create: $.proxy(opts.createMenu, this), | ||||
| 				focus: $.proxy(opts.focus, this), | ||||
| 				select: $.proxy(function(event, ui) { | ||||
| 					// User selected a menu entry | ||||
| 					var retval, | ||||
| 						isParent = $.moogle.contextmenu.isMenu(ui.item), | ||||
| 						actionHandler = ui.item.data("actionHandler"); | ||||
|  | ||||
| 					ui.cmd = ui.item.attr("data-command"); | ||||
| 					ui.target = $(this.currentTarget); | ||||
| 					ui.extraData = this.extraData; | ||||
| 					// ignore clicks, if they only open a sub-menu | ||||
| 					if ( !isParent || !opts.ignoreParentSelect) { | ||||
| 						retval = this._trigger.call(this, "select", event, ui); | ||||
| 						if ( actionHandler ) { | ||||
| 							retval = actionHandler.call(this, event, ui); | ||||
| 						} | ||||
| 						if ( retval !== false ) { | ||||
| 							this._closeMenu.call(this); | ||||
| 						} | ||||
| 						event.preventDefault(); | ||||
| 					} | ||||
| 				}, this) | ||||
| 			})); | ||||
| 	}, | ||||
| 	/** Open popup (called on 'contextmenu' event). */ | ||||
| 	_openMenu: function(event, recursive) { | ||||
| 		var res, promise, ui, | ||||
| 			opts = this.options, | ||||
| 			posOption = opts.position, | ||||
| 			self = this, | ||||
| 			manualTrigger = !!event.isTrigger; | ||||
|  | ||||
| 		if ( !opts.autoTrigger && !manualTrigger ) { | ||||
| 			// ignore browser's `contextmenu` events | ||||
| 			return; | ||||
| 		} | ||||
| 		// Prevent browser from opening the system context menu | ||||
| 		event.preventDefault(); | ||||
|  | ||||
| 		this.currentTarget = event.target; | ||||
| 		this.extraData = event._extraData || {}; | ||||
|  | ||||
| 		ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData, | ||||
| 			   originalEvent: event, result: null }; | ||||
|  | ||||
| 		if ( !recursive ) { | ||||
| 			res = this._trigger("beforeOpen", event, ui); | ||||
| 			promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null; | ||||
| 			ui.result = null; | ||||
| 			if ( res === false ) { | ||||
| 				this.currentTarget = null; | ||||
| 				return false; | ||||
| 			} else if ( promise ) { | ||||
| 				// Handler returned a Deferred or Promise. Delay menu open until | ||||
| 				// the promise is resolved | ||||
| 				promise.done(function() { | ||||
| 					self._openMenu(event, true); | ||||
| 				}); | ||||
| 				this.currentTarget = null; | ||||
| 				return false; | ||||
| 			} | ||||
| 			ui.menu = this.$menu; // Might have changed in beforeOpen | ||||
| 		} | ||||
|  | ||||
| 		// Register global event handlers that close the dropdown-menu | ||||
| 		$(document).on("keydown" + this.eventNamespace, function(event) { | ||||
| 			if ( event.which === $.ui.keyCode.ESCAPE ) { | ||||
| 				self._closeMenu(); | ||||
| 			} | ||||
| 		}).on("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace, | ||||
| 				function(event) { | ||||
| 			// Close menu when clicked outside menu | ||||
| 			if ( !$(event.target).closest(".ui-menu-item").length ) { | ||||
| 				self._closeMenu(); | ||||
| 			} | ||||
| 		}); | ||||
| 		$(window).on("blur" + this.eventNamespace, function(event) { | ||||
| 			if ( opts.closeOnWindowBlur ) { | ||||
| 				self._closeMenu(); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		// required for custom positioning (issue #18 and #13). | ||||
| 		if ($.isFunction(posOption)) { | ||||
| 			posOption = posOption(event, ui); | ||||
| 		} | ||||
| 		posOption = $.extend({ | ||||
| 			my: "left top", | ||||
| 			at: "left bottom", | ||||
| 			// if called by 'open' method, event does not have pageX/Y | ||||
| 			of: (event.pageX === undefined) ? event.target : event, | ||||
| 			collision: "fit" | ||||
| 		}, posOption); | ||||
|  | ||||
| 		// Update entry statuses from callbacks | ||||
| 		this._updateEntries(this.$menu); | ||||
|  | ||||
| 		// Finally display the popup | ||||
| 		this.$menu | ||||
| 			.show() // required to fix positioning error | ||||
| 			.css({ | ||||
| 				position: "absolute", | ||||
| 				left: 0, | ||||
| 				top: 0 | ||||
| 			}).position(posOption) | ||||
| 			.hide(); // hide again, so we can apply nice effects | ||||
|  | ||||
| 		if ( opts.preventContextMenuForPopup ) { | ||||
| 			this.$menu.on("contextmenu" + this.eventNamespace, function(event) { | ||||
| 				event.preventDefault(); | ||||
| 			}); | ||||
| 		} | ||||
| 		this._show(this.$menu, opts.show, function() { | ||||
| 			var $first; | ||||
|  | ||||
| 			// Set focus to first active menu entry | ||||
| 			if ( opts.autoFocus ) { | ||||
| 				self.previousFocus = $(event.target); | ||||
| 				// self.$menu.focus(); | ||||
| 				$first = self.$menu | ||||
| 					.children("li.ui-menu-item") | ||||
| 					.not(".ui-state-disabled") | ||||
| 					.first(); | ||||
| 				self.$menu.menu("focus", null, $first).focus(); | ||||
| 			} | ||||
| 			self._trigger.call(self, "open", event, ui); | ||||
| 		}); | ||||
| 	}, | ||||
| 	/** Close popup. */ | ||||
| 	_closeMenu: function(immediately) { | ||||
| 		var self = this, | ||||
| 			hideOpts = immediately ? false : this.options.hide, | ||||
| 			ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData }; | ||||
|  | ||||
| 		// Note: we don't want to unbind the 'contextmenu' event | ||||
| 		$(document) | ||||
| 			.off("mousedown" + this.eventNamespace) | ||||
| 			.off("touchstart" + this.eventNamespace) | ||||
| 			.off("keydown" + this.eventNamespace); | ||||
| 		$(window) | ||||
| 			.off("blur" + this.eventNamespace); | ||||
|  | ||||
| 		self.currentTarget = null; // issue #44 after hide animation is too late | ||||
| 		self.extraData = {}; | ||||
| 		if ( this.$menu ) { // #88: widget might have been destroyed already | ||||
| 			this.$menu | ||||
| 				.off("contextmenu" + this.eventNamespace); | ||||
| 			this._hide(this.$menu, hideOpts, function() { | ||||
| 				if ( self.previousFocus ) { | ||||
| 					self.previousFocus.focus(); | ||||
| 					self.previousFocus = null; | ||||
| 				} | ||||
| 				self._trigger("close", null, ui); | ||||
| 			}); | ||||
| 		} else { | ||||
| 			self._trigger("close", null, ui); | ||||
| 		} | ||||
| 	}, | ||||
| 	/** Handle $().contextmenu("option", key, value) calls. */ | ||||
| 	_setOption: function(key, value) { | ||||
| 		switch (key) { | ||||
| 		case "menu": | ||||
| 			this.replaceMenu(value); | ||||
| 			break; | ||||
| 		} | ||||
| 		$.Widget.prototype._setOption.apply(this, arguments); | ||||
| 	}, | ||||
| 	/** Return ui-menu entry (<LI> tag). */ | ||||
| 	_getMenuEntry: function(cmd) { | ||||
| 		return this.$menu.find("li[data-command=" + cmd + "]"); | ||||
| 	}, | ||||
| 	/** Close context menu. */ | ||||
| 	close: function() { | ||||
| 		if (this.isOpen()) { | ||||
| 			this._closeMenu(); | ||||
| 		} | ||||
| 	}, | ||||
| 	/* Apply status callbacks when menu is opened. */ | ||||
| 	_updateEntries: function() { | ||||
| 		var self = this, | ||||
| 			ui = { | ||||
| 				menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData }; | ||||
|  | ||||
| 		$.each(this.$menu.find(".ui-menu-item"), function(i, o) { | ||||
| 			var $entry = $(o), | ||||
| 				fn = $entry.data("disabledHandler"), | ||||
| 				res = fn ? fn({ type: "disabled" }, ui) : null; | ||||
|  | ||||
| 			ui.item = $entry; | ||||
| 			ui.cmd = $entry.attr("data-command"); | ||||
| 			// Evaluate `disabled()` callback | ||||
| 			if ( res != null ) { | ||||
| 				self.enableEntry(ui.cmd, !res); | ||||
| 				self.showEntry(ui.cmd, res !== "hide"); | ||||
| 			} | ||||
| 			// Evaluate `title()` callback | ||||
| 			fn = $entry.data("titleHandler"), | ||||
| 			res = fn ? fn({ type: "title" }, ui) : null; | ||||
| 			if ( res != null ) { | ||||
| 				self.setTitle(ui.cmd, "" + res); | ||||
| 			} | ||||
| 			// Evaluate `tooltip()` callback | ||||
| 			fn = $entry.data("tooltipHandler"), | ||||
| 			res = fn ? fn({ type: "tooltip" }, ui) : null; | ||||
| 			if ( res != null ) { | ||||
| 				$entry.attr("title", "" + res); | ||||
| 			} | ||||
| 		}); | ||||
| 	}, | ||||
| 	/** Enable or disable the menu command. */ | ||||
| 	enableEntry: function(cmd, flag) { | ||||
| 		this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false)); | ||||
| 	}, | ||||
| 	/** Return ui-menu entry (LI tag) as jQuery object. */ | ||||
| 	getEntry: function(cmd) { | ||||
| 		return this._getMenuEntry(cmd); | ||||
| 	}, | ||||
| 	/** Return ui-menu entry wrapper as jQuery object. | ||||
| 		UI 1.10: this is the <a> tag inside the LI | ||||
| 		UI 1.11: this is the LI istself | ||||
| 		UI 1.12: this is the <div> tag inside the LI | ||||
| 	 */ | ||||
| 	getEntryWrapper: function(cmd) { | ||||
| 		return this._getMenuEntry(cmd).find(">[role=menuitem]").addBack("[role=menuitem]"); | ||||
| 	}, | ||||
| 	/** Return Menu element (UL). */ | ||||
| 	getMenu: function() { | ||||
| 		return this.$menu; | ||||
| 	}, | ||||
| 	/** Return true if menu is open. */ | ||||
| 	isOpen: function() { | ||||
| //            return this.$menu && this.$menu.is(":visible"); | ||||
| 		return !!this.$menu && !!this.currentTarget; | ||||
| 	}, | ||||
| 	/** Open context menu on a specific target (must match options.delegate) | ||||
| 	 *  Optional `extraData` is passed to event handlers as `ui.extraData`. | ||||
| 	 */ | ||||
| 	open: function(targetOrEvent, extraData) { | ||||
| 		// Fake a 'contextmenu' event | ||||
| 		extraData = extraData || {}; | ||||
|  | ||||
| 		var isEvent = (targetOrEvent && targetOrEvent.type && targetOrEvent.target), | ||||
| 			event =  isEvent ? targetOrEvent : {}, | ||||
| 			target = isEvent ? targetOrEvent.target : targetOrEvent, | ||||
| 			e = jQuery.Event("contextmenu", { | ||||
| 				target: $(target).get(0), | ||||
| 				pageX: event.pageX, | ||||
| 				pageY: event.pageY, | ||||
| 				originalEvent: isEvent ? targetOrEvent : undefined, | ||||
| 				_extraData: extraData | ||||
| 			}); | ||||
| 		return this.element.trigger(e); | ||||
| 	}, | ||||
| 	/** Replace the menu altogether. */ | ||||
| 	replaceMenu: function(data) { | ||||
| 		this._createUiMenu(data); | ||||
| 	}, | ||||
| 	/** Redefine a whole menu entry. */ | ||||
| 	setEntry: function(cmd, entry) { | ||||
| 		var $ul, | ||||
| 			$entryLi = this._getMenuEntry(cmd); | ||||
|  | ||||
| 		if (typeof entry === "string") { | ||||
| 			window.console && window.console.warn( | ||||
| 				"setEntry(cmd, t) with a plain string title is deprecated since v1.18." + | ||||
| 				"Use setTitle(cmd, '" + entry + "') instead."); | ||||
| 			return this.setTitle(cmd, entry); | ||||
| 		} | ||||
| 		$entryLi.empty(); | ||||
| 		entry.cmd = entry.cmd || cmd; | ||||
| 		$.moogle.contextmenu.createEntryMarkup(entry, $entryLi); | ||||
| 		if ($.isArray(entry.children)) { | ||||
| 			$ul = $("<ul/>").appendTo($entryLi); | ||||
| 			$.moogle.contextmenu.createMenuMarkup(entry.children, $ul); | ||||
| 		} | ||||
| 		// #110: jQuery UI 1.12: refresh only works when this class is not set: | ||||
| 		$entryLi.removeClass("ui-menu-item"); | ||||
| 		this.getMenu().menu("refresh"); | ||||
| 	}, | ||||
| 	/** Set icon (pass null to remove). */ | ||||
| 	setIcon: function(cmd, icon) { | ||||
| 		return this.updateEntry(cmd, { uiIcon: icon }); | ||||
| 	}, | ||||
| 	/** Set title. */ | ||||
| 	setTitle: function(cmd, title) { | ||||
| 		return this.updateEntry(cmd, { title: title }); | ||||
| 	}, | ||||
| 	// /** Set tooltip (pass null to remove). */ | ||||
| 	// setTooltip: function(cmd, tooltip) { | ||||
| 	// 	this._getMenuEntry(cmd).attr("title", tooltip); | ||||
| 	// }, | ||||
| 	/** Show or hide the menu command. */ | ||||
| 	showEntry: function(cmd, flag) { | ||||
| 		this._getMenuEntry(cmd).toggle(flag !== false); | ||||
| 	}, | ||||
| 	/** Redefine selective attributes of a menu entry. */ | ||||
| 	updateEntry: function(cmd, entry) { | ||||
| 		var $icon, $wrapper, | ||||
| 			$entryLi = this._getMenuEntry(cmd); | ||||
|  | ||||
| 		if ( entry.title !== undefined ) { | ||||
| 			$.moogle.contextmenu.updateTitle($entryLi, "" + entry.title); | ||||
| 		} | ||||
| 		if ( entry.tooltip !== undefined ) { | ||||
| 			if ( entry.tooltip === null ) { | ||||
| 				$entryLi.removeAttr("title"); | ||||
| 			} else { | ||||
| 				$entryLi.attr("title", entry.tooltip); | ||||
| 			} | ||||
| 		} | ||||
| 		if ( entry.uiIcon !== undefined ) { | ||||
| 			$wrapper = this.getEntryWrapper(cmd), | ||||
| 			$icon = $wrapper.find("span.ui-icon").not(".ui-menu-icon"); | ||||
| 			$icon.remove(); | ||||
| 			if ( entry.uiIcon ) { | ||||
| 				$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon)); | ||||
| 			} | ||||
| 		} | ||||
| 		if ( entry.hide !== undefined ) { | ||||
| 			$entryLi.toggle(!entry.hide); | ||||
| 		} else if ( entry.show !== undefined ) { | ||||
| 			// Note: `show` is an undocumented variant. `hide: false` is preferred | ||||
| 			$entryLi.toggle(!!entry.show); | ||||
| 		} | ||||
| 		// if ( entry.isHeader !== undefined ) { | ||||
| 		// 	$entryLi.toggleClass("ui-widget-header", !!entry.isHeader); | ||||
| 		// } | ||||
| 		if ( entry.data !== undefined ) { | ||||
| 			$entryLi.data(entry.data); | ||||
| 		} | ||||
|  | ||||
| 		// Set/clear class names, but handle ui-state-disabled separately | ||||
| 		if ( entry.disabled === undefined ) { | ||||
| 			entry.disabled = $entryLi.hasClass("ui-state-disabled"); | ||||
| 		} | ||||
| 		if ( entry.setClass ) { | ||||
| 			if ( $entryLi.hasClass("ui-menu-item") ) { | ||||
| 				entry.setClass += " ui-menu-item"; | ||||
| 			} | ||||
| 			$entryLi.removeClass(); | ||||
| 			$entryLi.addClass(entry.setClass); | ||||
| 		} else if ( entry.addClass ) { | ||||
| 			$entryLi.addClass(entry.addClass); | ||||
| 		} | ||||
| 		$entryLi.toggleClass("ui-state-disabled", !!entry.disabled); | ||||
| 		// // #110: jQuery UI 1.12: refresh only works when this class is not set: | ||||
| 		// $entryLi.removeClass("ui-menu-item"); | ||||
| 		// this.getMenu().menu("refresh"); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| /* | ||||
|  * Global functions | ||||
|  */ | ||||
| $.extend($.moogle.contextmenu, { | ||||
| 	/** Convert a menu description into a into a <li> content. */ | ||||
| 	createEntryMarkup: function(entry, $parentLi) { | ||||
| 		var $wrapper = null; | ||||
|  | ||||
| 		$parentLi.attr("data-command", entry.cmd); | ||||
|  | ||||
| 		if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) { | ||||
| 			// hyphen, em dash, en dash: separator as defined by UI Menu 1.10 | ||||
| 			$parentLi.text(entry.title); | ||||
| 		} else { | ||||
| 			if ( isLTE110 ) { | ||||
| 				// jQuery UI Menu 1.10 or before required an `<a>` tag | ||||
| 				$wrapper = $("<a/>", { | ||||
| 						html: "" + entry.title, | ||||
| 						href: "#" | ||||
| 					}).appendTo($parentLi); | ||||
|  | ||||
| 			} else if ( isLTE111 ) { | ||||
| 				// jQuery UI Menu 1.11 preferes to avoid `<a>` tags or <div> wrapper | ||||
| 				$parentLi.html("" + entry.title); | ||||
| 				$wrapper = $parentLi; | ||||
|  | ||||
| 			} else { | ||||
| 				// jQuery UI Menu 1.12 introduced `<div>` wrappers | ||||
| 				$wrapper = $("<div/>", { | ||||
| 						html: "" + entry.title | ||||
| 					}).appendTo($parentLi); | ||||
| 			} | ||||
| 			if ( entry.uiIcon ) { | ||||
| 				$wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon)); | ||||
| 			} | ||||
| 			// Store option callbacks in entry's data | ||||
| 			$.each( [ "action", "disabled", "title", "tooltip" ], function(i, attr) { | ||||
| 				if ( $.isFunction(entry[attr]) ) { | ||||
| 					$parentLi.data(attr + "Handler", entry[attr]); | ||||
| 				} | ||||
| 			}); | ||||
| 			if ( entry.disabled === true ) { | ||||
| 				$parentLi.addClass("ui-state-disabled"); | ||||
| 			} | ||||
| 			if ( entry.isHeader ) { | ||||
| 				$parentLi.addClass("ui-widget-header"); | ||||
| 			} | ||||
| 			if ( entry.addClass ) { | ||||
| 				$parentLi.addClass(entry.addClass); | ||||
| 			} | ||||
| 			if ( $.isPlainObject(entry.data) ) { | ||||
| 				$parentLi.data(entry.data); | ||||
| 			} | ||||
| 			if ( typeof entry.tooltip === "string" ) { | ||||
| 				$parentLi.attr("title", entry.tooltip); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	/** Convert a nested array of command objects into a <ul> structure. */ | ||||
| 	createMenuMarkup: function(options, $parentUl) { | ||||
| 		var i, menu, $ul, $li; | ||||
| 		if ( $parentUl == null ) { | ||||
| 			$parentUl = $("<ul class='ui-helper-hidden' />").appendTo("body"); | ||||
| 		} | ||||
| 		for (i = 0; i < options.length; i++) { | ||||
| 			menu = options[i]; | ||||
| 			$li = $("<li/>").appendTo($parentUl); | ||||
|  | ||||
| 			$.moogle.contextmenu.createEntryMarkup(menu, $li); | ||||
|  | ||||
| 			if ( $.isArray(menu.children) ) { | ||||
| 				$ul = $("<ul/>").appendTo($li); | ||||
| 				$.moogle.contextmenu.createMenuMarkup(menu.children, $ul); | ||||
| 			} | ||||
| 		} | ||||
| 		return $parentUl; | ||||
| 	}, | ||||
| 	/** Returns true if the menu item has child menu items */ | ||||
| 	isMenu: function(item) { | ||||
| 		if ( isLTE110 ) { | ||||
| 			return item.has(">a[aria-haspopup='true']").length > 0; | ||||
| 		} else if ( isLTE111 ) {  // jQuery UI 1.11 used no tag wrappers | ||||
| 			return item.is("[aria-haspopup='true']"); | ||||
| 		} else { | ||||
| 			return item.has(">div[aria-haspopup='true']").length > 0; | ||||
| 		} | ||||
| 	}, | ||||
| 	/** Replace the title of elem', but retain icons andchild entries. */ | ||||
| 	replaceFirstTextNodeChild: function(elem, html) { | ||||
| 		var $icons = elem.find(">span.ui-icon,>ul.ui-menu").detach(); | ||||
|  | ||||
| 		elem | ||||
| 			.empty() | ||||
| 			.html(html) | ||||
| 			.append($icons); | ||||
| 	}, | ||||
| 	/** Updates the menu item's title */ | ||||
| 	updateTitle: function(item, title) { | ||||
| 		if ( isLTE110 ) {  // jQuery UI 1.10 and before used <a> tags | ||||
| 			$.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title); | ||||
| 		} else if ( isLTE111 ) {  // jQuery UI 1.11 used no tag wrappers | ||||
| 			$.moogle.contextmenu.replaceFirstTextNodeChild(item, title); | ||||
| 		} else {  // jQuery UI 1.12+ introduced <div> tag wrappers | ||||
| 			$.moogle.contextmenu.replaceFirstTextNodeChild($("div", item), title); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| })); | ||||
| @@ -298,13 +298,8 @@ div.ui-tooltip { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| #note-type .dropdown-menu li:not(.divider) { | ||||
|     padding: 5px; | ||||
|     width: 200px; | ||||
| } | ||||
|  | ||||
| .dropdown-menu li:not(.divider):hover, .dropdown-menu li:not(.divider) a:hover { | ||||
|     background-color: #ccc !important; | ||||
| .dropdown-menu a:hover:not(.disabled) { | ||||
|     background-color: #eee !important; | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| @@ -324,6 +319,10 @@ div.ui-tooltip { | ||||
|     float: right; | ||||
| } | ||||
|  | ||||
| .dropdown-item.disabled, .dropdown-item.disabled kbd { | ||||
|     color: #aaa !important; | ||||
| } | ||||
|  | ||||
| #note-detail-code { | ||||
|     min-height: 200px; | ||||
|     overflow: auto; | ||||
| @@ -513,3 +512,11 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | ||||
|     margin: 50px; | ||||
|     padding: 20px; | ||||
| } | ||||
|  | ||||
| .context-menu { | ||||
|     font-size: small; | ||||
| } | ||||
|  | ||||
| .context-menu .dropdown-item { | ||||
|     padding: 2px 10px 2px 10px; | ||||
| } | ||||
| @@ -754,6 +754,12 @@ | ||||
|         </form> | ||||
|       </div> | ||||
|  | ||||
|       <div class="dropdown-menu dropdown-menu-sm context-menu" id="tree-context-menu"> | ||||
|         <a class="dropdown-item" href="#">Action</a> | ||||
|         <a class="dropdown-item" href="#">Another action</a> | ||||
|         <a class="dropdown-item" href="#">Something else here</a> | ||||
|       </div> | ||||
|  | ||||
|       <div id="markdown-import-dialog" class="tdialog" title="Markdown import" style="padding: 20px;"> | ||||
|         <p>Because of browser sandbox it's not possible to directly read clipboard from JavaScript. Please paste the Markdown to import to textarea below and click on Import button</p> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user