Skip to content

Commit 8aab310

Browse files
authored
Merge pull request #6019 from anthonykim1/anthonykim1/fix-terminal-window-mouse-listeners
Use current document for mouse listeners
2 parents 53a98a7 + 9f232d9 commit 8aab310

2 files changed

Lines changed: 111 additions & 27 deletions

File tree

src/browser/services/MouseService.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,96 @@ describe('MouseService mouseEventsRequireAlt', () => {
351351
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
352352
});
353353
});
354+
355+
describe('MouseService multi-window', () => {
356+
interface IRecordingDocument {
357+
doc: Document;
358+
added: string[];
359+
removed: string[];
360+
listeners: { [type: string]: EventListener[] };
361+
}
362+
363+
function createRecordingDocument(): IRecordingDocument {
364+
const added: string[] = [];
365+
const removed: string[] = [];
366+
const listeners: { [type: string]: EventListener[] } = {};
367+
const doc = {
368+
addEventListener: (type: string, listener: EventListener) => {
369+
added.push(type);
370+
(listeners[type] ??= []).push(listener);
371+
},
372+
removeEventListener: (type: string) => removed.push(type)
373+
} as any;
374+
return { doc, added, removed, listeners };
375+
}
376+
377+
it('should manage protocol mouseup/mousemove listeners on the element\'s current document, not the document the terminal was opened with', () => {
378+
const mouseStateService = new MouseStateService();
379+
const optionsService = new OptionsService({});
380+
const mouseService = new MouseService(
381+
new MockRenderService(),
382+
{
383+
getMouseReportCoords: () => ({ col: 0, row: 0, x: 0, y: 0 })
384+
} as any,
385+
mouseStateService,
386+
{
387+
triggerDataEvent: () => {},
388+
triggerBinaryEvent: () => {},
389+
decPrivateModes: { applicationCursorKeys: false }
390+
} as any,
391+
bufferService,
392+
optionsService,
393+
new TestSelectionService(),
394+
logService,
395+
new MockCoreBrowserService()
396+
);
397+
398+
// Simulate a terminal that was opened with one document but whose element now lives in
399+
// another document (eg. it was moved to a child window)
400+
const openDocument = createRecordingDocument();
401+
const childDocument = createRecordingDocument();
402+
const elementListeners: { [type: string]: EventListener[] } = {};
403+
const element = {
404+
classList: {
405+
add: () => {},
406+
remove: () => {},
407+
contains: () => false
408+
},
409+
addEventListener: (type: string, listener: EventListener) => {
410+
(elementListeners[type] ??= []).push(listener);
411+
},
412+
removeEventListener: () => {},
413+
ownerDocument: childDocument.doc
414+
} as any;
415+
416+
mouseService.bindMouse({
417+
element,
418+
screenElement: createTestMouseTargetElement(),
419+
document: openDocument.doc
420+
}, disposable => disposable, () => {});
421+
mouseStateService.activeProtocol = 'ANY';
422+
423+
elementListeners['mousedown'][0]({
424+
type: 'mousedown',
425+
button: 0,
426+
buttons: 1,
427+
altKey: false,
428+
ctrlKey: false,
429+
shiftKey: false,
430+
preventDefault: () => {}
431+
} as any);
432+
assert.deepEqual(childDocument.added, ['mouseup', 'mousemove']);
433+
assert.deepEqual(openDocument.added, []);
434+
435+
childDocument.listeners['mouseup'][0]({
436+
type: 'mouseup',
437+
button: 0,
438+
buttons: 0,
439+
altKey: false,
440+
ctrlKey: false,
441+
shiftKey: false
442+
} as any);
443+
assert.deepEqual(childDocument.removed, ['mouseup', 'mousemove']);
444+
assert.deepEqual(openDocument.removed, []);
445+
});
446+
});

src/browser/services/MouseService.ts

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { addDisposableListener } from '../Dom';
77
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from '../../common/services/Services';
88
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from '../../common/Types';
99
import { C0 } from '../../common/data/EscapeSequences';
10-
import { DisposableStore, MutableDisposable, toDisposable } from '../../common/Lifecycle';
10+
import { DisposableStore, MutableDisposable } from '../../common/Lifecycle';
1111
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
1212
import { Gesture, EventType as GestureEventType, IGestureEvent } from '../scrollable/touch';
1313

@@ -21,6 +21,8 @@ interface IMouseBindContext {
2121
readonly target: IMouseServiceTarget;
2222
readonly focus: () => void;
2323
readonly requestedEvents: RequestedMouseEvents;
24+
readonly mouseupListener: MutableDisposable<IDisposable>;
25+
readonly mousedragListener: MutableDisposable<IDisposable>;
2426
}
2527

2628
export class MouseService implements IMouseService {
@@ -61,7 +63,11 @@ export class MouseService implements IMouseService {
6163
mousedrag: null,
6264
mousemove: null
6365
};
64-
const ctx: IMouseBindContext = { target, focus, requestedEvents };
66+
const mouseupListener = new MutableDisposable<IDisposable>();
67+
const mousedragListener = new MutableDisposable<IDisposable>();
68+
register(mouseupListener);
69+
register(mousedragListener);
70+
const ctx: IMouseBindContext = { target, focus, requestedEvents, mouseupListener, mousedragListener };
6571
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
6672
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
6773
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
@@ -85,16 +91,6 @@ export class MouseService implements IMouseService {
8591
// force initial onProtocolChange so we dont miss early mouse requests
8692
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
8793

88-
// Ensure document-level listeners are removed on dispose
89-
register(toDisposable(() => {
90-
if (requestedEvents.mouseup) {
91-
document.removeEventListener('mouseup', requestedEvents.mouseup);
92-
}
93-
if (requestedEvents.mousedrag) {
94-
document.removeEventListener('mousemove', requestedEvents.mousedrag);
95-
}
96-
}));
97-
9894
/**
9995
* "Always on" event listeners.
10096
*/
@@ -199,12 +195,8 @@ export class MouseService implements IMouseService {
199195
this._sendEvent(ctx, ev);
200196
if (!ev.buttons) {
201197
// if no other button is held remove global handlers
202-
if (ctx.requestedEvents.mouseup) {
203-
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
204-
}
205-
if (ctx.requestedEvents.mousedrag) {
206-
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
207-
}
198+
ctx.mouseupListener.clear();
199+
ctx.mousedragListener.clear();
208200
}
209201
}
210202

@@ -246,11 +238,14 @@ export class MouseService implements IMouseService {
246238
// of the terminal element.
247239
// Note: Other emulators also do this for 'mousedown' while a button
248240
// is held, we currently limit 'mousedown' to the terminal only.
241+
// Use the element's current document in case it moved to another window after open.
242+
const { element, document: targetDocument } = ctx.target;
243+
const listenerDocument = element.ownerDocument ?? targetDocument;
249244
if (ctx.requestedEvents.mouseup) {
250-
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
245+
ctx.mouseupListener.value = addDisposableListener(listenerDocument, 'mouseup', ctx.requestedEvents.mouseup);
251246
}
252247
if (ctx.requestedEvents.mousedrag) {
253-
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
248+
ctx.mousedragListener.value = addDisposableListener(listenerDocument, 'mousemove', ctx.requestedEvents.mousedrag);
254249
}
255250
}
256251

@@ -398,7 +393,7 @@ export class MouseService implements IMouseService {
398393
}
399394

400395
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
401-
const { element, document } = ctx.target;
396+
const { element } = ctx.target;
402397
const { requestedEvents } = ctx;
403398
// apply global changes on events
404399
if (events) {
@@ -433,18 +428,14 @@ export class MouseService implements IMouseService {
433428
}
434429

435430
if (!(events & CoreMouseEventType.UP)) {
436-
if (requestedEvents.mouseup) {
437-
document.removeEventListener('mouseup', requestedEvents.mouseup);
438-
}
431+
ctx.mouseupListener.clear();
439432
requestedEvents.mouseup = null;
440433
} else {
441434
requestedEvents.mouseup ??= eventListeners.mouseup;
442435
}
443436

444437
if (!(events & CoreMouseEventType.DRAG)) {
445-
if (requestedEvents.mousedrag) {
446-
document.removeEventListener('mousemove', requestedEvents.mousedrag);
447-
}
438+
ctx.mousedragListener.clear();
448439
requestedEvents.mousedrag = null;
449440
} else {
450441
requestedEvents.mousedrag ??= eventListeners.mousedrag;

0 commit comments

Comments
 (0)