Remove event listeners with AbortSignal in the GrabToPan class

This commit is contained in:
Jonas Jenwald 2024-10-19 11:59:58 +02:00
parent c88d3a31cf
commit c3bbeb51e3

View File

@ -23,6 +23,12 @@ const CSS_CLASS_GRAB = "grab-to-pan-grab";
*/ */
class GrabToPan { class GrabToPan {
#activateAC = null;
#mouseDownAC = null;
#scrollAC = null;
/** /**
* Construct a GrabToPan instance for a given HTML element. * Construct a GrabToPan instance for a given HTML element.
* @param {GrabToPanOptions} options * @param {GrabToPanOptions} options
@ -31,15 +37,6 @@ class GrabToPan {
this.element = element; this.element = element;
this.document = element.ownerDocument; this.document = element.ownerDocument;
// Bind the contexts to ensure that `this` always points to
// the GrabToPan instance.
this.activate = this.activate.bind(this);
this.deactivate = this.deactivate.bind(this);
this.toggle = this.toggle.bind(this);
this._onMouseDown = this.#onMouseDown.bind(this);
this._onMouseMove = this.#onMouseMove.bind(this);
this._endPan = this.#endPan.bind(this);
// This overlay will be inserted in the document when the mouse moves during // This overlay will be inserted in the document when the mouse moves during
// a grab operation, to ensure that the cursor has the desired appearance. // a grab operation, to ensure that the cursor has the desired appearance.
const overlay = (this.overlay = document.createElement("div")); const overlay = (this.overlay = document.createElement("div"));
@ -50,9 +47,13 @@ class GrabToPan {
* Bind a mousedown event to the element to enable grab-detection. * Bind a mousedown event to the element to enable grab-detection.
*/ */
activate() { activate() {
if (!this.active) { if (!this.#activateAC) {
this.active = true; this.#activateAC = new AbortController();
this.element.addEventListener("mousedown", this._onMouseDown, true);
this.element.addEventListener("mousedown", this.#onMouseDown.bind(this), {
capture: true,
signal: this.#activateAC.signal,
});
this.element.classList.add(CSS_CLASS_GRAB); this.element.classList.add(CSS_CLASS_GRAB);
} }
} }
@ -61,16 +62,17 @@ class GrabToPan {
* Removes all events. Any pending pan session is immediately stopped. * Removes all events. Any pending pan session is immediately stopped.
*/ */
deactivate() { deactivate() {
if (this.active) { if (this.#activateAC) {
this.active = false; this.#activateAC.abort();
this.element.removeEventListener("mousedown", this._onMouseDown, true); this.#activateAC = null;
this._endPan();
this.#endPan();
this.element.classList.remove(CSS_CLASS_GRAB); this.element.classList.remove(CSS_CLASS_GRAB);
} }
} }
toggle() { toggle() {
if (this.active) { if (this.#activateAC) {
this.deactivate(); this.deactivate();
} else { } else {
this.activate(); this.activate();
@ -109,12 +111,26 @@ class GrabToPan {
this.scrollTopStart = this.element.scrollTop; this.scrollTopStart = this.element.scrollTop;
this.clientXStart = event.clientX; this.clientXStart = event.clientX;
this.clientYStart = event.clientY; this.clientYStart = event.clientY;
this.document.addEventListener("mousemove", this._onMouseMove, true);
this.document.addEventListener("mouseup", this._endPan, true); this.#mouseDownAC = new AbortController();
const boundEndPan = this.#endPan.bind(this),
mouseOpts = { capture: true, signal: this.#mouseDownAC.signal };
this.document.addEventListener(
"mousemove",
this.#onMouseMove.bind(this),
mouseOpts
);
this.document.addEventListener("mouseup", boundEndPan, mouseOpts);
// When a scroll event occurs before a mousemove, assume that the user // When a scroll event occurs before a mousemove, assume that the user
// dragged a scrollbar (necessary for Opera Presto, Safari and IE) // dragged a scrollbar (necessary for Opera Presto, Safari and IE)
// (not needed for Chrome/Firefox) // (not needed for Chrome/Firefox)
this.element.addEventListener("scroll", this._endPan, true); this.#scrollAC = new AbortController();
this.element.addEventListener("scroll", boundEndPan, {
capture: true,
signal: this.#scrollAC.signal,
});
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -125,10 +141,12 @@ class GrabToPan {
} }
#onMouseMove(event) { #onMouseMove(event) {
this.element.removeEventListener("scroll", this._endPan, true); this.#scrollAC?.abort();
this.#scrollAC = null;
if (!(event.buttons & 1)) { if (!(event.buttons & 1)) {
// The left mouse button is released. // The left mouse button is released.
this._endPan(); this.#endPan();
return; return;
} }
const xDiff = event.clientX - this.clientXStart; const xDiff = event.clientX - this.clientXStart;
@ -145,9 +163,10 @@ class GrabToPan {
} }
#endPan() { #endPan() {
this.element.removeEventListener("scroll", this._endPan, true); this.#mouseDownAC?.abort();
this.document.removeEventListener("mousemove", this._onMouseMove, true); this.#mouseDownAC = null;
this.document.removeEventListener("mouseup", this._endPan, true); this.#scrollAC?.abort();
this.#scrollAC = null;
// Note: ChildNode.remove doesn't throw if the parentNode is undefined. // Note: ChildNode.remove doesn't throw if the parentNode is undefined.
this.overlay.remove(); this.overlay.remove();
} }