Skip to content

Commit 755d155

Browse files
committed
dom - use Disposable for dom listeners
1 parent 20f25ba commit 755d155

3 files changed

Lines changed: 116 additions & 121 deletions

File tree

src/vs/base/browser/dom.ts

Lines changed: 110 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import * as keyboardEvent from 'vs/base/browser/keyboardEvent';
99
import {isChrome, isWebKit} from 'vs/base/browser/browser';
1010
import types = require('vs/base/common/types');
1111
import {EventEmitter} from 'vs/base/common/eventEmitter';
12-
import {IDisposable} from 'vs/base/common/lifecycle';
12+
import {Disposable, IDisposable} from 'vs/base/common/lifecycle';
1313
import {onUnexpectedError} from 'vs/base/common/errors';
1414
import browserService = require('vs/base/browser/browserService');
15+
import {TimeoutTimer} from 'vs/base/common/async';
1516

1617
export type IKeyboardEvent = keyboardEvent.IKeyboardEvent;
1718
export type IMouseEvent = mouseEvent.IMouseEvent;
@@ -178,45 +179,58 @@ export function toggleClass(node: HTMLElement, className: string, shouldHaveIt?:
178179
}
179180
}
180181

182+
class DomListener extends Disposable {
181183

184+
private _usedAddEventListener: boolean;
185+
private _wrapHandler: (e: any) => void;
186+
private _node: any;
187+
private _type: string;
188+
private _useCapture: boolean;
182189

183-
function _addListener(node: Element, type: string, handler: (event: any) => void, useCapture?: boolean): () => void;
184-
function _addListener(node: Window, type: string, handler: (event: any) => void, useCapture?: boolean): () => void;
185-
function _addListener(node: Document, type: string, handler: (event: any) => void, useCapture?: boolean): () => void;
186-
function _addListener(node: any, type: string, handler: (event: any) => void, useCapture?: boolean): () => void {
187-
let wrapHandler = function(e): void {
188-
e = e || window.event;
189-
handler(e);
190-
};
190+
constructor(node: Element|Window|Document, type: string, handler: (e: any) => void, useCapture?: boolean) {
191+
super();
191192

192-
if (typeof node.addEventListener === 'function') {
193-
node.addEventListener(type, wrapHandler, useCapture || false);
194-
return function() {
195-
if (!wrapHandler) {
196-
// Already removed
197-
return;
198-
}
199-
node.removeEventListener(type, wrapHandler, useCapture || false);
193+
this._node = node;
194+
this._type = type;
195+
this._useCapture = (useCapture || false);
200196

201-
// Prevent leakers from holding on to the dom node or handler func
202-
wrapHandler = null;
203-
node = null;
204-
handler = null;
197+
this._wrapHandler = (e) => {
198+
e = e || window.event;
199+
handler(e);
205200
};
201+
202+
if (typeof this._node.addEventListener === 'function') {
203+
this._usedAddEventListener = true;
204+
this._node.addEventListener(this._type, this._wrapHandler, this._useCapture);
205+
} else {
206+
this._usedAddEventListener = false;
207+
this._node.attachEvent('on' + this._type, this._wrapHandler);
208+
}
206209
}
207210

208-
node.attachEvent('on' + type, wrapHandler);
209-
return function() { node.detachEvent('on' + type, wrapHandler); };
211+
public dispose(): void {
212+
if (!this._wrapHandler) {
213+
// Already disposed
214+
return;
215+
}
216+
217+
if (this._usedAddEventListener) {
218+
this._node.removeEventListener(this._type, this._wrapHandler, this._useCapture);
219+
} else {
220+
this._node.detachEvent('on' + this._type, this._wrapHandler);
221+
}
222+
223+
// Prevent leakers from holding on to the dom or handler func
224+
this._node = null;
225+
this._wrapHandler = null;
226+
}
210227
}
211228

212229
export function addDisposableListener(node: Element, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
213230
export function addDisposableListener(node: Window, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
214231
export function addDisposableListener(node: Document, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
215232
export function addDisposableListener(node: any, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
216-
let dispose = _addListener(node, type, handler, useCapture);
217-
return {
218-
dispose: dispose
219-
};
233+
return new DomListener(node, type, handler, useCapture);
220234
}
221235

222236
export interface IAddStandardDisposableListenerSignature {
@@ -262,8 +276,8 @@ export let addStandardDisposableListener: IAddStandardDisposableListenerSignatur
262276
};
263277
};
264278

265-
export function addNonBubblingMouseOutListener(node: Element, handler: (event: any) => void): () => void {
266-
return _addListener(node, 'mouseout', (e: MouseEvent) => {
279+
export function addDisposableNonBubblingMouseOutListener(node: Element, handler: (event: MouseEvent) => void): IDisposable {
280+
return addDisposableListener(node, 'mouseout', (e: MouseEvent) => {
267281
// Mouse out bubbles, so this is an attempt to ignore faux mouse outs coming from children elements
268282
let toElement = <Node>(e.relatedTarget || e.toElement);
269283
while (toElement && toElement !== node) {
@@ -276,12 +290,6 @@ export function addNonBubblingMouseOutListener(node: Element, handler: (event: a
276290
handler(e);
277291
});
278292
}
279-
export function addDisposableNonBubblingMouseOutListener(node: Element, handler: (event: MouseEvent) => void): IDisposable {
280-
let dispose = addNonBubblingMouseOutListener(node, handler);
281-
return {
282-
dispose: dispose
283-
};
284-
}
285293

286294
const _animationFrame = (function() {
287295
let emulatedRequestAnimationFrame = (callback: (time: number) => void): number => {
@@ -450,49 +458,38 @@ const DEFAULT_EVENT_MERGER: IEventMerger<Event> = function(lastEvent: Event, cur
450458
return currentEvent;
451459
};
452460

453-
function timeoutThrottledListener<R>(node: any, type: string, handler: (event: R) => void, eventMerger: IEventMerger<R> = <any>DEFAULT_EVENT_MERGER, minimumTimeMs: number = MINIMUM_TIME_MS): () => void {
454-
let lastEvent: R = null, lastHandlerTime = 0, timeout = -1;
461+
class TimeoutThrottledDomListener<R> extends Disposable {
455462

456-
function invokeHandler(): void {
457-
timeout = -1;
458-
lastHandlerTime = (new Date()).getTime();
459-
handler(lastEvent);
460-
lastEvent = null;
461-
};
463+
constructor(node: any, type: string, handler: (event: R) => void, eventMerger: IEventMerger<R> = <any>DEFAULT_EVENT_MERGER, minimumTimeMs: number = MINIMUM_TIME_MS) {
464+
super();
462465

463-
let unbinder = _addListener(node, type, function(e) {
464-
lastEvent = eventMerger(lastEvent, e);
465-
let elapsedTime = (new Date()).getTime() - lastHandlerTime;
466+
let lastEvent = null;
467+
let lastHandlerTime = 0;
468+
let timeout = this._register(new TimeoutTimer());
466469

467-
if (elapsedTime >= minimumTimeMs) {
468-
if (timeout !== -1) {
469-
window.clearTimeout(timeout);
470-
}
471-
invokeHandler();
472-
} else {
473-
if (timeout === -1) {
474-
timeout = window.setTimeout(invokeHandler, minimumTimeMs - elapsedTime);
475-
}
476-
}
477-
});
470+
let invokeHandler = () => {
471+
lastHandlerTime = (new Date()).getTime();
472+
handler(lastEvent);
473+
lastEvent = null;
474+
};
478475

479-
return function() {
480-
if (timeout !== -1) {
481-
window.clearTimeout(timeout);
482-
}
483-
unbinder();
484-
};
485-
}
476+
this._register(addDisposableListener(node, type, (e) => {
486477

487-
export function _addThrottledListener<R>(node: any, type: string, handler: (event: R) => void, eventMerger?: IEventMerger<R>, minimumTimeMs?: number): () => void {
488-
return timeoutThrottledListener(node, type, handler, eventMerger, minimumTimeMs);
478+
lastEvent = eventMerger(lastEvent, e);
479+
let elapsedTime = (new Date()).getTime() - lastHandlerTime;
480+
481+
if (elapsedTime >= minimumTimeMs) {
482+
timeout.cancel();
483+
invokeHandler();
484+
} else {
485+
timeout.setIfNotSet(invokeHandler, minimumTimeMs - elapsedTime);
486+
}
487+
}));
488+
}
489489
}
490490

491491
export function addDisposableThrottledListener<R>(node: any, type: string, handler: (event: R) => void, eventMerger?: IEventMerger<R>, minimumTimeMs?: number): IDisposable {
492-
let dispose = _addThrottledListener(node, type, handler, eventMerger, minimumTimeMs);
493-
return {
494-
dispose: dispose
495-
};
492+
return new TimeoutThrottledDomListener<R>(node, type, handler, eventMerger, minimumTimeMs);
496493
}
497494

498495
export function getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
@@ -857,8 +854,8 @@ export const EventHelper = {
857854
};
858855

859856
export interface IFocusTracker {
860-
addBlurListener(fn): () => void;
861-
addFocusListener(fn): () => void;
857+
addBlurListener(fn:()=>void): IDisposable;
858+
addFocusListener(fn:()=>void): IDisposable;
862859
dispose(): void;
863860
}
864861

@@ -897,56 +894,54 @@ export function restoreParentsScrollTop(node: Element, state: number[]): void {
897894
}
898895
}
899896

900-
export function trackFocus(element: HTMLElement): IFocusTracker {
897+
class FocusTracker extends Disposable implements IFocusTracker {
901898

902-
let hasFocus: boolean = false, loosingFocus = false;
903-
let eventEmitter = new EventEmitter(), unbind = [], result: IFocusTracker = null;
899+
private _eventEmitter: EventEmitter;
904900

905-
// fill result
906-
result = {
907-
addFocusListener: function(fn) {
908-
let h = eventEmitter.addListener('focus', fn);
909-
unbind.push(h);
910-
return h;
911-
},
912-
addBlurListener: function(fn) {
913-
let h = eventEmitter.addListener('blur', fn);
914-
unbind.push(h);
915-
return h;
916-
},
917-
dispose: function() {
918-
while (unbind.length > 0) {
919-
unbind.pop()();
901+
constructor(element: HTMLElement) {
902+
super();
903+
904+
let hasFocus = false;
905+
let loosingFocus = false;
906+
907+
this._eventEmitter = this._register(new EventEmitter());
908+
909+
let onFocus = (event) => {
910+
loosingFocus = false;
911+
if (!hasFocus) {
912+
hasFocus = true;
913+
this._eventEmitter.emit('focus', {});
920914
}
921-
}
922-
};
915+
};
923916

924-
let onFocus = function(event) {
925-
loosingFocus = false;
926-
if (!hasFocus) {
927-
hasFocus = true;
928-
eventEmitter.emit('focus', {});
929-
}
930-
};
917+
let onBlur = (event) => {
918+
if (hasFocus) {
919+
loosingFocus = true;
920+
window.setTimeout(() => {
921+
if (loosingFocus) {
922+
loosingFocus = false;
923+
hasFocus = false;
924+
this._eventEmitter.emit('blur', {});
925+
}
926+
}, 0);
927+
}
928+
};
931929

932-
let onBlur = function(event) {
933-
if (hasFocus) {
934-
loosingFocus = true;
935-
window.setTimeout(function() {
936-
if (loosingFocus) {
937-
loosingFocus = false;
938-
hasFocus = false;
939-
eventEmitter.emit('blur', {});
940-
}
941-
}, 0);
942-
}
943-
};
930+
this._register(addDisposableListener(element, EventType.FOCUS, onFocus, true));
931+
this._register(addDisposableListener(element, EventType.BLUR, onBlur, true));
932+
}
944933

945-
// bind
946-
unbind.push(_addListener(element, EventType.FOCUS, onFocus, true));
947-
unbind.push(_addListener(element, EventType.BLUR, onBlur, true));
934+
public addFocusListener(fn:()=>void): IDisposable {
935+
return this._eventEmitter.addListener2('focus', fn);
936+
}
948937

949-
return result;
938+
public addBlurListener(fn:()=>void): IDisposable {
939+
return this._eventEmitter.addListener2('blur', fn);
940+
}
941+
}
942+
943+
export function trackFocus(element: HTMLElement): IFocusTracker {
944+
return new FocusTracker(element);
950945
}
951946

952947
export function removeScriptTags(html: string): string {

src/vs/base/browser/ui/actionbar/actionbar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,9 @@ export class ActionBar extends EventEmitter implements IActionRunner {
480480
});
481481

482482
this.focusTracker = DOM.trackFocus(this.domNode);
483-
this.focusTracker.addBlurListener((e: Event) => {
483+
this.focusTracker.addBlurListener(() => {
484484
if (document.activeElement === this.domNode || !DOM.isAncestor(document.activeElement, this.domNode)) {
485-
this.emit(DOM.EventType.BLUR, e);
485+
this.emit(DOM.EventType.BLUR, {});
486486
this.focusedItem = undefined;
487487
}
488488
});

src/vs/base/parts/tree/browser/treeView.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,8 @@ export class TreeView extends HeightMap implements IScrollable {
511511
this.rowsContainer.className = 'monaco-tree-rows';
512512

513513
var focusTracker = DOM.trackFocus(this.domNode);
514-
focusTracker.addFocusListener((e: FocusEvent) => this.onFocus(e));
515-
focusTracker.addBlurListener((e: FocusEvent) => this.onBlur(e));
514+
focusTracker.addFocusListener(() => this.onFocus());
515+
focusTracker.addBlurListener(() => this.onBlur());
516516
this.viewListeners.push(focusTracker);
517517

518518
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'keydown', (e) => this.onKeyDown(e)));
@@ -1542,13 +1542,13 @@ export class TreeView extends HeightMap implements IScrollable {
15421542
delete this.dragAndDropMouseY;
15431543
}
15441544

1545-
private onFocus(e: FocusEvent): void {
1545+
private onFocus(): void {
15461546
if (!this.context.options.alwaysFocused) {
15471547
DOM.addClass(this.domNode, 'focused');
15481548
}
15491549
}
15501550

1551-
private onBlur(e: FocusEvent): void {
1551+
private onBlur(): void {
15521552
if (!this.context.options.alwaysFocused) {
15531553
DOM.removeClass(this.domNode, 'focused');
15541554
}

0 commit comments

Comments
 (0)