我有一些HTML菜单,当用户单击这些菜单的头部时,我会完全显示这些菜单。当用户在菜单区域外单击时,我希望隐藏这些元素。

jQuery是否可以实现这样的功能?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});

当前回答

如何检测元素外部的单击?

这个问题之所以如此流行并有如此多的答案,是因为它看起来很复杂。在经历了近八年的时间和数十个答案之后,我真的很惊讶地发现,人们对无障碍设施的关注程度如此之低。

当用户在菜单区域外单击时,我希望隐藏这些元素。

这是一项崇高的事业,也是实际问题。大多数答案似乎试图解决的问题的标题包含了一个不幸的红鲱鱼。

提示:就是“咔嚓”这个词!

您实际上不想绑定单击处理程序。

如果要绑定单击处理程序以关闭对话框,则已失败。你失败的原因是不是每个人都会触发点击事件。不使用鼠标的用户可以通过按Tab键退出对话框(弹出菜单可以说是一种对话框),然后他们就无法在不触发单击事件的情况下阅读对话框后面的内容。

所以让我们重新表述这个问题。

当用户完成对话框时,如何关闭对话框?

这就是目标。不幸的是,现在我们需要将用户绑定到对话框事件中,而绑定并不那么简单。

那么,我们如何检测用户是否已完成使用对话框?

聚焦事件

一个好的开始是确定焦点是否已离开对话框。

提示:注意模糊事件,如果事件绑定到冒泡阶段,则模糊不会传播!

jQuery的聚焦功能会很好。如果不能使用jQuery,那么可以在捕获阶段使用blur:

element.addEventListener('blur', ..., true);
//                       use capture: ^^^^

此外,对于许多对话框,您需要允许容器获得焦点。添加tabindex=“-1”以允许对话框动态接收焦点,而不会中断选项卡流。

$('a').on('click',函数(){$(this.hash).tggleClass('active').focus();});$('div').on('fout',函数(){$(this).removeClass('active');});第二部分{显示:无;}.活动{显示:块;}<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><a href=“#example”>示例</a><div id=“example”tabindex=“-1”>Lorem ipsum<a href=“http://example.com“>dolor</a>坐下。</div>


如果你玩了一分钟以上的演示,你应该很快开始发现问题。

第一个是对话框中的链接不可单击。尝试单击它或选项卡将导致对话框在交互发生之前关闭。这是因为在再次触发聚焦事件之前,聚焦内部元素会触发聚焦事件。

修复方法是在事件循环中对状态更改进行排队。这可以通过对不支持setImmediate的浏览器使用setImmediat(…)或setTimeout(…,0)来实现。排队后,可通过后续聚焦取消:

$('.submenu').on({
  focusout: function (e) {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function (e) {
    clearTimeout($(this).data('submenuTimer'));
  }
});

$('a').on('click',函数(){$(this.hash).tggleClass('active').focus();});$('div').在({focusout:函数(){$(this).data('timer',setTimeout(函数(){$(this).removeClass('active');}.bind(this),0));},focusin:函数(){clearTimeout($(this).data('timer'));}});第二部分{显示:无;}.活动{显示:块;}<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><a href=“#example”>示例</a><div id=“example”tabindex=“-1”>Lorem ipsum<a href=“http://example.com“>dolor</a>坐下。</div>

第二个问题是,当再次按下链接时,对话框不会关闭。这是因为对话框失去焦点,触发关闭行为,之后单击链接会触发对话框重新打开。

与上一期类似,需要管理焦点状态。鉴于状态更改已经排队,只需处理对话框触发器上的焦点事件:

这看起来应该很熟悉

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});

$('a').on('click',函数(){$(this.hash).tggleClass('active').focus();});$('div').在({focusout:函数(){$(this).data('timer',setTimeout(函数(){$(this).removeClass('active');}.bind(this),0));},focusin:函数(){clearTimeout($(this).data('timer'));}});$('a').开({focusout:函数(){$(this.hash).data('timer',setTimeout(函数(){$(this.hash).removeClass('active');}.bind(this),0));},focusin:函数(){clearTimeout($(this.hash).data('timer'));}});第二部分{显示:无;}.活动{显示:块;}<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><a href=“#example”>示例</a><div id=“example”tabindex=“-1”>Lorem ipsum<a href=“http://example.com“>dolor</a>坐下。</div>


Esc键

如果你认为你已经处理好了焦点状态,那么你可以做更多的事情来简化用户体验。

这通常是一个“很好拥有”的功能,但当您有任何类型的模式或弹出窗口时,Esc键都会关闭它。

keydown: function (e) {
  if (e.which === 27) {
    $(this).removeClass('active');
    e.preventDefault();
  }
}

$('a').on('click',函数(){$(this.hash).tggleClass('active').focus();});$('div').在({focusout:函数(){$(this).data('timer',setTimeout(函数(){$(this).removeClass('active');}.bind(this),0));},focusin:函数(){clearTimeout($(this).data('timer'));},向下键:函数(e){如果(e.whic===27){$(this).removeClass('active');e.预防违约();}}});$('a').开({focusout:函数(){$(this.hash).data('timer',setTimeout(函数(){$(this.hash).removeClass('active');}.bind(this),0));},focusin:函数(){clearTimeout($(this.hash).data('timer'));}});第二部分{显示:无;}.活动{显示:块;}<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><a href=“#example”>示例</a><div id=“example”tabindex=“-1”>Lorem ipsum<a href=“http://example.com“>dolor</a>坐下。</div>


如果您知道对话框中有可聚焦元素,则不需要直接聚焦对话框。如果你正在构建一个菜单,你可以把焦点放在第一个菜单项上。

click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}

$('.menu__link').上({点击:函数(e){$(this.hash).tggleClass('submenu--active').find('a:first').focus();e.预防违约();},focusout:函数(){$(this.hash).data('subenuTimer',setTimeout(函数(){$(this.hash).removeClass('submenu--active');}.bind(this),0));},focusin:函数(){clearTimeout($(this.hash).data('subenuTimer'));}});$('.submenu').打开({focusout:函数(){$(this).data('subenuTimer',setTimeout(函数(){$(this).removeClass('ssubmenu--active');}.bind(this),0));},focusin:函数(){clearTimeout($(this).data('subenuTimer'));},向下键:函数(e){如果(e.whic===27){$(this).removeClass('ssubmenu--active');e.预防违约();}}});.菜单{列表样式:无;边距:0;填充:0;}.menu:之后{清晰:两者都有;内容:“”;显示:表格;}.menu__项{浮动:左侧;位置:相对;}.menu__链接{背景色:浅蓝色;颜色:黑色;显示:块;衬垫:0.5em 1em;文本装饰:无;}.menu__link:悬停,.menu__link:焦点{背景色:黑色;颜色:浅蓝色;}.子菜单{边框:1px实心黑色;显示:无;左:0;列表样式:无;边距:0;填充:0;位置:绝对;顶部:100%;}.子菜单--活动{显示:块;}.subenu__项{宽度:150px;}.subenu__link{背景色:浅蓝色;颜色:黑色;显示:块;衬垫:0.5em 1em;文本装饰:无;}.subenu__link:悬停,.subenu__link:焦点{背景色:黑色;颜色:浅蓝色;}<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><ul class=“menu”><li class=“menu__item”>菜单1</a><ul class=“submenu”id=“menu-1”tabindex=“-1”><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#1“>示例1</a></li><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#2“>示例2</a></li><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#3“>示例3</a></li><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#4“>示例4</a></li></ul></li><li class=“menu__item”>菜单2</a><ul class=“submenu”id=“menu-2”tabindex=“-1”><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#1“>示例1</a></li><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#2“>示例2</a></li><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#3“>示例3</a></li><li class=“submenu__item”><a class=“subenu__link”href=“http://example.com/#4“>示例4</a></li></ul></li></ul>lorem ipsum<a href=“http://example.com/“>dolor</a>坐下。


WAI-ARIA角色和其他辅助功能支持

这个答案希望涵盖了对该功能的可访问键盘和鼠标支持的基础知识,但由于它已经相当大,我将避免任何关于WAI-ARIA角色和属性的讨论,但是我强烈建议实现者参考规范,以了解他们应该使用的角色和任何其他适当属性的详细信息。

其他回答

要做到这一点,最广泛的方法是选择网页上的所有内容,除了不希望检测到点击的元素,并在打开菜单时绑定点击事件。

然后,当菜单关闭时,移除绑定。

使用.stopPropagation可防止事件影响菜单容器的任何部分。

$("*").not($("#menuscontainer")).bind("click.OutsideMenus", function ()
{
    // hide the menus

    //then remove all of the handlers
    $("*").unbind(".OutsideMenus");
});

$("#menuscontainer").bind("click.OutsideMenus", function (event) 
{
    event.stopPropagation(); 
});

标记为接受答案的答案没有考虑到元素上可以有覆盖,如对话框、弹出窗口、日期选择器等。单击这些按钮不应隐藏元素。

我制作了自己的版本,确实考虑到了这一点。它是作为KnockoutJS绑定创建的,但它可以很容易地仅转换为jQuery。

它通过第一次查询所有具有z索引或绝对位置的可见元素来工作。然后,如果在外部单击,它会根据我想要隐藏的元素来测试这些元素。如果是命中,我会计算一个新的边界矩形,该矩形考虑到覆盖边界。

ko.bindingHandlers.clickedIn = (function () {
    function getBounds(element) {
        var pos = element.offset();
        return {
            x: pos.left,
            x2: pos.left + element.outerWidth(),
            y: pos.top,
            y2: pos.top + element.outerHeight()
        };
    }

    function hitTest(o, l) {
        function getOffset(o) {
            for (var r = { l: o.offsetLeft, t: o.offsetTop, r: o.offsetWidth, b: o.offsetHeight };
                o = o.offsetParent; r.l += o.offsetLeft, r.t += o.offsetTop);
            return r.r += r.l, r.b += r.t, r;
        }

        for (var b, s, r = [], a = getOffset(o), j = isNaN(l.length), i = (j ? l = [l] : l).length; i;
            b = getOffset(l[--i]), (a.l == b.l || (a.l > b.l ? a.l <= b.r : b.l <= a.r))
                && (a.t == b.t || (a.t > b.t ? a.t <= b.b : b.t <= a.b)) && (r[r.length] = l[i]));
        return j ? !!r.length : r;
    }

    return {
        init: function (element, valueAccessor) {
            var target = valueAccessor();
            $(document).click(function (e) {
                if (element._clickedInElementShowing === false && target()) {
                    var $element = $(element);
                    var bounds = getBounds($element);

                    var possibleOverlays = $("[style*=z-index],[style*=absolute]").not(":hidden");
                    $.each(possibleOverlays, function () {
                        if (hitTest(element, this)) {
                            var b = getBounds($(this));
                            bounds.x = Math.min(bounds.x, b.x);
                            bounds.x2 = Math.max(bounds.x2, b.x2);
                            bounds.y = Math.min(bounds.y, b.y);
                            bounds.y2 = Math.max(bounds.y2, b.y2);
                        }
                    });

                    if (e.clientX < bounds.x || e.clientX > bounds.x2 ||
                        e.clientY < bounds.y || e.clientY > bounds.y2) {

                        target(false);
                    }
                }
                element._clickedInElementShowing = false;
            });

            $(element).click(function (e) {
                e.stopPropagation();
            });
        },
        update: function (element, valueAccessor) {
            var showing = ko.utils.unwrapObservable(valueAccessor());
            if (showing) {
                element._clickedInElementShowing = true;
            }
        }
    };
})();

我不认为你真正需要的是当用户在外面点击时关闭菜单;您需要的是当用户单击页面上的任何位置时关闭菜单。如果你点击菜单,或者离开菜单,它应该会关闭吗?

发现上面没有令人满意的答案,促使我前几天写了这篇博文。对于更迂腐的人,有很多陷阱需要注意:

如果在单击时将单击事件处理程序附加到body元素,请确保在关闭菜单并解除事件绑定之前等待第二次单击。否则,打开菜单的单击事件将弹出到必须关闭菜单的侦听器。若在单击事件上使用event.stopProposition(),则页面中的其他元素都不能在任何位置单击以关闭功能。无限期地将click事件处理程序附加到body元素不是一个高性能的解决方案将事件的目标及其父级与处理程序的创建者进行比较时,假设您想要的是在单击关闭菜单时关闭菜单,而真正想要的是当您单击页面上的任何位置时关闭菜单。监听body元素上的事件会使代码更加脆弱。像这样无辜的风格会破坏它:body{margin-left:auto;margin-right:auto;width:960px;}

订阅捕获阶段的点击以处理调用preventDefault的点击元素。使用其他名称在文档元素上重新触发它,单击任意位置。

document.addEventListener('click', function (event) {
  event = $.event.fix(event);
  event.type = 'click-anywhere';
  $document.trigger(event);
}, true);

然后,在需要单击外部功能的地方,订阅文档上的单击任意位置事件,并检查单击是否在您感兴趣的元素之外:

$(document).on('click-anywhere', function (event) {
  if (!$(event.target).closest('#smth').length) {
    // Do anything you need here
  }
});

一些注意事项:

您必须使用文档,因为在发生单击的所有元素上触发事件是性能错误。这个功能可以封装到特殊的插件中,在外部点击时调用一些回调。您不能使用jQuery本身订阅捕获阶段。您不需要加载文档来订阅,因为订阅是在文档上进行的,甚至不在其主体上,所以它总是独立于脚本放置和加载状态而存在。

$('html').click(function(){//隐藏菜单(如果可见)});$('#menucontainer').click(函数(事件){event.stopPropagation();});<script src=“https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js“></script><html><button id=“#menucontainer”>确定</button></html>