我开发了一个不需要“setTimeout”的解决方案,并且不会强制您使用“mouseup”事件而不是单击事件。这是用户友好的,因为单击事件“使用户有机会通过在释放鼠标之前将鼠标移离按钮来中止单击”。(由 piccy 评论)
问题
正如Vinod 的回答中所述,这是事件年表中的一个问题:
- mousedown:按钮注册一个 mousedown 事件。
- focusout:由于鼠标按下按钮而注册。在这个独特的场景中,focusout 处理程序使按钮移动到另一个位置。
- mouseup:由于按钮的位置发生了变化,它不会注册 mouseup 事件。因此,单击事件也不会被注册,因为这需要在同一元素上先按下鼠标,然后再按下鼠标。
解决方案
我的解决方案是一个指令,它公开了在 mousedown 和 mouseup 事件之后发生的延迟聚焦事件。因此,click 事件在(延迟的)focusout 事件的事件处理程序更改按钮的位置之前注册。
这是通过存储鼠标当前是否按下的 BehaviourSubject 来完成的。当鼠标按下时注册了一个 focusout 事件,我们不会立即触发延迟的 focusout 事件(否则我们最终会遇到同样的老问题)。相反,我们等待鼠标再次返回,然后发出延迟的 focusout 事件。这导致以下顺序:
- 鼠标按下
- focusout(忽略此事件)
- mouseup
- 延迟聚焦+点击
代码解决方案
该指令的使用方式如下:
<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;
}
}