From c435b03bdc253c52c4fa3cf3bbb67a7ffb59d791 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 1 May 2015 21:58:26 -0700 Subject: [PATCH] use onScreenEvent to prevent memory leaks. --- README.md | 15 ++++++++-- lib/widget.js | 77 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index d3178bf..20b7977 100644 --- a/README.md +++ b/README.md @@ -501,8 +501,19 @@ The base element. - __onceKey(name, listener)__ - bind a keypress listener for a specific key once. - __unkey(name, listener)__ - remove a keypress listener for a specific key. -- __onScreenEvent(type, listener)__ - same as`el.on('screen', ...)` except this - will automatically cleanup listeners after the element is detached. +- __onScreenEvent(type, handler)__ - same as`el.on('screen', ...)` except this + will automatically keep track of which listeners are bound to the screen + object. for use with `removeScreenEvent()`, `free()`, and `destroy()`. +- __removeScreenEvent(type, handler)__ - same as`el.removeListener('screen', + ...)` except this will automatically keep track of which listeners are bound + to the screen object. for use with `onScreenEvent()`, `free()`, and + `destroy()`. +- __free()__ - free up the element. automatically unbind all events that may + have been bound to the screen object. this prevents memory leaks. for use + with `onScreenEvent()`, `removeScreenEvent()`, and `destroy()`. +- __destroy()__ - same as the `detach()` method, except this will automatically + call `free()` and unbind any screen events to prevent memory leaks. for use + with `onScreenEvent()`, `removeScreenEvent()`, and `free()`. - __setIndex(z)__ - set the z-index of the element (changes rendering order). - __setFront()__ - put the element in front of its siblings. - __setBack()__ - put the element in back of its siblings. diff --git a/lib/widget.js b/lib/widget.js index 024e965..d8204e6 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -2395,7 +2395,7 @@ function Element(options) { this.setHover(options.hoverText); } - // TODO: Possibly move this to Node for screen.on('mouse', ...). + // TODO: Possibly move this to Node for onScreenEvent('mouse', ...). this.on('newListener', function fn(type) { // type = type.split(' ').slice(1).join(' '); if (type === 'mouse' @@ -2502,17 +2502,42 @@ Element.prototype.sattr = function(style, fg, bg) { | colors.convert(bg); }; -Element.prototype.onScreenEvent = function(type, listener) { - var self = this; - if (this.parent) { - this.screen.on(type, listener); +Element.prototype.onScreenEvent = function(type, handler) { + this.screen.on(type, handler); + this._elisteners = this._elisteners || {}; + this._elisteners[this.uid] = this._elisteners[this.uid] || []; + this._elisteners[this.uid].push({ type: type, handler: handler }); +}; + +Element.prototype.removeScreenEvent = function(type, handler) { + this.screen.removeListener(type, handler); + this._elisteners = this._elisteners || {}; + var listeners = this.screen._elisteners[this.uid] || []; + for (var i = 0; i < listeners.length; i++) { + var listener = listeners[i]; + if (listener.type === type && listener.handler === handler) { + listeners.splice(i, 1); + if (this.screen._elisteners[this.uid].length === 0) { + delete this.screen._elisteners[this.uid]; + } + return; + } } - this.on('attach', function() { - self.screen.on(type, listener); - }); - this.on('detach', function() { - self.screen.removeListener(type, listener); - }); +}; + +Element.prototype.free = function() { + this._elisteners = this._elisteners || {}; + var listeners = this.screen._elisteners[this.uid] || []; + for (var i = 0; i < listeners.length; i++) { + var listener = listeners[i]; + this.screen.removeListener(listener.type, listener.handler); + } +}; + +Element.prototype.destroy = function() { + this.detach(); + this.free(); + this.screen.render(); }; Element.prototype.hide = function() { @@ -3006,7 +3031,7 @@ Element.prototype.enableDrag = function() { self.setFront(); }); - this.screen.on('mouse', this._dragM = function(data) { + this.onScreenEvent('mouse', this._dragM = function(data) { if (self.screen._dragging !== self) return; if (data.action !== 'mousedown') { @@ -3050,7 +3075,7 @@ Element.prototype.disableDrag = function() { delete this.screen._dragging; delete this._drag; this.removeListener('mousedown', this._dragMD); - this.screen.removeListener('mouse', this._dragM); + this.removeScreenEvent('mouse', this._dragM); return this._draggable = false; }; @@ -4981,7 +5006,7 @@ function ScrollableBox(options) { self.screen.render(); var smd, smu; self._scrollingBar = true; - self.screen.on('mousedown', smd = function(data) { + self.onScreenEvent('mousedown', smd = function(data) { var y = data.y - self.atop; var perc = y / self.height; self.setScrollPerc(perc * 100 | 0); @@ -4990,10 +5015,10 @@ function ScrollableBox(options) { // If mouseup occurs out of the window, no mouseup event fires, and // scrollbar will drag again on mousedown until another mouseup // occurs. - self.screen.on('mouseup', smu = function(data) { + self.onScreenEvent('mouseup', smu = function(data) { self._scrollingBar = false; - self.screen.removeListener('mousedown', smd); - self.screen.removeListener('mouseup', smu); + self.removeScreenEvent('mousedown', smd); + self.removeScreenEvent('mouseup', smu); }); } }); @@ -7111,7 +7136,7 @@ Question.prototype.ask = function(text, callback) { this.show(); this.setContent(' ' + text); - this.screen.on('keypress', press = function(ch, key) { + this.onScreenEvent('keypress', press = function(ch, key) { if (key.name === 'mouse') return; if (key.name !== 'enter' && key.name !== 'escape' @@ -7137,7 +7162,7 @@ Question.prototype.ask = function(text, callback) { function done(err, data) { self.hide(); self.screen.restoreFocus(); - self.screen.removeListener('keypress', press); + self.removeScreenEvent('keypress', press); self._.okay.removeListener('press', okay); self._.cancel.removeListener('press', cancel); return callback(err, data); @@ -7210,7 +7235,7 @@ Message.prototype.display = function(text, time, callback) { }; setTimeout(function() { - self.screen.on('keypress', function fn(ch, key) { + self.onScreenEvent('keypress', function fn(ch, key) { if (key.name === 'mouse') return; if (self.scrollable) { if ((key.name === 'up' || (self.options.vi && key.name === 'k')) @@ -7227,13 +7252,13 @@ Message.prototype.display = function(text, time, callback) { if (self.options.ignoreKeys && ~self.options.ignoreKeys.indexOf(key.name)) { return; } - self.screen.removeListener('keypress', fn); + self.removeScreenEvent('keypress', fn); end(); }); if (!self.options.mouse) return; - self.screen.on('mouse', function fn(data) { + self.onScreenEvent('mouse', function fn(data) { if (data.action === 'mousemove') return; - self.screen.removeListener('mouse', fn); + self.removeScreenEvent('mouse', fn); end(); }); }, 10); @@ -7399,7 +7424,7 @@ function Listbar(options) { } if (options.autoCommandKeys) { - this.screen.on('keypress', function(ch, key) { + this.onScreenEvent('keypress', function(ch, key) { if (/^[0-9]$/.test(ch)) { var i = +ch - 1; if (!~i) i = 9; @@ -8406,7 +8431,7 @@ Terminal.prototype.bootstrap = function() { } }); - this.screen.on('mouse', function(data) { + this.onScreenEvent('mouse', function(data) { if (self.screen.focused !== self) return; if (data.x < self.aleft + self.ileft) return; @@ -8510,7 +8535,7 @@ Terminal.prototype.bootstrap = function() { self.emit('exit', code || null); }); - this.screen.on('keypress', function() { + this.onScreenEvent('keypress', function() { self.screen.render(); });