4

我有focusout()event onelement1click()event on element2,当element1因为在 on 上执行了 click 事件而失去焦点时element2,只会触发 focusout,而不会触发 click 事件。

这在[在 jQuery][1] 上工作正常,但在 Angular 中不行。

window.setTimeout()我通过添加也适用于角度的方法找到了解决方法。不幸的是我不能这样做。

非常感谢另一个建议。

请使用 setTimeout 找到代码:



$('#txtName').on("focusout",function(event) {
    //Alternate solution required for `setTimeout`.
    window.setTimeout( function() {alert('focus gone');},1000); }); 

    $('#button1').on('click',function(){
       alert('button click');
    }); 
 }
4

4 回答 4

16

这是点击事件的问题。

点击事件由两个事件组成,mousedown 和 mouseup。

在您的情况下,事件的顺序是这样的

1) mousedown 2) focusout 3) mouseup

其中 1 和 3 进行点击事件。

当页面上显示其他元素(例如错误消息)并且应该发生单击的按钮从其原始 x 和 y 坐标移动时,可能会发生这种情况。因此 mouseup 发生在其他地方,而不是 mousedown 发生的地方。

所以基本上我认为你的 mousedown 有效,focusout 有效,但 mouseup 无效。

解决方案是使用 mousedown 事件而不是单击。因此,您的点击不应等待 mouseup 发生。

例子:

<input type="text" (focusout)="someMethod()">
<button (mousedown)="someMethod()">Click Me!</button> //Changed (click) to (mousedown)

希望这可以帮助。

于 2018-08-27T11:00:29.920 回答
2

我开发了一个不需要“setTimeout”的解决方案,并且不会强制您使用“mouseup”事件而不是单击事件。这是用户友好的,因为单击事件“使用户有机会通过在释放鼠标之前将鼠标移离按钮来中止单击”。(由 piccy 评论)

问题

正如Vinod 的回答中所述,这是事件年表中的一个问题:

  1. mousedown:按钮注册一个 mousedown 事件。
  2. focusout:由于鼠标按下按钮而注册。在这个独特的场景中,focusout 处理程序使按钮移动到另一个位置。
  3. mouseup:由于按钮的位置发生了变化,它不会注册 mouseup 事件。因此,单击事件也不会被注册,因为这需要在同一元素上先按下鼠标,然后再按下鼠标。

解决方案

我的解决方案是一个指令,它公开了在 mousedown 和 mouseup 事件之后发生的延迟聚焦事件。因此,click 事件在(延迟的)focusout 事件的事件处理程序更改按钮的位置之前注册。

这是通过存储鼠标当前是否按下的 BehaviourSubject 来完成的。当鼠标按下时注册了一个 focusout 事件,我们不会立即触发延迟的 focusout 事件(否则我们最终会遇到同样的老问题)。相反,我们等待鼠标再次返回,然后发出延迟的 focusout 事件。这导致以下顺序:

  1. 鼠标按下
  2. focusout(忽略此事件)
  3. mouseup
  4. 延迟聚焦+点击

代码解决方案

该指令的使用方式如下:

<input appDelayedFocusout (delayedFocusout)="yourLayoutChangingHandler()">

我的指令实现使用until-destroy库来防止内存泄漏从永无止境的订阅,但可以随意修改。

import {Directive, EventEmitter, HostListener, OnInit, Output} from '@angular/core';
import {BehaviorSubject, fromEvent} from 'rxjs';
import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy';
import {filter, map, take} from 'rxjs/operators';

/**
 * This directive exposes a special variant of the 'focusout' event. The regular 'focusout' event has a quirk:
 * Imagine the user clicks on some button on the page. This triggers the following events in the following order:
 * mousedown, focusout, mouseup. But the focusout event handler might change the layout of the website so that
 * the button on which the mousedown event occurred moves around. This leads to no mouseup event registered on
 * that button. Therefore a click event is also not registered because a click event consists of
 * a mousedown AND a mouseup event on that button. In order to fix that problem, this directive exposes a delayed focusout
 * event that is triggered AFTER the mousedown and mouseup events. When the delayed focusout event handler changes
 * positions of buttons, click events are still registered as you would expect.
 */
@UntilDestroy()
@Directive({
  selector: '[appDelayedFocusout]'
})
export class DelayedFocusoutDirective implements OnInit {
  @Output() delayedFocusout = new EventEmitter<boolean>();
  isMouseDownSubject = new BehaviorSubject(false);

  ngOnInit(): void {
    fromEvent(document.body, 'mousedown').pipe(untilDestroyed(this))
      .subscribe(() => this.isMouseDownSubject.next(true));
    fromEvent(document.body, 'mouseup').pipe(untilDestroyed(this))
      .subscribe(() => this.isMouseDownSubject.next(false));
  }

  @HostListener('focusout') onFocusout() {
    // If the mouse is currently down, we subscribe to the the event of
    // 'mouse being released' to then trigger the delayed focusout.
    // If the mouse is currently not down, we can trigger the delayed focusout immediately.
    if (this.isMouseDown()) {
      this.mouseRelease().subscribe(() => {
        // This code is executed once the mouse has been released.
        this.delayedFocusout.emit(true);
      });
    } else {
      this.delayedFocusout.emit(true);
    }
  }

  /**
   * Emits the value true once the mouse has been released and then completes.
   * Also completes when the mouse is not released but this directive is being destroyed.
   */
  mouseRelease() {
    return this.isMouseDownSubject.pipe(
      untilDestroyed(this),
      // Just negate isDown to get the value isReleased.
      map(isDown => !isDown),
      // Only proceed when the the mouse is released.
      filter(isReleased => isReleased),
      take(1)
    );
  }

  isMouseDown() {
    return this.isMouseDownSubject.value;
  }
}
于 2020-04-02T19:49:27.110 回答
1

我已经在这个答案中实现了@simon-lammes共享的 DelayedFocusout 指令, 而没有使用until-destroy库。我使用 shareReplay 来销毁订阅。

import { Directive, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
import { BehaviorSubject, fromEvent, ReplaySubject } from 'rxjs';
import { filter, map, take, takeUntil } from 'rxjs/operators';

/**
 * This directive exposes a special variant of the 'focusout' event. The regular 'focusout' event has a quirk:
 * Imagine the user clicks on some button on the page. This triggers the following events in the following order:
 * mousedown, focusout, mouseup. But the focusout event handler might change the layout of the website so that
 * the button on which the mousedown event occurred moves around. This leads to no mouseup event registered on
 * that button. Therefore a click event is also not registered because a click event consists of
 * a mousedown AND a mouseup event on that button. In order to fix that problem, this directive exposes a delayed focusout
 * event that is triggered AFTER the mousedown and mouseup events. When the delayed focusout event handler changes
 * positions of buttons, click events are still registered as you would expect.
 */
@Directive({
  selector: '[appDelayedFocusout]'
})
export class DelayedFocusoutDirective implements OnInit, OnDestroy {

  @Output() delayedFocusout = new EventEmitter<boolean>();
  isMouseDownSubject = new BehaviorSubject(false);

  private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

  ngOnInit(): void {
    fromEvent(document.body, 'mousedown').pipe(takeUntil(this.destroyed$))
      .subscribe(() => this.isMouseDownSubject.next(true));
    fromEvent(document.body, 'mouseup').pipe(takeUntil(this.destroyed$))
      .subscribe(() => this.isMouseDownSubject.next(false));
  }

  @HostListener('focusout') onFocusout() {
    // If the mouse is currently down, we subscribe to the the event of
    // 'mouse being released' to then trigger the delayed focusout.
    // If the mouse is currently not down, we can trigger the delayed focusout immediately.
    if (this.isMouseDown()) {
      this.mouseRelease().subscribe(() => {
        // This code is executed once the mouse has been released.
        this.delayedFocusout.emit(true);
      });
    } else {
      this.delayedFocusout.emit(true);
    }
  }

  /**
   * Emits the value true once the mouse has been released and then completes.
   * Also completes when the mouse is not released but this directive is being destroyed.
   */
  mouseRelease() {
    return this.isMouseDownSubject.pipe(
      // Just negate isDown to get the value isReleased.
      takeUntil(this.destroyed$),
      map(isDown => !isDown),
      // Only proceed when the the mouse is released.
      filter(isReleased => isReleased),
      take(1)
    );
  }

  isMouseDown() {
    return this.isMouseDownSubject.value;
  }

  ngOnDestroy() {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}
于 2021-09-06T10:00:58.537 回答
0

对我来说,鼠标点击事件将焦点设置在不同的元素上,所以当我试图捕捉 focusout 事件时,我没有捕捉到它。

我的场景是一个文本元素,一旦您单击文本元素,它就会被替换为输入元素。

解决方案是在使用 setTimeout 完成单击文本元素后,将注意力集中在编辑元素上

(setTimeout 是必需的,以便在尝试调用 element.focus() 之前完成渲染过程)

于 2021-11-18T12:45:06.927 回答