简体   繁体   English

Angular Click Outside Directive问题以及额外的点击事件

[英]Angular Click Outside Directive issue with an extra click event

I have implemented a custom Click Outside Directive which is meant to be used to close modal dialogs, notifications, popovers, etc (let's call them 'popups' for simplicity). 我已经实现了一个自定义的Click Outside Directive,它用于关闭模式对话框,通知,弹出窗口等(为简单起见,我们称它们为“弹出窗口”)。 However, I'm struggling to implement a generic solution for both popups opened via mouse click (ie button click) and popups opened by some other action (ie a mouse over). 但是,我正在努力为通过鼠标单击(即按钮单击)打开的弹出窗口和通过其他操作(即鼠标悬停)打开的弹出窗口实现通用解决方案。

Here is a Plunker example of the issue I'm facing: the popup produced by the mouseover event needs two clicks to close, while the same popup produced by the 'Click Me' button only needs a single click to be dismissed. 这是我面临的问题的一个Plunker示例 :mouseover事件产生的弹出窗口需要单击两次才能关闭,而“ Click Me”按钮产生的同一弹出窗口只需单击一次即可关闭。

Here is the code for the directive: 这是指令的代码:

@Directive({
  selector: '[clickOutside]'
})
export class ClickOutsideDirective {
  @Output('clickOutside') clickOutsideEvent = new EventEmitter();

  private globalClick: Subscription;

  constructor(private elRef: ElementRef) { }

  ngOnInit() {
    this.globalClick = Observable
      .fromEvent(document, 'click')
      .skip(1)
      .subscribe((event: MouseEvent) => {
        this.clicked(event);
      });
  }

  ngOnDestroy() {
    this.globalClick.unsubscribe();
  }

  private clicked(event: MouseEvent) {
    let clickedInside = this.elRef.nativeElement.contains(event.target);

    if (!clickedInside) {
      this.clickOutsideEvent.emit();
    }
  }
}

Inside ngOnInit() I initialise an observable that listens for the clicks on the document and passes the events to clicked() function, which will check if the click was made outside and raise an event if so. 在ngOnInit()内部,我初始化了一个可观察的对象,该对象可监听文档上的单击并将事件传递给clicked()函数,该函数将检查单击是否在外部进行,如果是,则引发一个事件。 The observable uses a skip operator to filter the first click event. 可观察对象使用跳过运算符过滤首次点击事件。 This is a workaround because if you click the 'Click Me' button, that original click event is the one I want to skip as it is not useful to me inside the directive. 这是一种解决方法,因为如果您单击“ Click Me”按钮,则该原始click事件就是我要跳过的事件,因为它对指令中的我没有用。 If I don't skip it, the popup will close itself, as it will think that there was a click outside. 如果我不跳过它,则弹出窗口将自行关闭,因为它将认为在外部单击。 However, this workaround has a side effect that two clicks are now needed to close a popup which was triggered some other way, other then by a click (the Mouse Over example in Plunker demonstrates this issue). 但是,此替代方法有一个副作用,现在需要两次单击才能关闭弹出窗口,该弹出窗口是通过其他方式触发的,而不是通过单击来触发的(Plunker中的“鼠标悬停”示例演示了此问题)。

I'm struggling to think of an elegant and generic solution to solve this problem. 我正在努力想一个优雅而通用的解决方案来解决这个问题。 I can think of multiple 'hacky' solutions: 我可以想到多种“ hacky”解决方案:

Hacky solution 1: I could stop the event propagation at the 'Click Me' button, meaning that the ClickOutsideDirective will not receive the initial click event and I can remove the .skip operator from the document click observable. Hacky解决方案1:我可以在“ Click Me”按钮处停止事件传播,这意味着ClickOutsideDirective将不会收到初始click事件,并且可以从可观察到的文档单击中删除.skip运算符。 That should theoretically work (have not tried this) but what I don't like is that the consumer of the directive now needs to know that they need to stop the event propagation, and if they don't the directive will not work as designed. 从理论上讲这应该可行(没有尝试过),但我不喜欢的是指令的使用者现在需要知道他们需要停止事件传播,如果不这样做,指令将无法按设计工作。

Hacky solution 2: Pass a boolean flag to the directive which can be used to determine if the first click needs to be skipped or not. Hacky解决方案2:将布尔值标志传递给指令,该标志可用于确定是否需要跳过第一次单击。 Very easy to do, but again, the user of the API now needs to know that they need to set this flag. 很容易做到,但是同样,API的用户现在需要知道他们需要设置此标志。 If they forget, the directive will not work as expected. 如果他们忘记了,该指令将无法按预期工作。

Hacky solution 3: Skip the first event that occurs a short period of time (let's say 100ms) after the directive is initialised. hacky解决方案3:跳过指令初始化后短时间内(例如100ms)发生的第一个事件。 This is also easy to do and the advantage of this solution is that the user of the API does not need to provide any additional information to the directive (or stop any event propagation) and the directive will 'just work'. 这也很容易做到,该解决方案的优势在于,API的用户无需向指令提供任何其他信息(或停止任何事件传播),并且该指令将“正常工作”。 The problem here is to figure out what delay to use that will work across all browsers and hardware. 这里的问题是弄清楚在所有浏览器和硬件上可以使用的延迟时间。 The delay needs to be no smaller then the time necessary for the event to arrive after the directive was instantiated, but not big enough to skip a legitimate user clicks if the user decides to click away as soon as the popup is displayed. 延迟必须不小于实例化指令后事件到达所必需的时间,但如果用户决定在显示弹出窗口后立即单击鼠标,则该延迟必须足够大以跳过合法的用户单击。

Can anyone think of a better, more simple and elegant solution to this problem? 谁能想到一个更好,更简单,更优雅的解决方案?

Instead of keeping the logic in a separate directive, why not keep it in the popup component, seeing as that is what it is responsible for closing. 不要将逻辑保留在单独的指令中,而是为什么不将其保留在popup组件中,因为这是它负责关闭的原因。

Change the component, so that there is a fixed position div with a click listener, positioned behind the popup. 更改组件,以便在弹出窗口后面有一个带有单击侦听器的固定位置div。 Then, whenever it is clicked, send the clickOutside event. 然后,只要单击它,就发送clickOutside事件。

import {Component, Output, EventEmitter} from '@angular/core';

@Component({
  selector: 'popup',
  styles: [`
      .background {
    z-index: 1;
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    backgrouc
      }

      .modal-content {
    left: 0;
    right: 0;
    z-index: 3;
      }
  `],
  template: `
    <div class="background" (click)="close()">
      <div class="modal-content">
    Click outside to close me!
      </div>
    </div>
  `,
})
export class PopupComponent {

  @Output()
  clickOutside = new EventEmitter();

  close() {
    this.clickOutside.emit();
  }
}

Here is a working plunkr: https://plnkr.co/edit/9QBJ0PXOg2GAUpBacgQV?p=preview 这是一个工作正常的plunkr: https ://plnkr.co/edit/9QBJ0PXOg2GAUpBacgQV ? p = preview

So I have finally found an answer to this. 因此,我终于找到了答案。 The solution is not as clean as I was hoping, but basically I have added a timer observable (with a zero value) that will trigger the .fromEvent(document, 'click') observable. 该解决方案不像我希望的那样干净,但是基本上我添加了一个可观察的计时器(值为零),它将触发可观察的.fromEvent(document,'click')。 I have tested this in all modern browsers (including iPad and Android Chrome) and it seem to work fine everywhere. 我已经在所有现代浏览器(包括iPad和Android Chrome)中对此进行了测试,它似乎在任何地方都可以正常工作。

ngOnInit() {
   this.globalClick = Observable
      .timer(0)
      .switchMap(() => {
          return Observable.fromEvent(this.document, 'click');
      })
      .subscribe((event: MouseEvent) => {
        this.clicked(event);
      });
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM