Skip to content

Commit 9f3c369

Browse files
authored
perf: centralize global event listeners in widgetWindows.js (#6671)
1 parent 4afb26c commit 9f3c369

File tree

2 files changed

+92
-92
lines changed

2 files changed

+92
-92
lines changed

js/widgets/__tests__/widgetWindows.test.js

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ beforeEach(() => {
8181
};
8282
window.widgetWindows.openWindows = {};
8383
window.widgetWindows._posCache = {};
84+
window.widgetWindows._globalListenersInitialized = false;
85+
window.widgetWindows.draggingWindow = null;
8486
// Restore functions
8587
Object.assign(window.widgetWindows, savedFunctions);
8688
});
@@ -105,12 +107,6 @@ describe("widgetWindows", () => {
105107
expect(win._rolled).toBe(false);
106108
});
107109

108-
test("creates a window with _dragging set to false", () => {
109-
const win = createTestWindow();
110-
111-
expect(win._dragging).toBe(false);
112-
});
113-
114110
test("creates a window with _buttons as empty array", () => {
115111
const win = createTestWindow();
116112

@@ -157,6 +153,22 @@ describe("widgetWindows", () => {
157153

158154
expect(win._title).toBe("My Custom Title");
159155
});
156+
157+
test("registers global listeners only once regardless of window count", () => {
158+
const addSpy = jest.spyOn(document, "addEventListener");
159+
160+
createTestWindow("Window 1");
161+
createTestWindow("Window 2");
162+
createTestWindow("Window 3");
163+
164+
// mouseup, mousemove, mousedown (each once)
165+
const globalMouseListeners = addSpy.mock.calls.filter(call =>
166+
["mouseup", "mousemove", "mousedown"].includes(call[0])
167+
);
168+
expect(globalMouseListeners).toHaveLength(3);
169+
170+
addSpy.mockRestore();
171+
});
160172
});
161173

162174
describe("_create helper", () => {
@@ -359,36 +371,16 @@ describe("widgetWindows", () => {
359371
expect(spy).toHaveBeenCalled();
360372
});
361373

362-
test("removes mouseup event listener", () => {
363-
const win = createTestWindow();
364-
const removeSpy = jest.spyOn(document, "removeEventListener");
365-
win.onclose = jest.fn();
366-
367-
win.close();
368-
369-
expect(removeSpy).toHaveBeenCalledWith("mouseup", win._dragTopHandler, true);
370-
removeSpy.mockRestore();
371-
});
372-
373-
test("removes mousemove event listener", () => {
374-
const win = createTestWindow();
374+
test("does not remove global listeners (delegation persists)", () => {
375375
const removeSpy = jest.spyOn(document, "removeEventListener");
376-
win.onclose = jest.fn();
377-
378-
win.close();
379-
380-
expect(removeSpy).toHaveBeenCalledWith("mousemove", win._docMouseMoveHandler, true);
381-
removeSpy.mockRestore();
382-
});
383-
384-
test("removes mousedown event listener", () => {
385376
const win = createTestWindow();
386-
const removeSpy = jest.spyOn(document, "removeEventListener");
387-
win.onclose = jest.fn();
388377

389378
win.close();
390379

391-
expect(removeSpy).toHaveBeenCalledWith("mousedown", win._docMouseDownHandler, true);
380+
const globalMouseRemovals = removeSpy.mock.calls.filter(call =>
381+
["mouseup", "mousemove", "mousedown"].includes(call[0])
382+
);
383+
expect(globalMouseRemovals).toHaveLength(0);
392384
removeSpy.mockRestore();
393385
});
394386
});
@@ -424,15 +416,16 @@ describe("widgetWindows", () => {
424416
expect(window.widgetWindows.openWindows[key]).toBeUndefined();
425417
});
426418

427-
test("removes all three event listeners", () => {
428-
const win = createTestWindow();
419+
test("does not remove global listeners (delegation persists)", () => {
429420
const removeSpy = jest.spyOn(document, "removeEventListener");
421+
const win = createTestWindow();
430422

431423
win.destroy();
432424

433-
expect(removeSpy).toHaveBeenCalledWith("mouseup", win._dragTopHandler, true);
434-
expect(removeSpy).toHaveBeenCalledWith("mousemove", win._docMouseMoveHandler, true);
435-
expect(removeSpy).toHaveBeenCalledWith("mousedown", win._docMouseDownHandler, true);
425+
const globalMouseRemovals = removeSpy.mock.calls.filter(call =>
426+
["mouseup", "mousemove", "mousedown"].includes(call[0])
427+
);
428+
expect(globalMouseRemovals).toHaveLength(0);
436429
removeSpy.mockRestore();
437430
});
438431

js/widgets/widgetWindows.js

Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ window.widgetWindows = {
2121
openWindows: {},
2222
_posCache: {},
2323
focused: null,
24+
draggingWindow: null,
2425
_shortcutsInitialized: false,
26+
_globalListenersInitialized: false,
2527
_handleGlobalKeyDown(e) {
2628
const focused = window.widgetWindows.focused;
2729
if (!focused || e.repeat) return; // Guard against no focus or rapid-fire repeat
@@ -60,6 +62,65 @@ window.widgetWindows = {
6062
e.stopPropagation();
6163
}
6264
}
65+
},
66+
_initGlobalListeners() {
67+
if (this._globalListenersInitialized) return;
68+
69+
this._handleGlobalMouseMove = this._handleGlobalMouseMove.bind(this);
70+
this._handleGlobalMouseUp = this._handleGlobalMouseUp.bind(this);
71+
this._handleGlobalMouseDown = this._handleGlobalMouseDown.bind(this);
72+
73+
document.addEventListener("mouseup", this._handleGlobalMouseUp, true);
74+
document.addEventListener("mousemove", this._handleGlobalMouseMove, true);
75+
document.addEventListener("mousedown", this._handleGlobalMouseDown, true);
76+
77+
this._globalListenersInitialized = true;
78+
},
79+
_handleGlobalMouseMove(e) {
80+
if (this.draggingWindow) {
81+
this.draggingWindow._docMouseMoveHandler(e);
82+
}
83+
},
84+
_handleGlobalMouseUp(e) {
85+
if (this.draggingWindow) {
86+
this.draggingWindow._dragTopHandler(e);
87+
this.draggingWindow = null;
88+
}
89+
},
90+
_handleGlobalMouseDown(e) {
91+
const isToolbarInteraction =
92+
e.target?.closest &&
93+
(e.target.closest("#toolbars") ||
94+
e.target.closest("#aux-toolbar") ||
95+
e.target.closest(".dropdown-content") ||
96+
e.target.closest(".dropdown-trigger"));
97+
98+
const windows = Object.values(this.openWindows).filter(win => win !== undefined);
99+
let focusedAny = false;
100+
101+
for (let i = 0; i < windows.length; i++) {
102+
const win = windows[i];
103+
if (
104+
e.target === win._frame ||
105+
win._frame.contains(e.target) ||
106+
win._fullscreenEnabled ||
107+
isToolbarInteraction
108+
) {
109+
// Focus this window
110+
win._frame.style.opacity = "1";
111+
win._frame.style.zIndex = "10000";
112+
this.focused = win;
113+
focusedAny = true;
114+
} else {
115+
// Dim other windows
116+
win._frame.style.opacity = ".7";
117+
win._frame.style.zIndex = "0";
118+
}
119+
}
120+
121+
if (!focusedAny) {
122+
this.focused = null;
123+
}
63124
}
64125
};
65126

@@ -83,21 +144,13 @@ class WidgetWindow {
83144

84145
// Drag offset for correct positioning
85146
this._dx = this._dy = 0;
86-
this._dragging = false;
87147
// RAF throttle flag for mousemove performance
88148
this._rafTicking = false;
89149

90150
this._createUIelements();
91151
this._setupLanguage();
92152

93-
// Global watchers
94-
this._dragTopHandler = this._dragTopHandler.bind(this);
95-
this._docMouseMoveHandler = this._docMouseMoveHandler.bind(this);
96-
this._docMouseDownHandler = this._docMouseDownHandler.bind(this);
97-
98-
document.addEventListener("mouseup", this._dragTopHandler, true);
99-
document.addEventListener("mousemove", this._docMouseMoveHandler, true);
100-
document.addEventListener("mousedown", this._docMouseDownHandler, true);
153+
window.widgetWindows._initGlobalListeners();
101154

102155
if (!window.widgetWindows._shortcutsInitialized) {
103156
// Use capture phase (true) to ensure global window control shortcuts are handled
@@ -183,7 +236,7 @@ class WidgetWindow {
183236
titleEl.id = `${this._key}WidgetID`;
184237

185238
this._nonclose.onmousedown = e => {
186-
this._dragging = true;
239+
window.widgetWindows.draggingWindow = this;
187240
if (this._maximized) {
188241
// Perform special repositioning to make the drag feel right when
189242
// restoring a window from maximized.
@@ -286,8 +339,6 @@ class WidgetWindow {
286339
* @returns {void}
287340
*/
288341
_docMouseMoveHandler(e) {
289-
if (!this._dragging) return;
290-
291342
// Throttle using requestAnimationFrame to prevent layout thrashing
292343
if (this._rafTicking) return;
293344
this._rafTicking = true;
@@ -324,44 +375,12 @@ class WidgetWindow {
324375
}
325376
}
326377

327-
/**
328-
* @private
329-
* @param {MouseEvent} e
330-
* @returns {void}
331-
*/
332-
_docMouseDownHandler(e) {
333-
const isToolbarInteraction =
334-
e.target?.closest &&
335-
(e.target.closest("#toolbars") ||
336-
e.target.closest("#aux-toolbar") ||
337-
e.target.closest(".dropdown-content") ||
338-
e.target.closest(".dropdown-trigger"));
339-
340-
if (
341-
e.target === this._frame ||
342-
this._frame.contains(e.target) ||
343-
this._fullscreenEnabled ||
344-
isToolbarInteraction
345-
) {
346-
this._frame.style.opacity = "1";
347-
this._frame.style.zIndex = "10000";
348-
window.widgetWindows.focused = this;
349-
} else {
350-
this._frame.style.opacity = ".7";
351-
this._frame.style.zIndex = "0";
352-
if (window.widgetWindows.focused === this) {
353-
window.widgetWindows.focused = null;
354-
}
355-
}
356-
}
357-
358378
/**
359379
* @private
360380
* @param {MouseEvent} e
361381
* @returns {void}
362382
*/
363383
_dragTopHandler(e) {
364-
this._dragging = false;
365384
if (this._fullscreenEnabled && this._frame.style.top === "64px" && !this._maximized) {
366385
this._maximize();
367386
this.takeFocus();
@@ -477,9 +496,6 @@ class WidgetWindow {
477496
* @returns {void}
478497
*/
479498
close() {
480-
document.removeEventListener("mouseup", this._dragTopHandler, true);
481-
document.removeEventListener("mousemove", this._docMouseMoveHandler, true);
482-
document.removeEventListener("mousedown", this._docMouseDownHandler, true);
483499
this.onclose();
484500
}
485501

@@ -632,15 +648,6 @@ class WidgetWindow {
632648
* @returns {void}
633649
*/
634650
destroy() {
635-
if (this._dragTopHandler) {
636-
document.removeEventListener("mouseup", this._dragTopHandler, true);
637-
}
638-
if (this._docMouseMoveHandler) {
639-
document.removeEventListener("mousemove", this._docMouseMoveHandler, true);
640-
}
641-
if (this._docMouseDownHandler) {
642-
document.removeEventListener("mousedown", this._docMouseDownHandler, true);
643-
}
644651
if (this._widget && this._widgetWheelHandler) {
645652
this._widget.removeEventListener("wheel", this._widgetWheelHandler, false);
646653
this._widget.removeEventListener("DOMMouseScroll", this._widgetWheelHandler, false);

0 commit comments

Comments
 (0)