Javascript 如何在外部点击时关闭下拉菜单?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/35712379/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How can I close a dropdown on click outside?
提问by Clement
I would like to close my login menu dropdown when the user click anywhere outside of that dropdown, and I'd like to do that with Angular2 and with the Angular2 "approach"...
当用户点击该下拉菜单之外的任何地方时,我想关闭我的登录菜单下拉菜单,我想用 Angular2 和 Angular2“方法”来做到这一点......
I have implemented a solution, but I really do not feel confident with it. I think there must be an easiest way to achieve the same result, so if you have any ideas ... let's discuss :) !
我已经实施了一个解决方案,但我真的对它没有信心。我认为必须有一种最简单的方法来实现相同的结果,所以如果您有任何想法......让我们讨论 :) !
Here is my implementation:
这是我的实现:
The dropdown component:
下拉组件:
This is the component for my dropdown:
这是我的下拉列表的组件:
- Every time this component it set to visible, (For example: when the user click on a button to display it) it subscribe to a "global" rxjs subject userMenustored within the SubjectsService.
- And every time it is hidden, it unsubscribe to this subject.
- Every click anywhere withinthe template of this component trigger the onClick()method, which just stop event bubbling to the top (and the application component)
- 每次将此组件设置为可见时(例如:当用户单击按钮以显示它时),它都会订阅存储在SubjectsService 中的“全局”rxjs 主题userMenu。
- 每次隐藏时,它都会取消订阅此主题。
- 每次单击此组件模板内的任何位置都会触发onClick()方法,该方法只会停止事件冒泡到顶部(和应用程序组件)
Here is the code
这是代码
export class UserMenuComponent {
_isVisible: boolean = false;
_subscriptions: Subscription<any> = null;
constructor(public subjects: SubjectsService) {
}
onClick(event) {
event.stopPropagation();
}
set isVisible(v) {
if( v ){
setTimeout( () => {
this._subscriptions = this.subjects.userMenu.subscribe((e) => {
this.isVisible = false;
})
}, 0);
} else {
this._subscriptions.unsubscribe();
}
this._isVisible = v;
}
get isVisible() {
return this._isVisible;
}
}
The application component:
应用组件:
On the other hand, there is the application component (which is a parent of the dropdown component):
另一方面,还有应用程序组件(它是下拉组件的父级):
- This component catch every click event and emit on the same rxjs Subject (userMenu)
- 该组件捕获每个点击事件并在同一个 rxjs 主题(userMenu)上发出
Here is the code:
这是代码:
export class AppComponent {
constructor( public subjects: SubjectsService) {
document.addEventListener('click', () => this.onClick());
}
onClick( ) {
this.subjects.userMenu.next({});
}
}
What bother me:
什么困扰我:
- I do not feel really comfortable with the idea of having a global Subject that act as the connector between those components.
- The setTimeout: This is needed because here is what happen otherwise if the user click on the button that show the dropdown:
- The user click on the button (which is not a part of the dropdown component) to show the dropdown.
- The dropdown is displayed and it immediately subscribe to the userMenu subject.
- The click event bubble up to the app component and gets caught
- The application component emit an event on the userMenusubject
- The dropdown component catch this action on userMenuand hide the dropdown.
- At the end the dropdown is never displayed.
- 我对拥有一个全局主题作为这些组件之间的连接器的想法感到非常不舒服。
- 该setTimeout的:这是必要的,因为这里是,如果在此按钮,用户点击,显示的下拉什么,否则会发生:
- 用户单击按钮(它不是下拉组件的一部分)以显示下拉列表。
- 显示下拉菜单并立即订阅 userMenu 主题。
- 单击事件冒泡到应用程序组件并被捕获
- 应用程序组件在userMenu主题上发出一个事件
- 下拉组件在userMenu上捕获此操作并隐藏下拉列表。
- 最后,下拉列表永远不会显示。
This set timeout delay the subscription to the end of the current JavaScript code turn which solve the problem, but in a very in elegant way in my opinion.
这个设置超时将订阅延迟到当前 JavaScript 代码轮的末尾,这解决了问题,但在我看来以一种非常优雅的方式。
If you know cleaner, better, smarter, faster or stronger solutions, please let me know :) !
如果您知道更清洁、更好、更智能、更快或更强大的解决方案,请告诉我:)!
回答by Sasxa
You can use (document:click)
event:
您可以使用(document:click)
事件:
@Component({
host: {
'(document:click)': 'onClick($event)',
},
})
class SomeComponent() {
constructor(private _eref: ElementRef) { }
onClick(event) {
if (!this._eref.nativeElement.contains(event.target)) // or some similar check
doSomething();
}
}
Another approach is to create custom event as a directive. Check out these posts by Ben Nadel:
另一种方法是创建自定义事件作为指令。查看 Ben Nadel 的这些帖子:
回答by Kamil Kie?czewski
ELEGANT METHOD
优雅的方法
I found this clickOut
directive:
https://github.com/chliebel/angular2-click-outside. I check it and it works well (i only copy clickOutside.directive.ts
to my project). U can use it in this way:
我找到了这个clickOut
指令:https:
//github.com/chliebel/angular2-click-outside。我检查它并且它运行良好(我只复制clickOutside.directive.ts
到我的项目)。你可以这样使用它:
<div (clickOutside)="close($event)"></div>
Where close
is your function which will be call when user click outside div. It is very elegant way to handle problem described in question.
close
当用户在 div 外单击时将调用的函数在哪里。这是处理问题中描述的问题的非常优雅的方式。
If you use above directive to close popUp window, remember first to add event.stopPropagation()
to button click event handler which open popUp.
如果您使用上述指令关闭弹出窗口,请记住首先添加event.stopPropagation()
到打开弹出窗口的按钮单击事件处理程序。
BONUS:
奖金:
Below i copy oryginal directive code from file clickOutside.directive.ts
(in case if link will stop working in future) - the author is Christian Liebel:
下面我从文件中复制原始指令代码clickOutside.directive.ts
(以防链接将来停止工作) - 作者是Christian Liebel:
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core';
@Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
constructor(private _elementRef: ElementRef) {
}
@Output()
public clickOutside = new EventEmitter<MouseEvent>();
@HostListener('document:click', ['$event', '$event.target'])
public onClick(event: MouseEvent, targetElement: HTMLElement): void {
if (!targetElement) {
return;
}
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
if (!clickedInside) {
this.clickOutside.emit(event);
}
}
}
回答by Tony
I've done it this way.
我是这样做的。
Added an event listener on document click
and in that handler checked if my container
contains event.target
, if not - hide the dropdown.
在文档上添加了一个事件侦听器,click
并在该处理程序中检查我是否container
包含event.target
,如果没有 - 隐藏下拉列表。
It would look like this.
它看起来像这样。
@Component({})
class SomeComponent {
@ViewChild('container') container;
@ViewChild('dropdown') dropdown;
constructor() {
document.addEventListener('click', this.offClickHandler.bind(this)); // bind on doc
}
offClickHandler(event:any) {
if (!this.container.nativeElement.contains(event.target)) { // check click origin
this.dropdown.nativeElement.style.display = "none";
}
}
}
回答by JuHarm89
I think Sasxa accepted answer works for most people. However, I had a situation, where the content of the Element, that should listen to off-click events, changed dynamically. So the Elements nativeElement did not contain the event.target, when it was created dynamically. I could solve this with the following directive
我认为 Sasxa 接受的答案适用于大多数人。但是,我遇到了一种情况,即应该侦听关闭点击事件的 Element 的内容会动态更改。因此,当动态创建时,Elements nativeElement 不包含 event.target。我可以用以下指令解决这个问题
@Directive({
selector: '[myOffClick]'
})
export class MyOffClickDirective {
@Output() offClick = new EventEmitter();
constructor(private _elementRef: ElementRef) {
}
@HostListener('document:click', ['$event.path'])
public onGlobalClick(targetElementPath: Array<any>) {
let elementRefInPath = targetElementPath.find(e => e === this._elementRef.nativeElement);
if (!elementRefInPath) {
this.offClick.emit(null);
}
}
}
Instead of checking if elementRef contains event.target, I check if elementRef is in the path (DOM path to target) of the event. That way it is possible to handle dynamically created Elements.
我没有检查 elementRef 是否包含 event.target,而是检查 elementRef 是否在事件的路径(到目标的 DOM 路径)中。这样就可以处理动态创建的元素。
回答by Xavier
If you're doing this on iOS, use the touchstart
event as well:
如果您在 iOS 上执行此操作,也请使用该touchstart
事件:
As of Angular 4, the HostListener
decorate is the preferred way to do this
从 Angular 4 开始,HostListener
装饰是执行此操作的首选方式
import { Component, OnInit, HostListener, ElementRef } from '@angular/core';
...
@Component({...})
export class MyComponent implement OnInit {
constructor(private eRef: ElementRef){}
@HostListener('document:click', ['$event'])
@HostListener('document:touchstart', ['$event'])
handleOutsideClick(event) {
// Some kind of logic to exclude clicks in Component.
// This example is borrowed Kamil's answer
if (!this.eRef.nativeElement.contains(event.target) {
doSomethingCool();
}
}
}
回答by Paige Bolduc
We've been working on a similar issue at work today, trying to figure out how to make a dropdown div disappear when it is clicked off of. Ours is slightly different than the initial poster's question because we didn't want to click away from a different componentor directive, but merely outside of the particular div.
我们今天在工作中一直在研究类似的问题,试图弄清楚如何让下拉 div 在被点击时消失。我们的问题与最初发布者的问题略有不同,因为我们不想点击不同的组件或指令,而只是在特定的 div 之外。
We ended up solving it by using the (window:mouseup) event handler.
我们最终通过使用 (window:mouseup) 事件处理程序解决了它。
Steps:
1.) We gave the entire dropdown menu div a unique class name.
2.) On the inner dropdown menu itself (the only portion that we wanted clicks to NOT close the menu), we added a (window:mouseup) event handler and passed in the $event.
NOTE: It could not be done with a typical "click" handler because this conflicted with the parent click handler.
3.) In our controller, we created the method that we wanted to be called on the click out event, and we use the event.closest (docs here) to find out if the clicked spot is within our targeted-class div.
步骤:
1.) 我们给整个下拉菜单 div 一个唯一的类名。
2.) 在内部下拉菜单本身(我们希望点击不关闭菜单的唯一部分),我们添加了一个 (window:mouseup) 事件处理程序并传入了 $event。
注意:它不能用典型的“点击”处理程序完成,因为这与父点击处理程序冲突。
3.) 在我们的控制器中,我们创建了我们想要在点击事件上调用的方法,我们使用 event.closest(此处的文档)来确定点击的位置是否在我们的目标类 div 内。
autoCloseForDropdownCars(event) {
var target = event.target;
if (!target.closest(".DropdownCars")) {
// do whatever you want here
}
}
<div class="DropdownCars">
<span (click)="toggleDropdown(dropdownTypes.Cars)" class="searchBarPlaceholder">Cars</span>
<div class="criteriaDropdown" (window:mouseup)="autoCloseForDropdownCars($event)" *ngIf="isDropdownShown(dropdownTypes.Cars)">
</div>
</div>
回答by Patrick Graham
You could create a sibling element to the dropdown that covers the entire screen that would be invisible and be there just for capturing click events. Then you could detect clicks on that element and close the dropdown when it is clicked. Lets say that element is of class silkscreen, here is some style for it:
您可以为覆盖整个屏幕的下拉列表创建一个兄弟元素,该元素将不可见并且仅用于捕获点击事件。然后您可以检测对该元素的点击并在点击时关闭下拉列表。让我们说元素是丝印类,这是它的一些样式:
.silkscreen {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
The z-index needs to be high enough to position it above everything but your dropdown. In this case my dropdown would b z-index 2.
z-index 需要足够高以将其定位在除下拉列表之外的所有内容之上。在这种情况下,我的下拉列表将 b z-index 2。
The other answers worked in some cases for me, except sometimes my dropdown closed when I interacted with elements within it and I didn't want that. I had dynamically added elements who were not contained in my component, according to the event target, like I expected. Rather than sorting that mess out I figured I'd just try it the silkscreen way.
其他答案在某些情况下对我有用,除了有时当我与其中的元素交互时我的下拉菜单关闭,我不想要那样。我根据事件目标动态添加了未包含在我的组件中的元素,就像我预期的那样。我想我不会用丝网印刷的方式来解决这个问题。
回答by Elie Nehmé
I didn't make any workaround. I've just attached document:click on my toggle function as follow :
我没有做任何解决方法。我刚刚附加了文档:单击我的切换功能,如下所示:
@Directive({ selector: '[appDropDown]' }) export class DropdownDirective implements OnInit { @HostBinding('class.open') isOpen: boolean; constructor(private elemRef: ElementRef) { } ngOnInit(): void { this.isOpen = false; } @HostListener('document:click', ['$event']) @HostListener('document:touchstart', ['$event']) toggle(event) { if (this.elemRef.nativeElement.contains(event.target)) { this.isOpen = !this.isOpen; } else { this.isOpen = false; } }
So, when I am outside my directive, I close the dropdown.
因此,当我超出指令范围时,我会关闭下拉菜单。
回答by Alex Egli
import { Component, HostListener } from '@angular/core';
@Component({
selector: 'custom-dropdown',
template: `
<div class="custom-dropdown-container">
Dropdown code here
</div>
`
})
export class CustomDropdownComponent {
thisElementClicked: boolean = false;
constructor() { }
@HostListener('click', ['$event'])
onLocalClick(event: Event) {
this.thisElementClicked = true;
}
@HostListener('document:click', ['$event'])
onClick(event: Event) {
if (!this.thisElementClicked) {
//click was outside the element, do stuff
}
this.thisElementClicked = false;
}
}
DOWNSIDES: - Two click event listeners for every one of these components on page. Don't use this on components that are on the page hundreds of times.
缺点: - 页面上这些组件中的每一个都有两个单击事件侦听器。不要在页面上数百次的组件上使用它。
回答by Alex Mikitevich
You can write directive:
您可以编写指令:
@Directive({
selector: '[clickOut]'
})
export class ClickOutDirective implements AfterViewInit {
@Input() clickOut: boolean;
@Output() clickOutEvent: EventEmitter<any> = new EventEmitter<any>();
@HostListener('document:mousedown', ['$event']) onMouseDown(event: MouseEvent) {
if (this.clickOut &&
!event.path.includes(this._element.nativeElement))
{
this.clickOutEvent.emit();
}
}
}
In your component:
在您的组件中:
@Component({
selector: 'app-root',
template: `
<h1 *ngIf="isVisible"
[clickOut]="true"
(clickOutEvent)="onToggle()"
>{{title}}</h1>
`,
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
title = 'app works!';
isVisible = false;
onToggle() {
this.isVisible = !this.isVisible;
}
}
This directive emit event when html element is containing in DOM and when [clickOut] input property is 'true'. It listen mousedown event to handle event before element will be removed from DOM.
当 html 元素包含在 DOM 中并且 [clickOut] 输入属性为“true”时,此指令会发出事件。在元素从 DOM 中移除之前,它会监听 mousedown 事件来处理事件。
And one note: firefox doesn't contain property 'path' on event you can use function to create path:
一个注意事项:firefox 不包含事件的属性“路径”,您可以使用函数创建路径:
const getEventPath = (event: Event): HTMLElement[] => {
if (event['path']) {
return event['path'];
}
if (event['composedPath']) {
return event['composedPath']();
}
const path = [];
let node = <HTMLElement>event.target;
do {
path.push(node);
} while (node = node.parentElement);
return path;
};
So you should change event handler on the directive: event.path should be replaced getEventPath(event)
所以你应该改变指令上的事件处理程序: event.path 应该被替换为 getEventPath(event)
This module can help. https://www.npmjs.com/package/ngx-clickoutIt contains the same logic but also handle esc event on source html element.
这个模块可以提供帮助。https://www.npmjs.com/package/ngx-clickout它包含相同的逻辑,但也处理源 html 元素上的 esc 事件。