Using Bootstrap 3, how can I place the dropdown menu at the cursor and open it from code?
I need to use it on a table as a context menu for its rows.
I just wanted to improve on letiagoalves great answer with a couple more suggestions.
Here's a walkthrough on how to add a context menu to any html element.
First, let's add a menu from the bootstrap dropdown control. Add it anywhere to your HTML, preferably at the root level of the body. The .dropdown-menu
class will set display:none
so it's initially invisible.
It should look like this:
<ul id="contextMenu" class="dropdown-menu" role="menu"> <li><a tabindex="-1" href="#">Action</a></li> <li><a tabindex="-1" href="#">Another action</a></li> <li><a tabindex="-1" href="#">Something else here</a></li> <li class="divider"></li> <li><a tabindex="-1" href="#">Separated link</a></li> </ul>
To keep our design modular, we'll add our JavaScript code as a jQuery extension called contextMenu
.
When we call $.contextMenu
, we'll pass in a settings object with 2 properties:
menuSelector
takes the jQuery selector of the menu we created earlier in HTML.menuSelected
will be called when the context menu action is clicked.$("#myTable").contextMenu({ menuSelector: "#contextMenu", menuSelected: function (invokedOn, selectedMenu) { // context menu clicked }); });
Based off the jQuery boilerplate plugin template, we'll use an Immediately-Invoked Function Expression so we don't muddle up the global namespace. Since we have dependencies on jQuery and need access to the window, we'll pass them in as variables so we can survive minification. It will look like this:
(function($, window){ $.fn.contextMenu = function(settings) { return this.each(function() { // Code Goes Here } }; })(jQuery, window);
We'll handle the contextmenu
mouse event on the object that called the extension. When the event fires, we'll grab the dropdown menu that we added in the beginning. We'll locate it by using the selector string passed in by the settings when we initialized the function. We'll modify the menu by doing the following:
e.target
property and store it as a data attribute called invokedOn
, so we can later identify the element that raised the context menu..show()
.css()
. position
is set to absolute
. pageX
and pageY
properties of the event. return false
to stop the javascript from handling anything else.It will look like this:
$(this).on("contextmenu", function (e) { $(settings.menuSelector) .data("invokedOn", $(e.target)) .show() .css({ position: "absolute", left: e.pageX, top: e.pageY }); return false; });
This will open the menu to the bottom right of the cursor that opened it. However, if the cursor is to the far right of the screen, the menu should open to the left. Likewise, if the cursor is on the bottom, the menu should open to the top. It's also important to differentiate between the bottom of the window
, which contains the physical frame, and the bottom of the document
which represents the entire html DOM and can scroll far past the window.
To accomplish this, we'll set the location using the following functions:
We'll call them like this:
.css({ left: getMenuPosition(e.clientX, 'width', 'scrollLeft'), top: getMenuPosition(e.clientY, 'height', 'scrollTop') });
Which will call this function to return the appropriate position:
function getMenuPosition(mouse, direction, scrollDir) { var win = $(window)[direction](), scroll = $(window)[scrollDir](), menu = $(settings.menuSelector)[direction](), position = mouse + scroll; // opening menu would pass the side of the page if (mouse + menu > win && menu < mouse) position -= menu; return position }
After we display the context menu, we need to add an event handler to listen for click events on it. We'll remove any other bindings that might have already been added so that we won't fire the same event twice. These can occur anytime the menu was opened, but nothing was selected due to clicking off. Then we can add a new binding on the click
event where we'll handle the logic in the next section.
As valepu noted, we don't want to register clicks on anything other than menu items, so we setup a delegated handler by passing a selector into the on
function which will "filter the descendants of the selected elements that trigger the event".
So far, the function should look like this:
$(settings.menuSelector) .off('click') .on( 'click', "a", function (e) { //CODE IN NEXT SECTION GOES HERE });
Once we know a click has occurred on the menu, we'll do the following things: We'll hide the menu from the screen with .hide()
. Next, we want to save the element on which the menu was originally invoked as well as the selection from the current menu. Finally, we'll fire the function option that was passed into the extension by using .call()
on the property and passing in the event targets as arguments.
$menu.hide(); var $invokedOn = $menu.data("invokedOn"); var $selectedMenu = $(e.target); settings.menuSelected.call($(this), $invokedOn, $selectedMenu);
Finally, as with most context menus, we want to close the menu when a user clicks off of it as well. To do so, we'll listen for any click events on the body and close the context menu if it's open like this:
$('body').click(function () { $(settings.menuSelector).hide(); });
Note: Thanks to Sadhir's comment, Firefox linux triggers the click event on
document
during a right click, so you have to setup the listener onbody
.
The extension will return with the original object that raised the context menu and the menu item that was clicked. You may have to traverse the dom using jQuery to find something meaningful from the event targets, but this should provide a good layer of base functionality.
Here's a example to return info for the item and action selected:
$("#myTable").contextMenu({ menuSelector: "#contextMenu", menuSelected: function (invokedOn, selectedMenu) { var msg = "You selected the menu item '" + selectedMenu.text() + "' on the value '" + invokedOn.text() + "'"; alert(msg); } });
This answer has been updated substantially by wrapping it in a jQuery extension method. If you'd like to see my original, you can view the post history, but I believe this final version utilizes much better coding practices.
Bonus Feature:
If you want to add some nice functionality for powerusers or yourself in developing features, you can bypass the context menu based on any key combinations being held when your right click. For example, if you wanted to allow the original browser context menu to display when holding Ctrl, you could add this as the first line of the contextMenu handler:
// return native menu if pressing control if (e.ctrlKey) return;
It is possible. I made you a working demo to give a good start.
Working demo (Right click on any table row to see it in action)
First create your dropdown menu, hide it and change its position
to absolute
:
#contextMenu { position: absolute; display:none; }
Then bind a contextmenu
event to your table rows so it shows dropdown/context menu and position it at the cursor:
var $contextMenu = $("#contextMenu"); $("body").on("contextmenu", "table tr", function(e) { $contextMenu.css({ display: "block", left: e.pageX, top: e.pageY }); return false; });
Then when user select an option hide dropdown/context menu:
$contextMenu.on("click", "a", function() { $contextMenu.hide(); });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With