diff --git a/src/api/objects/MutableObject.js b/src/api/objects/MutableObject.js index 3386cab4fd..82736e0aa1 100644 --- a/src/api/objects/MutableObject.js +++ b/src/api/objects/MutableObject.js @@ -48,6 +48,7 @@ define([ this.unlisteners.forEach(function (unlisten) { unlisten(); }); + this.unlisteners = []; }; /** diff --git a/src/plugins/summaryWidget/src/Condition.js b/src/plugins/summaryWidget/src/Condition.js index 983a7cd0a4..3dd9586f37 100644 --- a/src/plugins/summaryWidget/src/Condition.js +++ b/src/plugins/summaryWidget/src/Condition.js @@ -3,6 +3,7 @@ define([ './input/ObjectSelect', './input/KeySelect', './input/OperationSelect', + './eventHelpers', 'EventEmitter', 'zepto' ], function ( @@ -10,10 +11,10 @@ define([ ObjectSelect, KeySelect, OperationSelect, + eventHelpers, EventEmitter, $ ) { - /** * Represents an individual condition for a summary widget rule. Manages the * associated inputs and view. @@ -25,6 +26,7 @@ define([ * selects with configuration data */ function Condition(conditionConfig, index, conditionManager) { + eventHelpers.extend(this); this.config = conditionConfig; this.index = index; this.conditionManager = conditionManager; @@ -71,15 +73,17 @@ define([ value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber), inputIndex = self.valueInputs.indexOf(elem); - self.eventEmitter.emit('change', { - value: value, - property: 'values[' + inputIndex + ']', - index: self.index - }); + if (elem.tagName.toUpperCase() === 'INPUT') { + self.eventEmitter.emit('change', { + value: value, + property: 'values[' + inputIndex + ']', + index: self.index + }); + } } - this.deleteButton.on('click', this.remove); - this.duplicateButton.on('click', this.duplicate); + this.listenTo(this.deleteButton, 'click', this.remove, this); + this.listenTo(this.duplicateButton, 'click', this.duplicate, this); this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ ['any', 'any telemetry'], @@ -105,7 +109,7 @@ define([ $('.t-configuration', self.domElement).append(select.getDOM()); }); - $(this.domElement).on('input', 'input', onValueInput); + this.listenTo($(this.domElement), 'input', onValueInput); } Condition.prototype.getDOM = function (container) { @@ -139,6 +143,14 @@ define([ */ Condition.prototype.remove = function () { this.eventEmitter.emit('remove', this.index); + this.destroy(); + }; + + Condition.prototype.destroy = function () { + this.stopListening(); + Object.values(this.selects).forEach(function (select) { + select.destroy(); + }); }; /** diff --git a/src/plugins/summaryWidget/src/Rule.js b/src/plugins/summaryWidget/src/Rule.js index a6487fa43d..516bc11726 100644 --- a/src/plugins/summaryWidget/src/Rule.js +++ b/src/plugins/summaryWidget/src/Rule.js @@ -3,6 +3,7 @@ define([ './Condition', './input/ColorPalette', './input/IconPalette', + './eventHelpers', 'EventEmitter', 'lodash', 'zepto' @@ -11,11 +12,11 @@ define([ Condition, ColorPalette, IconPalette, + eventHelpers, EventEmitter, _, $ ) { - /** * An object representing a summary widget rule. Maintains a set of text * 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 */ function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { + eventHelpers.extend(this); var self = this; this.config = ruleConfig; @@ -196,24 +198,24 @@ define([ Object.keys(this.textInputs).forEach(function (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); }); }); - this.deleteButton.on('click', this.remove); - this.duplicateButton.on('click', this.duplicate); - this.addConditionButton.on('click', function () { + this.listenTo(this.deleteButton, 'click', this.remove); + this.listenTo(this.duplicateButton, 'click', this.duplicate); + this.listenTo(this.addConditionButton, 'click', function () { self.initCondition(); }); - this.toggleConfigButton.on('click', toggleConfig); - this.trigger.on('change', onTriggerInput); + this.listenTo(this.toggleConfigButton, 'click', toggleConfig); + this.listenTo(this.trigger, 'change', onTriggerInput); this.title.html(self.config.name); this.description.html(self.config.description); this.trigger.prop('value', self.config.trigger); - this.grippy.on('mousedown', onDragStart); + this.listenTo(this.grippy, 'mousedown', onDragStart); this.widgetDnD.on('drop', function () { this.domElement.show(); $('.t-drag-indicator').hide(); @@ -258,6 +260,10 @@ define([ palette.destroy(); }); this.iconInput.destroy(); + this.stopListening(); + this.conditions.forEach(function (condition) { + condition.destroy(); + }); }; /** diff --git a/src/plugins/summaryWidget/src/SummaryWidget.js b/src/plugins/summaryWidget/src/SummaryWidget.js index 98bf2721d3..100718572d 100644 --- a/src/plugins/summaryWidget/src/SummaryWidget.js +++ b/src/plugins/summaryWidget/src/SummaryWidget.js @@ -4,6 +4,7 @@ define([ './ConditionManager', './TestDataManager', './WidgetDnD', + './eventHelpers', 'lodash', 'zepto' ], function ( @@ -12,6 +13,7 @@ define([ ConditionManager, TestDataManager, WidgetDnD, + eventHelpers, _, $ ) { @@ -32,6 +34,8 @@ define([ * @param {MCT} openmct An MCT instance */ function SummaryWidget(domainObject, openmct) { + eventHelpers.extend(this); + this.domainObject = domainObject; this.openmct = openmct; @@ -86,7 +90,7 @@ define([ self.outerWrapper.toggleClass('expanded-widget-test-data'); self.toggleTestDataControl.toggleClass('expanded'); } - this.toggleTestDataControl.on('click', toggleTestData); + this.listenTo(this.toggleTestDataControl, 'click', toggleTestData); /** * Toggles the configuration area for rules in the view @@ -96,7 +100,7 @@ define([ self.outerWrapper.toggleClass('expanded-widget-rules'); self.toggleRulesControl.toggleClass('expanded'); } - this.toggleRulesControl.on('click', toggleRules); + this.listenTo(this.toggleRulesControl, 'click', toggleRules); openmct.$injector.get('objectService') .getObjects([id]) @@ -160,13 +164,15 @@ define([ this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById); this.initRule('default', 'Default'); this.domainObject.configuration.ruleOrder.forEach(function (ruleId) { - self.initRule(ruleId); + if (ruleId !== 'default') { + self.initRule(ruleId); + } }); this.refreshRules(); this.updateWidget(); this.updateView(); - this.addRuleButton.on('click', this.addRule); + this.listenTo(this.addRuleButton, 'click', this.addRule); this.conditionManager.on('receiveTelemetry', this.executeRules, this); this.widgetDnD.on('drop', this.reorder, this); }; @@ -178,11 +184,14 @@ define([ SummaryWidget.prototype.destroy = function (container) { this.editListenerUnsubscribe(); this.conditionManager.destroy(); + this.testDataManager.destroy(); this.widgetDnD.destroy(); this.watchForChangesUnsubscribe(); Object.values(this.rulesById).forEach(function (rule) { rule.destroy(); }); + + this.stopListening(); }; /** diff --git a/src/plugins/summaryWidget/src/TestDataItem.js b/src/plugins/summaryWidget/src/TestDataItem.js index b18380b259..a4af9ff7bd 100644 --- a/src/plugins/summaryWidget/src/TestDataItem.js +++ b/src/plugins/summaryWidget/src/TestDataItem.js @@ -2,12 +2,14 @@ define([ 'text!../res/testDataItemTemplate.html', './input/ObjectSelect', './input/KeySelect', + './eventHelpers', 'EventEmitter', 'zepto' ], function ( itemTemplate, ObjectSelect, KeySelect, + eventHelpers, EventEmitter, $ ) { @@ -24,6 +26,7 @@ define([ * @constructor */ function TestDataItem(itemConfig, index, conditionManager) { + eventHelpers.extend(this); this.config = itemConfig; this.index = index; this.conditionManager = conditionManager; @@ -70,16 +73,17 @@ define([ function onValueInput(event) { var elem = event.target, value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber); - - self.eventEmitter.emit('change', { - value: value, - property: 'value', - index: self.index - }); + if (elem.tagName.toUpperCase() === 'INPUT') { + self.eventEmitter.emit('change', { + value: value, + property: 'value', + index: self.index + }); + } } - this.deleteButton.on('click', this.remove); - this.duplicateButton.on('click', this.duplicate); + this.listenTo(this.deleteButton, 'click', this.remove); + this.listenTo(this.duplicateButton, 'click', this.duplicate); this.selects.object = new ObjectSelect(this.config, this.conditionManager); this.selects.key = new KeySelect( @@ -97,8 +101,7 @@ define([ Object.values(this.selects).forEach(function (select) { $('.t-configuration', self.domElement).append(select.getDOM()); }); - - $(this.domElement).on('input', 'input', onValueInput); + this.listenTo(this.domElement, 'input', onValueInput); } /** @@ -137,6 +140,11 @@ define([ TestDataItem.prototype.remove = function () { var self = this; this.eventEmitter.emit('remove', self.index); + this.stopListening(); + + Object.values(this.selects).forEach(function (select) { + select.destroy(); + }); }; /** diff --git a/src/plugins/summaryWidget/src/TestDataManager.js b/src/plugins/summaryWidget/src/TestDataManager.js index 570eec0951..30923795a9 100644 --- a/src/plugins/summaryWidget/src/TestDataManager.js +++ b/src/plugins/summaryWidget/src/TestDataManager.js @@ -1,9 +1,11 @@ define([ + './eventHelpers', 'text!../res/testDataTemplate.html', './TestDataItem', 'zepto', 'lodash' ], function ( + eventHelpers, testDataTemplate, TestDataItem, $, @@ -18,6 +20,7 @@ define([ * @param {MCT} openmct and MCT instance */ function TestDataManager(domainObject, conditionManager, openmct) { + eventHelpers.extend(this); var self = this; this.domainObject = domainObject; @@ -45,10 +48,10 @@ define([ self.updateTestCache(); } - this.addItemButton.on('click', function () { + this.listenTo(this.addItemButton, 'click', function () { self.initItem(); }); - this.testDataInput.on('change', toggleTestData); + this.listenTo(this.testDataInput, 'change', toggleTestData); this.evaluator.setTestDataCache(this.testCache); this.evaluator.useTestData(false); @@ -186,5 +189,12 @@ define([ 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; }); diff --git a/src/plugins/summaryWidget/src/eventHelpers.js b/src/plugins/summaryWidget/src/eventHelpers.js new file mode 100644 index 0000000000..3dc80de611 --- /dev/null +++ b/src/plugins/summaryWidget/src/eventHelpers.js @@ -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; +}); diff --git a/src/plugins/summaryWidget/src/input/KeySelect.js b/src/plugins/summaryWidget/src/input/KeySelect.js index 07edf6cd97..bb96f63e2f 100644 --- a/src/plugins/summaryWidget/src/input/KeySelect.js +++ b/src/plugins/summaryWidget/src/input/KeySelect.js @@ -1,4 +1,8 @@ -define(['./Select'], function (Select) { +define([ + './Select' +], function ( + Select +) { /** * Create a {Select} element whose composition is dynamically updated with @@ -62,7 +66,7 @@ define(['./Select'], function (Select) { onMetadataLoad(); } - this.objectSelect.on('change', onObjectChange); + this.objectSelect.on('change', onObjectChange, this); this.manager.on('metadata', onMetadataLoad); return this.select; @@ -85,6 +89,10 @@ define(['./Select'], function (Select) { } }; + KeySelect.prototype.destroy = function () { + this.objectSelect.destroy(); + }; + return KeySelect; }); diff --git a/src/plugins/summaryWidget/src/input/OperationSelect.js b/src/plugins/summaryWidget/src/input/OperationSelect.js index c33b9f6064..8ee6b65f56 100644 --- a/src/plugins/summaryWidget/src/input/OperationSelect.js +++ b/src/plugins/summaryWidget/src/input/OperationSelect.js @@ -1,4 +1,10 @@ -define(['./Select'], function (Select) { +define([ + './Select', + '../eventHelpers' +], function ( + Select, + eventHelpers +) { /** * Create a {Select} element whose composition is dynamically updated with @@ -17,6 +23,7 @@ define(['./Select'], function (Select) { var NULLVALUE = '- Select Comparison -'; function OperationSelect(config, keySelect, manager, changeCallback) { + eventHelpers.extend(this); var self = this; this.config = config; @@ -31,7 +38,7 @@ define(['./Select'], function (Select) { this.select.hide(); this.select.addOption('', NULLVALUE); 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); } - this.keySelect.on('change', onKeyChange); this.manager.on('metadata', onMetadataLoad); @@ -109,6 +115,10 @@ define(['./Select'], function (Select) { }); }; + OperationSelect.prototype.destroy = function () { + this.stopListening(); + }; + return OperationSelect; }); diff --git a/src/plugins/summaryWidget/src/input/Palette.js b/src/plugins/summaryWidget/src/input/Palette.js index 7bda402ccb..bdc1a54d9e 100644 --- a/src/plugins/summaryWidget/src/input/Palette.js +++ b/src/plugins/summaryWidget/src/input/Palette.js @@ -1,13 +1,14 @@ define([ + '../eventHelpers', 'text!../../res/input/paletteTemplate.html', 'EventEmitter', 'zepto' ], function ( + eventHelpers, paletteTemplate, EventEmitter, $ ) { - /** * Instantiates a new Open MCT Color Palette input * @constructor @@ -19,6 +20,8 @@ define([ * up to the descendent class */ function Palette(cssClass, container, items) { + eventHelpers.extend(this); + var self = this; this.cssClass = cssClass; @@ -49,8 +52,8 @@ define([ $('.menu', self.domElement).hide(); - $(document).on('click', this.hideMenu); - $('.l-click-area', self.domElement).on('click', function (event) { + this.listenTo($(document), 'click', this.hideMenu); + this.listenTo($('.l-click-area', self.domElement), 'click', function (event) { event.stopPropagation(); $('.menu', self.container).hide(); $('.menu', self.domElement).show(); @@ -69,7 +72,7 @@ define([ $('.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 */ Palette.prototype.destroy = function () { - $(document).off('click', this.hideMenu); + this.stopListening(); }; Palette.prototype.hideMenu = function () { diff --git a/src/plugins/summaryWidget/src/input/Select.js b/src/plugins/summaryWidget/src/input/Select.js index a5cc1390de..d0619a6896 100644 --- a/src/plugins/summaryWidget/src/input/Select.js +++ b/src/plugins/summaryWidget/src/input/Select.js @@ -1,8 +1,10 @@ define([ + '../eventHelpers', 'text!../../res/input/selectTemplate.html', 'EventEmitter', 'zepto' ], function ( + eventHelpers, selectTemplate, EventEmitter, $ @@ -14,6 +16,8 @@ define([ * @constructor */ function Select() { + eventHelpers.extend(this); + var self = this; this.domElement = $(selectTemplate); @@ -36,7 +40,7 @@ define([ 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'); }; + Select.prototype.destroy = function () { + this.stopListening(); + }; + return Select; });