Merge pull request #1834 from nasa/summary-widgets-ml

Summary widgets Memory Leak Fix
This commit is contained in:
Deep Tailor 2017-12-07 14:19:16 -08:00 committed by GitHub
commit 284dec4903
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 193 additions and 44 deletions

View File

@ -48,6 +48,7 @@ define([
this.unlisteners.forEach(function (unlisten) { this.unlisteners.forEach(function (unlisten) {
unlisten(); unlisten();
}); });
this.unlisteners = [];
}; };
/** /**

View File

@ -3,6 +3,7 @@ define([
'./input/ObjectSelect', './input/ObjectSelect',
'./input/KeySelect', './input/KeySelect',
'./input/OperationSelect', './input/OperationSelect',
'./eventHelpers',
'EventEmitter', 'EventEmitter',
'zepto' 'zepto'
], function ( ], function (
@ -10,10 +11,10 @@ define([
ObjectSelect, ObjectSelect,
KeySelect, KeySelect,
OperationSelect, OperationSelect,
eventHelpers,
EventEmitter, EventEmitter,
$ $
) { ) {
/** /**
* Represents an individual condition for a summary widget rule. Manages the * Represents an individual condition for a summary widget rule. Manages the
* associated inputs and view. * associated inputs and view.
@ -25,6 +26,7 @@ define([
* selects with configuration data * selects with configuration data
*/ */
function Condition(conditionConfig, index, conditionManager) { function Condition(conditionConfig, index, conditionManager) {
eventHelpers.extend(this);
this.config = conditionConfig; this.config = conditionConfig;
this.index = index; this.index = index;
this.conditionManager = conditionManager; this.conditionManager = conditionManager;
@ -71,15 +73,17 @@ define([
value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber), value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber),
inputIndex = self.valueInputs.indexOf(elem); inputIndex = self.valueInputs.indexOf(elem);
self.eventEmitter.emit('change', { if (elem.tagName.toUpperCase() === 'INPUT') {
value: value, self.eventEmitter.emit('change', {
property: 'values[' + inputIndex + ']', value: value,
index: self.index property: 'values[' + inputIndex + ']',
}); index: self.index
});
}
} }
this.deleteButton.on('click', this.remove); this.listenTo(this.deleteButton, 'click', this.remove, this);
this.duplicateButton.on('click', this.duplicate); this.listenTo(this.duplicateButton, 'click', this.duplicate, this);
this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ this.selects.object = new ObjectSelect(this.config, this.conditionManager, [
['any', 'any telemetry'], ['any', 'any telemetry'],
@ -105,7 +109,7 @@ define([
$('.t-configuration', self.domElement).append(select.getDOM()); $('.t-configuration', self.domElement).append(select.getDOM());
}); });
$(this.domElement).on('input', 'input', onValueInput); this.listenTo($(this.domElement), 'input', onValueInput);
} }
Condition.prototype.getDOM = function (container) { Condition.prototype.getDOM = function (container) {
@ -139,6 +143,14 @@ define([
*/ */
Condition.prototype.remove = function () { Condition.prototype.remove = function () {
this.eventEmitter.emit('remove', this.index); this.eventEmitter.emit('remove', this.index);
this.destroy();
};
Condition.prototype.destroy = function () {
this.stopListening();
Object.values(this.selects).forEach(function (select) {
select.destroy();
});
}; };
/** /**

View File

@ -3,6 +3,7 @@ define([
'./Condition', './Condition',
'./input/ColorPalette', './input/ColorPalette',
'./input/IconPalette', './input/IconPalette',
'./eventHelpers',
'EventEmitter', 'EventEmitter',
'lodash', 'lodash',
'zepto' 'zepto'
@ -11,11 +12,11 @@ define([
Condition, Condition,
ColorPalette, ColorPalette,
IconPalette, IconPalette,
eventHelpers,
EventEmitter, EventEmitter,
_, _,
$ $
) { ) {
/** /**
* An object representing a summary widget rule. Maintains a set of text * An object representing a summary widget rule. Maintains a set of text
* and css properties for output, and a set of conditions for configuring * and css properties for output, and a set of conditions for configuring
@ -29,6 +30,7 @@ define([
* @param {element} container The DOM element which cotains this summary widget * @param {element} container The DOM element which cotains this summary widget
*/ */
function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) {
eventHelpers.extend(this);
var self = this; var self = this;
this.config = ruleConfig; this.config = ruleConfig;
@ -196,24 +198,24 @@ define([
Object.keys(this.textInputs).forEach(function (inputKey) { Object.keys(this.textInputs).forEach(function (inputKey) {
self.textInputs[inputKey].prop('value', self.config[inputKey] || ''); self.textInputs[inputKey].prop('value', self.config[inputKey] || '');
self.textInputs[inputKey].on('input', function () { self.listenTo(self.textInputs[inputKey], 'input', function () {
onTextInput(this, inputKey); onTextInput(this, inputKey);
}); });
}); });
this.deleteButton.on('click', this.remove); this.listenTo(this.deleteButton, 'click', this.remove);
this.duplicateButton.on('click', this.duplicate); this.listenTo(this.duplicateButton, 'click', this.duplicate);
this.addConditionButton.on('click', function () { this.listenTo(this.addConditionButton, 'click', function () {
self.initCondition(); self.initCondition();
}); });
this.toggleConfigButton.on('click', toggleConfig); this.listenTo(this.toggleConfigButton, 'click', toggleConfig);
this.trigger.on('change', onTriggerInput); this.listenTo(this.trigger, 'change', onTriggerInput);
this.title.html(self.config.name); this.title.html(self.config.name);
this.description.html(self.config.description); this.description.html(self.config.description);
this.trigger.prop('value', self.config.trigger); this.trigger.prop('value', self.config.trigger);
this.grippy.on('mousedown', onDragStart); this.listenTo(this.grippy, 'mousedown', onDragStart);
this.widgetDnD.on('drop', function () { this.widgetDnD.on('drop', function () {
this.domElement.show(); this.domElement.show();
$('.t-drag-indicator').hide(); $('.t-drag-indicator').hide();
@ -258,6 +260,10 @@ define([
palette.destroy(); palette.destroy();
}); });
this.iconInput.destroy(); this.iconInput.destroy();
this.stopListening();
this.conditions.forEach(function (condition) {
condition.destroy();
});
}; };
/** /**

View File

@ -4,6 +4,7 @@ define([
'./ConditionManager', './ConditionManager',
'./TestDataManager', './TestDataManager',
'./WidgetDnD', './WidgetDnD',
'./eventHelpers',
'lodash', 'lodash',
'zepto' 'zepto'
], function ( ], function (
@ -12,6 +13,7 @@ define([
ConditionManager, ConditionManager,
TestDataManager, TestDataManager,
WidgetDnD, WidgetDnD,
eventHelpers,
_, _,
$ $
) { ) {
@ -32,6 +34,8 @@ define([
* @param {MCT} openmct An MCT instance * @param {MCT} openmct An MCT instance
*/ */
function SummaryWidget(domainObject, openmct) { function SummaryWidget(domainObject, openmct) {
eventHelpers.extend(this);
this.domainObject = domainObject; this.domainObject = domainObject;
this.openmct = openmct; this.openmct = openmct;
@ -86,7 +90,7 @@ define([
self.outerWrapper.toggleClass('expanded-widget-test-data'); self.outerWrapper.toggleClass('expanded-widget-test-data');
self.toggleTestDataControl.toggleClass('expanded'); self.toggleTestDataControl.toggleClass('expanded');
} }
this.toggleTestDataControl.on('click', toggleTestData); this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);
/** /**
* Toggles the configuration area for rules in the view * Toggles the configuration area for rules in the view
@ -96,7 +100,7 @@ define([
self.outerWrapper.toggleClass('expanded-widget-rules'); self.outerWrapper.toggleClass('expanded-widget-rules');
self.toggleRulesControl.toggleClass('expanded'); self.toggleRulesControl.toggleClass('expanded');
} }
this.toggleRulesControl.on('click', toggleRules); this.listenTo(this.toggleRulesControl, 'click', toggleRules);
openmct.$injector.get('objectService') openmct.$injector.get('objectService')
.getObjects([id]) .getObjects([id])
@ -160,13 +164,15 @@ define([
this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById); this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById);
this.initRule('default', 'Default'); this.initRule('default', 'Default');
this.domainObject.configuration.ruleOrder.forEach(function (ruleId) { this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {
self.initRule(ruleId); if (ruleId !== 'default') {
self.initRule(ruleId);
}
}); });
this.refreshRules(); this.refreshRules();
this.updateWidget(); this.updateWidget();
this.updateView(); this.updateView();
this.addRuleButton.on('click', this.addRule); this.listenTo(this.addRuleButton, 'click', this.addRule);
this.conditionManager.on('receiveTelemetry', this.executeRules, this); this.conditionManager.on('receiveTelemetry', this.executeRules, this);
this.widgetDnD.on('drop', this.reorder, this); this.widgetDnD.on('drop', this.reorder, this);
}; };
@ -178,11 +184,14 @@ define([
SummaryWidget.prototype.destroy = function (container) { SummaryWidget.prototype.destroy = function (container) {
this.editListenerUnsubscribe(); this.editListenerUnsubscribe();
this.conditionManager.destroy(); this.conditionManager.destroy();
this.testDataManager.destroy();
this.widgetDnD.destroy(); this.widgetDnD.destroy();
this.watchForChangesUnsubscribe(); this.watchForChangesUnsubscribe();
Object.values(this.rulesById).forEach(function (rule) { Object.values(this.rulesById).forEach(function (rule) {
rule.destroy(); rule.destroy();
}); });
this.stopListening();
}; };
/** /**

View File

@ -2,12 +2,14 @@ define([
'text!../res/testDataItemTemplate.html', 'text!../res/testDataItemTemplate.html',
'./input/ObjectSelect', './input/ObjectSelect',
'./input/KeySelect', './input/KeySelect',
'./eventHelpers',
'EventEmitter', 'EventEmitter',
'zepto' 'zepto'
], function ( ], function (
itemTemplate, itemTemplate,
ObjectSelect, ObjectSelect,
KeySelect, KeySelect,
eventHelpers,
EventEmitter, EventEmitter,
$ $
) { ) {
@ -24,6 +26,7 @@ define([
* @constructor * @constructor
*/ */
function TestDataItem(itemConfig, index, conditionManager) { function TestDataItem(itemConfig, index, conditionManager) {
eventHelpers.extend(this);
this.config = itemConfig; this.config = itemConfig;
this.index = index; this.index = index;
this.conditionManager = conditionManager; this.conditionManager = conditionManager;
@ -70,16 +73,17 @@ define([
function onValueInput(event) { function onValueInput(event) {
var elem = event.target, var elem = event.target,
value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber); value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber);
if (elem.tagName.toUpperCase() === 'INPUT') {
self.eventEmitter.emit('change', { self.eventEmitter.emit('change', {
value: value, value: value,
property: 'value', property: 'value',
index: self.index index: self.index
}); });
}
} }
this.deleteButton.on('click', this.remove); this.listenTo(this.deleteButton, 'click', this.remove);
this.duplicateButton.on('click', this.duplicate); this.listenTo(this.duplicateButton, 'click', this.duplicate);
this.selects.object = new ObjectSelect(this.config, this.conditionManager); this.selects.object = new ObjectSelect(this.config, this.conditionManager);
this.selects.key = new KeySelect( this.selects.key = new KeySelect(
@ -97,8 +101,7 @@ define([
Object.values(this.selects).forEach(function (select) { Object.values(this.selects).forEach(function (select) {
$('.t-configuration', self.domElement).append(select.getDOM()); $('.t-configuration', self.domElement).append(select.getDOM());
}); });
this.listenTo(this.domElement, 'input', onValueInput);
$(this.domElement).on('input', 'input', onValueInput);
} }
/** /**
@ -137,6 +140,11 @@ define([
TestDataItem.prototype.remove = function () { TestDataItem.prototype.remove = function () {
var self = this; var self = this;
this.eventEmitter.emit('remove', self.index); this.eventEmitter.emit('remove', self.index);
this.stopListening();
Object.values(this.selects).forEach(function (select) {
select.destroy();
});
}; };
/** /**

View File

@ -1,9 +1,11 @@
define([ define([
'./eventHelpers',
'text!../res/testDataTemplate.html', 'text!../res/testDataTemplate.html',
'./TestDataItem', './TestDataItem',
'zepto', 'zepto',
'lodash' 'lodash'
], function ( ], function (
eventHelpers,
testDataTemplate, testDataTemplate,
TestDataItem, TestDataItem,
$, $,
@ -18,6 +20,7 @@ define([
* @param {MCT} openmct and MCT instance * @param {MCT} openmct and MCT instance
*/ */
function TestDataManager(domainObject, conditionManager, openmct) { function TestDataManager(domainObject, conditionManager, openmct) {
eventHelpers.extend(this);
var self = this; var self = this;
this.domainObject = domainObject; this.domainObject = domainObject;
@ -45,10 +48,10 @@ define([
self.updateTestCache(); self.updateTestCache();
} }
this.addItemButton.on('click', function () { this.listenTo(this.addItemButton, 'click', function () {
self.initItem(); self.initItem();
}); });
this.testDataInput.on('change', toggleTestData); this.listenTo(this.testDataInput, 'change', toggleTestData);
this.evaluator.setTestDataCache(this.testCache); this.evaluator.setTestDataCache(this.testCache);
this.evaluator.useTestData(false); this.evaluator.useTestData(false);
@ -186,5 +189,12 @@ define([
this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config); this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config);
}; };
TestDataManager.prototype.destroy = function () {
this.items.forEach(function (item) {
item.remove();
});
this.stopListening();
};
return TestDataManager; return TestDataManager;
}); });

View File

@ -0,0 +1,74 @@
var listenersCount = 0;
/*global define*/
// jscs:disable disallowDanglingUnderscores
define([], function () {
var helperFunctions = {
listenTo: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
var listener = {
object: object,
event: event,
callback: callback,
context: context,
_cb: !!context ? callback.bind(context) : callback
};
if (object.$watch && event.indexOf('change:') === 0) {
var scopePath = event.replace('change:', '');
listener.unlisten = object.$watch(scopePath, listener._cb, true);
} else if (object.$on) {
listener.unlisten = object.$on(event, listener._cb);
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
listenersCount++;
},
stopListening: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
listenersCount--;
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
}
};
return helperFunctions;
});

View File

@ -1,4 +1,8 @@
define(['./Select'], function (Select) { define([
'./Select'
], function (
Select
) {
/** /**
* Create a {Select} element whose composition is dynamically updated with * Create a {Select} element whose composition is dynamically updated with
@ -62,7 +66,7 @@ define(['./Select'], function (Select) {
onMetadataLoad(); onMetadataLoad();
} }
this.objectSelect.on('change', onObjectChange); this.objectSelect.on('change', onObjectChange, this);
this.manager.on('metadata', onMetadataLoad); this.manager.on('metadata', onMetadataLoad);
return this.select; return this.select;
@ -85,6 +89,10 @@ define(['./Select'], function (Select) {
} }
}; };
KeySelect.prototype.destroy = function () {
this.objectSelect.destroy();
};
return KeySelect; return KeySelect;
}); });

View File

@ -1,4 +1,10 @@
define(['./Select'], function (Select) { define([
'./Select',
'../eventHelpers'
], function (
Select,
eventHelpers
) {
/** /**
* Create a {Select} element whose composition is dynamically updated with * Create a {Select} element whose composition is dynamically updated with
@ -17,6 +23,7 @@ define(['./Select'], function (Select) {
var NULLVALUE = '- Select Comparison -'; var NULLVALUE = '- Select Comparison -';
function OperationSelect(config, keySelect, manager, changeCallback) { function OperationSelect(config, keySelect, manager, changeCallback) {
eventHelpers.extend(this);
var self = this; var self = this;
this.config = config; this.config = config;
@ -31,7 +38,7 @@ define(['./Select'], function (Select) {
this.select.hide(); this.select.hide();
this.select.addOption('', NULLVALUE); this.select.addOption('', NULLVALUE);
if (changeCallback) { if (changeCallback) {
this.select.on('change', changeCallback); this.listenTo(this.select, 'change', changeCallback);
} }
/** /**
@ -63,7 +70,6 @@ define(['./Select'], function (Select) {
} }
self.select.setSelected(self.config.operation); self.select.setSelected(self.config.operation);
} }
this.keySelect.on('change', onKeyChange); this.keySelect.on('change', onKeyChange);
this.manager.on('metadata', onMetadataLoad); this.manager.on('metadata', onMetadataLoad);
@ -109,6 +115,10 @@ define(['./Select'], function (Select) {
}); });
}; };
OperationSelect.prototype.destroy = function () {
this.stopListening();
};
return OperationSelect; return OperationSelect;
}); });

View File

@ -1,13 +1,14 @@
define([ define([
'../eventHelpers',
'text!../../res/input/paletteTemplate.html', 'text!../../res/input/paletteTemplate.html',
'EventEmitter', 'EventEmitter',
'zepto' 'zepto'
], function ( ], function (
eventHelpers,
paletteTemplate, paletteTemplate,
EventEmitter, EventEmitter,
$ $
) { ) {
/** /**
* Instantiates a new Open MCT Color Palette input * Instantiates a new Open MCT Color Palette input
* @constructor * @constructor
@ -19,6 +20,8 @@ define([
* up to the descendent class * up to the descendent class
*/ */
function Palette(cssClass, container, items) { function Palette(cssClass, container, items) {
eventHelpers.extend(this);
var self = this; var self = this;
this.cssClass = cssClass; this.cssClass = cssClass;
@ -49,8 +52,8 @@ define([
$('.menu', self.domElement).hide(); $('.menu', self.domElement).hide();
$(document).on('click', this.hideMenu); this.listenTo($(document), 'click', this.hideMenu);
$('.l-click-area', self.domElement).on('click', function (event) { this.listenTo($('.l-click-area', self.domElement), 'click', function (event) {
event.stopPropagation(); event.stopPropagation();
$('.menu', self.container).hide(); $('.menu', self.container).hide();
$('.menu', self.domElement).show(); $('.menu', self.domElement).show();
@ -69,7 +72,7 @@ define([
$('.menu', self.domElement).hide(); $('.menu', self.domElement).hide();
} }
$('.s-palette-item', self.domElement).on('click', handleItemClick); this.listenTo($('.s-palette-item', self.domElement), 'click', handleItemClick);
} }
/** /**
@ -83,7 +86,7 @@ define([
* Clean up any event listeners registered to DOM elements external to the widget * Clean up any event listeners registered to DOM elements external to the widget
*/ */
Palette.prototype.destroy = function () { Palette.prototype.destroy = function () {
$(document).off('click', this.hideMenu); this.stopListening();
}; };
Palette.prototype.hideMenu = function () { Palette.prototype.hideMenu = function () {

View File

@ -1,8 +1,10 @@
define([ define([
'../eventHelpers',
'text!../../res/input/selectTemplate.html', 'text!../../res/input/selectTemplate.html',
'EventEmitter', 'EventEmitter',
'zepto' 'zepto'
], function ( ], function (
eventHelpers,
selectTemplate, selectTemplate,
EventEmitter, EventEmitter,
$ $
@ -14,6 +16,8 @@ define([
* @constructor * @constructor
*/ */
function Select() { function Select() {
eventHelpers.extend(this);
var self = this; var self = this;
this.domElement = $(selectTemplate); this.domElement = $(selectTemplate);
@ -36,7 +40,7 @@ define([
self.eventEmitter.emit('change', value[0]); self.eventEmitter.emit('change', value[0]);
} }
$('select', this.domElement).on('change', onChange); this.listenTo($('select', this.domElement), 'change', onChange, this);
} }
/** /**
@ -140,5 +144,9 @@ define([
$('.equal-to').removeClass('hidden'); $('.equal-to').removeClass('hidden');
}; };
Select.prototype.destroy = function () {
this.stopListening();
};
return Select; return Select;
}); });