为我的 web 应用制作自定义右键上下文菜单

139

我有几个网站,如Google Docs和MapQuest,在右键单击时会出现自定义下拉菜单。它们以某种方式覆盖了浏览器的下拉菜单行为,但我不确定它们是如何做到的。我找到了一个 jQuery 插件 可以实现这个效果,但我仍然好奇以下几点:

  • 这是如何实现的?浏览器的下拉菜单是否真的被覆盖了,还是只是模拟了效果?如果是模拟的话,是如何实现的?
  • 插件抽象出了什么?背后发生了什么?
  • 这是实现此效果的唯一方法吗?

自定义上下文菜单图片

查看几个自定义上下文菜单的实例

9个回答

241

我知道这个问题很老了,但是我遇到了同样的问题并自己解决了,所以我在这里回答,以防有人像我一样通过谷歌找到它。我的解决方案基于@Andrew的方案,但基本上修改了之后的所有内容。

编辑:看到最近这个问题很受欢迎,我决定更新样式,使其看起来更像2014年,而不是Windows 95。我修复了@Quantico和@Trengot发现的错误,所以现在它是一个更可靠的答案。

编辑2:我使用StackSnippets设置了它们作为一个非常酷的新功能。我将good jsfiddle 留在这里供参考(单击第四个面板以查看它们的工作)。

新的Stack Snippet:

// JAVASCRIPT (jQuery)

// Trigger action when the contexmenu is about to be shown
$(document).bind("contextmenu", function (event) {
    
    // Avoid the real one
    event.preventDefault();
    
    // Show contextmenu
    $(".custom-menu").finish().toggle(100).
    
    // In the right position (the mouse)
    css({
        top: event.pageY + "px",
        left: event.pageX + "px"
    });
});


// If the document is clicked somewhere
$(document).bind("mousedown", function (e) {
    
    // If the clicked element is not the menu
    if (!$(e.target).parents(".custom-menu").length > 0) {
        
        // Hide it
        $(".custom-menu").hide(100);
    }
});


// If the menu element is clicked
$(".custom-menu li").click(function(){
    
    // This is the triggered action name
    switch($(this).attr("data-action")) {
        
        // A case for each action. Your actions here
        case "first": alert("first"); break;
        case "second": alert("second"); break;
        case "third": alert("third"); break;
    }
  
    // Hide it AFTER the action was triggered
    $(".custom-menu").hide(100);
  });
/* CSS3 */

/* The whole thing */
.custom-menu {
    display: none;
    z-index: 1000;
    position: absolute;
    overflow: hidden;
    border: 1px solid #CCC;
    white-space: nowrap;
    font-family: sans-serif;
    background: #FFF;
    color: #333;
    border-radius: 5px;
    padding: 0;
}

/* Each of the items in the list */
.custom-menu li {
    padding: 8px 12px;
    cursor: pointer;
    list-style-type: none;
    transition: all .3s ease;
    user-select: none;
}

.custom-menu li:hover {
    background-color: #DEF;
}
<!-- HTML -->
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.js"></script>

<ul class='custom-menu'>
  <li data-action="first">First thing</li>
  <li data-action="second">Second thing</li>
  <li data-action="third">Third thing</li>
</ul>

<!-- Not needed, only for making it clickable on StackOverflow -->
Right click me

注意:您可能会看到一些小错误(下拉菜单远离光标等),请确保它在 jsfiddle 中工作,因为那比 StackSnippets 更类似于您的网页。

1
我认为你的mousedown可能存在问题。这可能会导致竞争条件,因为单击菜单项会触发点击事件,即mousedown和mouseup。 - Quantico
2
感谢 @Quantico,确实如此,现在应该已经修复了,代码和 jsfiddle 都已更新。还有其他问题吗?顺便提一句,哇,之前对 jsfiddle 进行了 170 次编辑,它变得真的很受欢迎了。 - Francisco Presencia
1
当使用新的 fiddle 时,如果您在页面上使用任何其他 HTML 元素,则弹出窗口会变为透明。编辑:将背景颜色添加到 CSS 中即可解决该问题。 - Holloway
1
另一个小问题:如果在菜单可见时右键单击某个地方,它会闪烁一下才显示。我认为它应该要么隐藏(像默认情况),要么隐藏然后出现在新位置。 - Holloway
$(document).bind("contextmenu", ...); 中的绑定对象从 document 更改为正确的类。 - Francisco Presencia
显示剩余2条评论

65

正如Adrian所说,插件的工作方式将是相同的。你需要三个基本部分:

1:处理'contextmenu'事件的事件处理程序:

$(document).bind("contextmenu", function(event) {
    event.preventDefault();
    $("<div class='custom-menu'>Custom menu</div>")
        .appendTo("body")
        .css({top: event.pageY + "px", left: event.pageX + "px"});
});

你可以将事件处理程序绑定到任何你想显示菜单的选择器上。我选择了整个文档。

2: 'click' 事件的事件处理程序(用于关闭自定义菜单):

$(document).bind("click", function(event) {
    $("div.custom-menu").hide();
});

3:控制菜单位置的CSS:

.custom-menu {
    z-index:1000;
    position: absolute;
    background-color:#C0C0C0;
    border: 1px solid black;
    padding: 2px;
}

CSS的重要之处在于包括z-indexposition: absolute

将所有这些内容包装成一个流畅的jQuery插件并不太困难。

您可以在此处查看简单演示:http://jsfiddle.net/andrewwhitaker/fELma/


我认为如果用户在菜单内部点击时它能保持打开状态会更有用(但在用户在外部点击时关闭)。是否可以修改以实现这种方式的工作? - Anderson Green
2
document的点击绑定中查看event.target。如果它不在上下文菜单中,则隐藏菜单:http://jsfiddle.net/fELma/286/ - Andrew Whitaker
2
我稍微修改了一下(以防止同时显示多个菜单):http://jsfiddle.net/fELma/287/ - Anderson Green
我正在尝试创建一个类似于这里的右键径向上下文菜单(如:http://www.pushing-pixels.org/wp-content/uploads/2012/07/onenote-radial.png)。这是了解如何处理它的良好开端,谢谢! - Boris
@AndrewWhitaker 您的回答说它将应用于整个文档。如果我想将其应用于特定控件,例如一个按钮(假设其ID为button1)怎么办? - Tk1993

9

<!DOCTYPE html>
<html>
<head>
    <title>Right Click</title>

    <link href="https://swisnl.github.io/jQuery-contextMenu/dist/jquery.contextMenu.css" rel="stylesheet" type="text/css" />

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://swisnl.github.io/jQuery-contextMenu/dist/jquery.contextMenu.js" type="text/javascript"></script>

    <script src="https://swisnl.github.io/jQuery-contextMenu/dist/jquery.ui.position.min.js" type="text/javascript"></script>

</head>
<body>
    <span class="context-menu-one" style="border:solid 1px black; padding:5px;">Right Click Me</span>
    <script type="text/javascript">
        
        $(function() {
        $.contextMenu({
            selector: '.context-menu-one', 
            callback: function(key, options) {
                var m = "clicked: " + key;
                window.console && console.log(m) || alert(m); 
            },
            items: {
                "edit": {name: "Edit", icon: "edit"},
                "cut": {name: "Cut", icon: "cut"},
               copy: {name: "Copy", icon: "copy"},
                "paste": {name: "Paste", icon: "paste"},
                "delete": {name: "Delete", icon: "delete"},
                "sep1": "---------",
                "quit": {name: "Quit", icon: function(){
                    return 'context-menu-icon context-menu-icon-quit';
                }}
            }
        });

        $('.context-menu-one').on('click', function(e){
            console.log('clicked', this);
        })    
    });
    </script>
</body>
</html>


5
这里有一个关于JavaScript右键菜单的例子:右键菜单 使用原始JavaScript代码实现上下文菜单功能。请检查一下,希望这对你有所帮助。
实时代码:

(function() {
  
  "use strict";


  /*********************************************** Context Menu Function Only ********************************/
  function clickInsideElement( e, className ) {
    var el = e.srcElement || e.target;
    if ( el.classList.contains(className) ) {
      return el;
    } else {
      while ( el = el.parentNode ) {
        if ( el.classList && el.classList.contains(className) ) {
          return el;
        }
      }
    }
    return false;
  }

  function getPosition(e) {
    var posx = 0, posy = 0;
    if (!e) var e = window.event;
    if (e.pageX || e.pageY) {
      posx = e.pageX;
      posy = e.pageY;
    } else if (e.clientX || e.clientY) {
      posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
      posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
    }
    return {
      x: posx,
      y: posy
    }
  }

  // Your Menu Class Name
  var taskItemClassName = "thumb";
  var contextMenuClassName = "context-menu",contextMenuItemClassName = "context-menu__item",contextMenuLinkClassName = "context-menu__link", contextMenuActive = "context-menu--active";
  var taskItemInContext, clickCoords, clickCoordsX, clickCoordsY, menu = document.querySelector("#context-menu"), menuItems = menu.querySelectorAll(".context-menu__item");
  var menuState = 0, menuWidth, menuHeight, menuPosition, menuPositionX, menuPositionY, windowWidth, windowHeight;

  function initMenuFunction() {
    contextListener();
    clickListener();
    keyupListener();
    resizeListener();
  }

  /**
   * Listens for contextmenu events.
   */
  function contextListener() {
    document.addEventListener( "contextmenu", function(e) {
      taskItemInContext = clickInsideElement( e, taskItemClassName );

      if ( taskItemInContext ) {
        e.preventDefault();
        toggleMenuOn();
        positionMenu(e);
      } else {
        taskItemInContext = null;
        toggleMenuOff();
      }
    });
  }

  /**
   * Listens for click events.
   */
  function clickListener() {
    document.addEventListener( "click", function(e) {
      var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName );

      if ( clickeElIsLink ) {
        e.preventDefault();
        menuItemListener( clickeElIsLink );
      } else {
        var button = e.which || e.button;
        if ( button === 1 ) {
          toggleMenuOff();
        }
      }
    });
  }

  /**
   * Listens for keyup events.
   */
  function keyupListener() {
    window.onkeyup = function(e) {
      if ( e.keyCode === 27 ) {
        toggleMenuOff();
      }
    }
  }

  /**
   * Window resize event listener
   */
  function resizeListener() {
    window.onresize = function(e) {
      toggleMenuOff();
    };
  }

  /**
   * Turns the custom context menu on.
   */
  function toggleMenuOn() {
    if ( menuState !== 1 ) {
      menuState = 1;
      menu.classList.add( contextMenuActive );
    }
  }

  /**
   * Turns the custom context menu off.
   */
  function toggleMenuOff() {
    if ( menuState !== 0 ) {
      menuState = 0;
      menu.classList.remove( contextMenuActive );
    }
  }

  function positionMenu(e) {
    clickCoords = getPosition(e);
    clickCoordsX = clickCoords.x;
    clickCoordsY = clickCoords.y;
    menuWidth = menu.offsetWidth + 4;
    menuHeight = menu.offsetHeight + 4;

    windowWidth = window.innerWidth;
    windowHeight = window.innerHeight;

    if ( (windowWidth - clickCoordsX) < menuWidth ) {
      menu.style.left = (windowWidth - menuWidth)-0 + "px";
    } else {
      menu.style.left = clickCoordsX-0 + "px";
    }

    // menu.style.top = clickCoordsY + "px";

    if ( Math.abs(windowHeight - clickCoordsY) < menuHeight ) {
      menu.style.top = (windowHeight - menuHeight)-0 + "px";
    } else {
      menu.style.top = clickCoordsY-0 + "px";
    }
  }


  function menuItemListener( link ) {
    var menuSelectedPhotoId = taskItemInContext.getAttribute("data-id");
    console.log('Your Selected Photo: '+menuSelectedPhotoId)
    var moveToAlbumSelectedId = link.getAttribute("data-action");
    if(moveToAlbumSelectedId == 'remove'){
      console.log('You Clicked the remove button')
    }else if(moveToAlbumSelectedId && moveToAlbumSelectedId.length > 7){
      console.log('Clicked Album Name: '+moveToAlbumSelectedId);
    }
    toggleMenuOff();
  }
  initMenuFunction();

})();
/* For Body Padding and content */
body { padding-top: 70px; }
li a { text-decoration: none !important; }

/* Thumbnail only */
.thumb {
  margin-bottom: 30px;
}
.thumb:hover a, .thumb:active a, .thumb:focus a {
  border: 1px solid purple;
}

/************** For Context menu ***********/
/* context menu */
.context-menu {  display: none;  position: absolute;  z-index: 9999;  padding: 12px 0;  width: 200px;  background-color: #fff;  border: solid 1px #dfdfdf;  box-shadow: 1px 1px 2px #cfcfcf;  }
.context-menu--active {  display: block;  }

.context-menu__items { list-style: none;  margin: 0;  padding: 0;  }
.context-menu__item { display: block;  margin-bottom: 4px;  }
.context-menu__item:last-child {  margin-bottom: 0;  }
.context-menu__link {  display: block;  padding: 4px 12px;  color: #0066aa;  text-decoration: none;  }
.context-menu__link:hover {  color: #fff;  background-color: #0066aa;  }
.context-menu__items ul {  position: absolute;  white-space: nowrap;  z-index: 1;  left: -99999em;}
.context-menu__items > li:hover > ul {  left: auto;  padding-top: 5px  ;  min-width: 100%;  }
.context-menu__items > li li ul {  border-left:1px solid #fff;}
.context-menu__items > li li:hover > ul {  left: 100%;  top: -1px;  }
.context-menu__item ul { background-color: #ffffff; padding: 7px 11px;  list-style-type: none;  text-decoration: none; margin-left: 40px; }
.page-media .context-menu__items ul li { display: block; }
/************** For Context menu ***********/
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<body>



    <!-- Page Content -->
    <div class="container">

        <div class="row">

            <div class="col-lg-12">
                <h1 class="page-header">Thumbnail Gallery <small>(Right click to see the context menu)</small></h1>
            </div>

            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>
            <div class="col-lg-3 col-md-4 col-xs-6 thumb">
                <a class="thumbnail" href="#">
                    <img class="img-responsive" src="http://placehold.it/400x300" alt="">
                </a>
            </div>

        </div>

        <hr>


    </div>
    <!-- /.container -->


    <!-- / The Context Menu -->
    <nav id="context-menu" class="context-menu">
        <ul class="context-menu__items">
            <li class="context-menu__item">
                <a href="#" class="context-menu__link" data-action="Delete This Photo"><i class="fa fa-empire"></i> Delete This Photo</a>
            </li>
            <li class="context-menu__item">
                <a href="#" class="context-menu__link" data-action="Photo Option 2"><i class="fa fa-envira"></i> Photo Option 2</a>
            </li>
            <li class="context-menu__item">
                <a href="#" class="context-menu__link" data-action="Photo Option 3"><i class="fa fa-first-order"></i> Photo Option 3</a>
            </li>
            <li class="context-menu__item">
                <a href="#" class="context-menu__link" data-action="Photo Option 4"><i class="fa fa-gitlab"></i> Photo Option 4</a>
            </li>
            <li class="context-menu__item">
                <a href="#" class="context-menu__link" data-action="Photo Option 5"><i class="fa fa-ioxhost"></i> Photo Option 5</a>
            </li>
            <li class="context-menu__item">
                <a href="#" class="context-menu__link"><i class="fa fa-arrow-right"></i> Add Photo to</a>
                <ul>
                    <li><a href="#!" class="context-menu__link" data-action="album-one"><i class="fa fa-camera-retro"></i> Album One</a></li>
                    <li><a href="#!" class="context-menu__link" data-action="album-two"><i class="fa fa-camera-retro"></i> Album Two</a></li>
                    <li><a href="#!" class="context-menu__link" data-action="album-three"><i class="fa fa-camera-retro"></i> Album Three</a></li>
                    <li><a href="#!" class="context-menu__link" data-action="album-four"><i class="fa fa-camera-retro"></i> Album Four</a></li>
                </ul>
            </li>
        </ul>
    </nav>

    <!-- End # Context Menu -->


</body>


做得很好,使用了原生JS和简洁的布局! - kurt

3
浏览器的上下文菜单被覆盖了。在任何主要浏览器中都没有办法增强本机上下文菜单。
由于插件正在创建自己的菜单,因此实际上唯一被抽象化的部分是浏览器的上下文菜单事件。该插件根据您的配置创建基于html的菜单,然后将该内容放置在您单击的位置。
是的,这是创建自定义上下文菜单的唯一方法。显然,不同的插件会做出略有不同的事情,但它们都会覆盖浏览器的事件,并在正确的位置放置自己的基于html的菜单。

2
只是提一下,Firefox现在正在添加对HTML5本地“上下文菜单”(通过标记声明)的支持。它现在可以在Firefox 8 beta中使用。(https://developer.mozilla.org/en/Firefox_8_for_developers)。 - poshaughnessy

2
你可以观看这个教程:http://www.youtube.com/watch?v=iDyEfKWCzhg请确保上下文菜单一开始被隐藏并具有绝对位置。这将确保不会有多个上下文菜单和无用的上下文菜单创建。页面链接放在YouTube视频的描述中。
$(document).bind("contextmenu", function(event){
$("#contextmenu").css({"top": event.pageY +  "px", "left": event.pageX +  "px"}).show();
});
$(document).bind("click", function(){
$("#contextmenu").hide();
});

1

简单的一个

  1. 在文档中的任何位置右键单击时显示上下文菜单
  2. 避免在上下文菜单内单击时隐藏上下文菜单
  3. 按下左鼠标按钮时关闭上下文菜单

注意: 不要使用display:none而是使用opacity来隐藏和显示

var menu= document.querySelector('.context_menu');
document.addEventListener("contextmenu", function(e) {      
            e.preventDefault();  
            menu.style.position = 'absolute';
            menu.style.left = e.pageX + 'px';
            menu.style.top = e.pageY + 'px';        
             menu.style.opacity = 1;
        });
       
  document.addEventListener("click", function(e){
  if(e.target.closest('.context_menu'))
  return;
      menu.style.opacity = 0;
  });
.context_menu{

width:70px;
background:lightgrey;
padding:5px;
 opacity :0;
}
.context_menu div{
margin:5px;
background:grey;

}
.context_menu div:hover{
margin:5px;
background:red;
   cursor:pointer;

}
<div class="context_menu">
<div>menu 1</div>
<div>menu 2</div>
</div>

额外的 CSS

var menu= document.querySelector('.context_menu');
document.addEventListener("contextmenu", function(e) {      
            e.preventDefault();  
            menu.style.position = 'absolute';
            menu.style.left = e.pageX + 'px';
            menu.style.top = e.pageY + 'px';        
             menu.style.opacity = 1;
        });
       
  document.addEventListener("click", function(e){
  if(e.target.closest('.context_menu'))
  return;
      menu.style.opacity = 0;
  });
.context_menu{

width:120px;
background:white;
border:1px solid lightgrey;

 opacity :0;
}
.context_menu div{
padding:5px;
padding-left:15px;
margin:5px 2px;
border-bottom:1px solid lightgrey;
}
.context_menu div:last-child {
border:none;
}
.context_menu div:hover{

background:lightgrey;
   cursor:pointer;

}
<div class="context_menu">
<div>menu 1</div>
<div>menu 2</div>
<div>menu 3</div>
<div>menu 4</div>
</div>


1
我知道这已经相当老了。最近,我需要创建一个上下文菜单,将其注入到其他具有不同属性的站点中,这些属性基于所点击的元素而异。
这还比较粗糙,可能有更好的方法来实现此目的。 它使用jQuery上下文菜单库位于此处
我很享受创造它,并认为你们可能会用得上它。
这里是fiddle。希望它能帮助某个人。
$(function() {
  function createSomeMenu() {
    var all_array = '{';
    var x = event.clientX,
      y = event.clientY,
      elementMouseIsOver = document.elementFromPoint(x, y);
    if (elementMouseIsOver.closest('a')) {
      all_array += '"Link-Fold": {"name": "Link", "icon": "fa-external-link", "items": {"fold2-key1": {"name": "Open Site in New Tab"}, "fold2-key2": {"name": "Open Site in Split Tab"}, "fold2-key3": {"name": "Copy URL"}}},';
    }
    if (elementMouseIsOver.closest('img')) {
      all_array += '"Image-Fold": {"name": "Image","icon": "fa-picture-o","items": {"fold1-key1": {"name":"Download Image"},"fold1-key2": {"name": "Copy Image Location"},"fold1-key3": {"name": "Go To Image"}}},';
    }
    all_array += '"copy": {"name": "Copy","icon": "copy"},"paste": {"name": "Paste","icon": "paste"},"edit": {"name": "Edit HTML","icon": "fa-code"}}';
    return JSON.parse(all_array);
  }

  // setup context menu
  $.contextMenu({
    selector: 'body',
    build: function($trigger, e) {
      return {
        callback: function(key, options) {
          var m = "clicked: " + key;
          console.log(m);
        },
        items: createSomeMenu()
      };
    }
  });
});

1
我有一个漂亮且易于实现的使用Bootstrap的方案,如下所示。
<select class="custom-select" id="list" multiple></select>

<div class="dropdown-menu" id="menu-right-click" style=>
    <h6 class="dropdown-header">Actions</h6>
    <a class="dropdown-item" href="" onclick="option1();">Option 1</a>
    <a class="dropdown-item" href="" onclick="option2();">Option 2</a>
</div>

<script>
    $("#menu-right-click").hide();

    $(document).on("contextmenu", "#list", function (e) {
        $("#menu-right-click")
            .css({
                position: 'absolute',
                left: e.pageX,
                top: e.pageY,
                display: 'block'
            })
        return false;
    });

    function option1() {
        // something you want...
        $("#menu-right-click").hide();
    }

    function option2() {
        // something else 
        $("#menu-right-click").hide();
    }
</script>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接