Compare commits
	
		
			49 Commits
		
	
	
		
			summary-wi
			...
			v0.13.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 94fa70abb1 | ||
|   | 12574a1333 | ||
|   | dc91a94f0e | ||
|   | 0243aa6584 | ||
|   | e5d869f01e | ||
|   | d4e3e6689c | ||
|   | 0363d0e8ad | ||
|   | 3669e776a9 | ||
|   | 5d3adc6a7f | ||
|   | c1b2db848a | ||
|   | 5d19294c11 | ||
|   | 9b8d5f3f9c | ||
|   | d03f323a9b | ||
|   | 54a453e5a0 | ||
|   | 14894cf197 | ||
|   | 0c6786198a | ||
|   | 6d077b775d | ||
|   | 144437a06e | ||
|   | 557cd91b21 | ||
|   | 39d3e92094 | ||
|   | 7529a86d01 | ||
|   | d34e36831c | ||
|   | aa8fa9168a | ||
|   | 3f1b7e0a87 | ||
|   | 5ec3b98d1c | ||
|   | 1ad5094b72 | ||
|   | b54ee2257e | ||
|   | fcef4274e5 | ||
|   | 744a5340d3 | ||
|   | d140051054 | ||
|   | 8da74f2665 | ||
|   | 2390278b97 | ||
|   | 8a66731271 | ||
|   | 0a9ea48355 | ||
|   | 01d93306f3 | ||
|   | 0588f9190a | ||
|   | 1378b57567 | ||
|   | 9e12886c66 | ||
|   | 2d352ac574 | ||
|   | 284dec4903 | ||
|   | 5a0656c700 | ||
|   | 425655bae0 | ||
|   | 50b4d5cb28 | ||
|   | bc62d7d5ae | ||
|   | c0dcf4495e | ||
|   | a51b9bc63f | ||
|   | ff003c3dab | ||
|   | de7c4d2ce3 | ||
|   | 4b07930305 | 
| @@ -21,5 +21,6 @@ | ||||
|     "shadow": "outer", | ||||
|     "strict": "implied", | ||||
|     "undef": true, | ||||
|     "unused": "vars" | ||||
|     "unused": "vars", | ||||
|     "latedef": "nofunc" | ||||
| } | ||||
|   | ||||
| @@ -88,7 +88,7 @@ and [`gulp`](http://gulpjs.com/). | ||||
|  | ||||
| To build Open MCT for deployment: | ||||
|  | ||||
| `npm run prepublish` | ||||
| `npm run prepare` | ||||
|  | ||||
| This will compile and minify JavaScript sources, as well as copy over assets. | ||||
| The contents of the `dist` folder will contain a runnable Open MCT | ||||
|   | ||||
							
								
								
									
										10
									
								
								circle.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								circle.yml
									
									
									
									
									
								
							| @@ -1,3 +1,11 @@ | ||||
| machine: | ||||
|   node: | ||||
|     version: 4.7.0 | ||||
|  | ||||
| dependencies: | ||||
|   pre: | ||||
|     - npm install -g npm@latest | ||||
|  | ||||
| deployment: | ||||
|   production: | ||||
|     branch: master | ||||
| @@ -16,4 +24,4 @@ test: | ||||
| general: | ||||
|   branches: | ||||
|     ignore: | ||||
|       - gh-pages | ||||
|       - gh-pages | ||||
| @@ -2283,7 +2283,7 @@ To install build dependencies (only needs to be run once): | ||||
|  | ||||
| To build: | ||||
|  | ||||
| `npm run prepublish` | ||||
| `npm run prepare` | ||||
|  | ||||
| This will compile and minify JavaScript sources, as well as copy over assets. | ||||
| The contents of the `dist` folder will contain a runnable Open MCT | ||||
|   | ||||
							
								
								
									
										121
									
								
								docs/src/guide/security.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								docs/src/guide/security.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| # Security Guide | ||||
|  | ||||
| Open MCT is a rich client with plugin support that executes as a single page | ||||
| web application in a browser environment. Security concerns and | ||||
| vulnerabilities associated with the web as a platform should be considered | ||||
| before deploying Open MCT (or any other web application) for mission or | ||||
| production usage. | ||||
|  | ||||
| This document describes several important points to consider when developing | ||||
| for or deploying Open MCT securely. Other resources such as | ||||
| [Open Web Application Security Project (OWASP)](https://www.owasp.org) | ||||
| provide a deeper and more general overview of security for web applications. | ||||
|  | ||||
|  | ||||
| ## Security Model | ||||
|  | ||||
| Open MCT has been architected assuming the following deployment pattern: | ||||
|  | ||||
| * A tagged, tested Open MCT version will be used. | ||||
| * Externally authored plugins will be installed. | ||||
| * A server will provide persistent storage, telemetry, and other shared data. | ||||
| * Authorization, authentication, and auditing will be handled by a server. | ||||
|  | ||||
|  | ||||
| ## Security Procedures | ||||
|  | ||||
| The Open MCT team secures our code base using a combination of code review, | ||||
| dependency review, and periodic security reviews. Static analysis performed  | ||||
| during automated verification additionally safeguards against common  | ||||
| coding errors which may result in vulnerabilities. | ||||
|  | ||||
|  | ||||
| ### Code Review | ||||
|  | ||||
| All contributions are reviewed by internal team members. External | ||||
| contributors receive increased scrutiny for security and quality, | ||||
| and must sign a licensing agreement. | ||||
|  | ||||
| ### Dependency Review | ||||
|  | ||||
| Before integrating third-party dependencies, they are reviewed for security | ||||
| and quality, with consideration given to authors and users of these | ||||
| dependencies, as well as review of open source code. | ||||
|  | ||||
| ### Periodic Security Reviews | ||||
|  | ||||
| Open MCT's code, design, and architecture are periodically reviewed | ||||
| (approximately annually) for common security issues, such as the | ||||
| [OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project). | ||||
|  | ||||
|  | ||||
| ## Security Concerns | ||||
|  | ||||
| Certain security concerns deserve special attention when deploying Open MCT, | ||||
| or when authoring plugins. | ||||
|  | ||||
| ### Identity Spoofing | ||||
|  | ||||
| Open MCT issues calls to web services with the privileges of a logged in user. | ||||
| Compromised sources (either for Open MCT itself or a plugin) could | ||||
| therefore allow malicious code to execute with those privileges. | ||||
|  | ||||
| To avoid this: | ||||
|  | ||||
| * Serve Open MCT and other scripts over SSL (https rather than http) | ||||
|   to prevent man-in-the-middle attacks. | ||||
| * Exercise precautions such as security reviews for any plugins or | ||||
|   applications built for or with Open MCT to reject malicious changes. | ||||
|  | ||||
| ### Information Disclosure | ||||
|  | ||||
| If Open MCT is used to handle or display sensitive data, any components | ||||
| (such as adapter plugins) must take care to avoid leaking or disclosing | ||||
| this information. For example, avoid sending sensitive data to third-party | ||||
| servers or insecure APIs. | ||||
|  | ||||
| ### Data Tampering | ||||
|  | ||||
| The web application architecture leaves open the possibility that direct | ||||
| calls will be made to back-end services, circumventing Open MCT entirely. | ||||
| As such, Open MCT assumes that server components will perform any necessary | ||||
| data validation during calls issues to the server. | ||||
|  | ||||
| Additionally, plugins which serialize and write data to the server must | ||||
| escape that data to avoid database injection attacks, and similar. | ||||
|  | ||||
| ### Repudiation | ||||
|  | ||||
| Open MCT assumes that servers log any relevant interactions and associates | ||||
| these with a user identity; the specific user actions taken within the | ||||
| application are assumed not to be of concern for auditing. | ||||
|  | ||||
| In the absence of server-side logging, users may disclaim (maliciously, | ||||
| mistakenly, or otherwise) actions taken within the system without any | ||||
| way to prove otherwise. | ||||
|  | ||||
| If keeping client-level interactions is important, this will need to be | ||||
| implemented via a plugin. | ||||
|  | ||||
| ### Denial-of-service | ||||
|  | ||||
| Open MCT assumes that server-side components will be insulated against | ||||
| denial-of-service attacks. Services should only permit resource-intensive | ||||
| tasks to be initiated by known or trusted users. | ||||
|  | ||||
| ### Elevation of Privilege | ||||
|  | ||||
| Corollary to the assumption that servers guide against identity spoofing, | ||||
| Open MCT assumes that services do not allow a user to act with | ||||
| inappropriately escalated privileges. Open MCT cannot protect against | ||||
| such escalation; in the clearest case, a malicious actor could interact | ||||
| with web services directly to exploit such a vulnerability. | ||||
|  | ||||
| ## Additional Reading | ||||
|  | ||||
| The following resources have been used as a basis for identifying potential | ||||
| security threats to Open MCT deployments in preparation of this document: | ||||
|  | ||||
| * [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE) | ||||
| * [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet) | ||||
| * [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) | ||||
| @@ -44,9 +44,7 @@ define([ | ||||
|         message = message.data; | ||||
|         var callback = this.callbacks[message.id]; | ||||
|         if (callback) { | ||||
|             if (callback(message)) { | ||||
|                 delete this.callbacks[message.id]; | ||||
|             } | ||||
|             callback(message); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
| @@ -72,6 +70,7 @@ define([ | ||||
|             deferred.resolve = resolve; | ||||
|             deferred.reject = reject; | ||||
|         }); | ||||
|         var messageId; | ||||
|  | ||||
|         function callback(message) { | ||||
|             if (message.error) { | ||||
| @@ -79,33 +78,27 @@ define([ | ||||
|             } else { | ||||
|                 deferred.resolve(message.data); | ||||
|             } | ||||
|             return true; | ||||
|             delete this.callbacks[messageId]; | ||||
|         } | ||||
|  | ||||
|         this.dispatch('request', request, callback); | ||||
|         messageId = this.dispatch('request', request, callback.bind(this)); | ||||
|  | ||||
|         return promise; | ||||
|     }; | ||||
|  | ||||
|     WorkerInterface.prototype.subscribe = function (request, cb) { | ||||
|         var isCancelled = false; | ||||
|  | ||||
|         var callback = function (message) { | ||||
|             if (isCancelled) { | ||||
|                 return true; | ||||
|             } | ||||
|         function callback(message) { | ||||
|             cb(message.data); | ||||
|         }; | ||||
|  | ||||
|         var messageId = this.dispatch('subscribe', request, callback) | ||||
|         var messageId = this.dispatch('subscribe', request, callback); | ||||
|  | ||||
|         return function () { | ||||
|             isCancelled = true; | ||||
|             this.dispatch('unsubscribe', { | ||||
|                 id: messageId | ||||
|             }); | ||||
|             delete this.callbacks[messageId]; | ||||
|         }.bind(this); | ||||
|  | ||||
|     }; | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,8 @@ | ||||
|  | ||||
| /*global require,__dirname*/ | ||||
|  | ||||
| require("v8-compile-cache"); | ||||
|  | ||||
| var gulp = require('gulp'), | ||||
|     sourcemaps = require('gulp-sourcemaps'), | ||||
|     path = require('path'), | ||||
| @@ -177,4 +179,4 @@ gulp.task('install', [ 'assets', 'scripts' ]); | ||||
|  | ||||
| gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]); | ||||
|  | ||||
| gulp.task('build', [ 'verify', 'install' ]); | ||||
| gulp.task('build', [ 'verify', 'install' ]); | ||||
| @@ -43,6 +43,9 @@ | ||||
|             openmct.install(openmct.plugins.ExampleImagery()); | ||||
|             openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|             openmct.install(openmct.plugins.ImportExport()); | ||||
|             openmct.install(openmct.plugins.AutoflowView({ | ||||
|                 type: "telemetry.panel" | ||||
|             })); | ||||
|             openmct.install(openmct.plugins.Conductor({ | ||||
|                 menuOptions: [ | ||||
|                     { | ||||
|   | ||||
| @@ -36,6 +36,7 @@ module.exports = function(config) { | ||||
|         files: [ | ||||
|             {pattern: 'bower_components/**/*.js', included: false}, | ||||
|             {pattern: 'node_modules/d3-*/**/*.js', included: false}, | ||||
|             {pattern: 'node_modules/vue/**/*.js', included: false}, | ||||
|             {pattern: 'src/**/*.js', included: false}, | ||||
|             {pattern: 'example/**/*.html', included: false}, | ||||
|             {pattern: 'example/**/*.js', included: false}, | ||||
|   | ||||
| @@ -37,9 +37,10 @@ requirejs.config({ | ||||
|         "screenfull": "bower_components/screenfull/dist/screenfull.min", | ||||
|         "text": "bower_components/text/text", | ||||
|         "uuid": "bower_components/node-uuid/uuid", | ||||
|         "vue": "node_modules/vue/dist/vue.min", | ||||
|         "zepto": "bower_components/zepto/zepto.min", | ||||
|         "lodash": "bower_components/lodash/lodash", | ||||
|         "d3-selection": "node_modules/d3-selection/build/d3-selection.min", | ||||
|         "d3-selection": "node_modules/d3-selection/dist/d3-selection.min", | ||||
|         "d3-scale": "node_modules/d3-scale/build/d3-scale.min", | ||||
|         "d3-axis": "node_modules/d3-axis/build/d3-axis.min", | ||||
|         "d3-array": "node_modules/d3-array/build/d3-array.min", | ||||
| @@ -66,6 +67,9 @@ requirejs.config({ | ||||
|         "moment-duration-format": { | ||||
|             "deps": ["moment"] | ||||
|         }, | ||||
|         "saveAs": { | ||||
|             "exports": "saveAs" | ||||
|         }, | ||||
|         "screenfull": { | ||||
|             "exports": "screenfull" | ||||
|         }, | ||||
|   | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "0.12.1-SNAPSHOT", | ||||
|   "version": "0.13.1", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": { | ||||
|     "d3-array": "^1.0.2", | ||||
| @@ -15,7 +15,8 @@ | ||||
|     "d3-time-format": "^2.0.3", | ||||
|     "express": "^4.13.1", | ||||
|     "minimist": "^1.1.1", | ||||
|     "request": "^2.69.0" | ||||
|     "request": "^2.69.0", | ||||
|     "vue": "^2.5.6" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "bower": "^1.7.7", | ||||
| @@ -49,7 +50,8 @@ | ||||
|     "moment": "^2.11.1", | ||||
|     "node-bourbon": "^4.2.3", | ||||
|     "requirejs": "2.1.x", | ||||
|     "split": "^1.0.0" | ||||
|     "split": "^1.0.0", | ||||
|     "v8-compile-cache": "^1.1.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "node app.js", | ||||
| @@ -59,7 +61,7 @@ | ||||
|     "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", | ||||
|     "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", | ||||
|     "docs": "npm run jsdoc ; npm run otherdoc", | ||||
|     "prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install" | ||||
|     "prepare": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install" | ||||
|   }, | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|   | ||||
| @@ -57,7 +57,12 @@ | ||||
|             </div> | ||||
|             <mct-representation key="representation.selected.key" | ||||
|                                 mct-object="representation.selected.key && domainObject" | ||||
|                                 class="abs flex-elem grows object-holder-main scroll"> | ||||
|                                 class="abs flex-elem grows object-holder-main scroll" | ||||
|                                 mct-selectable="{ | ||||
|                                     item: domainObject.useCapability('adapter'), | ||||
|                                     oldItem: domainObject | ||||
|                                 }" | ||||
|                                 mct-init-select> | ||||
|             </mct-representation> | ||||
|         </div> | ||||
|     </div> | ||||
|   | ||||
| @@ -19,12 +19,21 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div ng-controller="InspectorController"> | ||||
|     <div ng-repeat="region in regions"> | ||||
| <div ng-controller="InspectorController as controller"> | ||||
|         <mct-representation | ||||
|                 key="region.content.key" | ||||
|                 mct-object="domainObject" | ||||
|                 key="'object-properties'" | ||||
|                 mct-object="controller.selectedItem()" | ||||
|                 ng-model="ngModel"> | ||||
|         </mct-representation> | ||||
|     </div> | ||||
|  | ||||
|         <div ng-if="!controller.hasProviderView()"> | ||||
|             <mct-representation | ||||
|                     key="inspectorKey" | ||||
|                     mct-object="controller.selectedItem()" | ||||
|                     ng-model="ngModel"> | ||||
|             </mct-representation> | ||||
|         </div> | ||||
|  | ||||
|         <div class='inspector-provider-view'> | ||||
|         </div> | ||||
| </div> | ||||
|   | ||||
| @@ -38,8 +38,6 @@ | ||||
|                   ng-class="{ last:($index + 1) === contextualParents.length }"> | ||||
|                 <mct-representation key="'label'" | ||||
|                                     mct-object="parent" | ||||
|                                     ng-model="ngModel" | ||||
|                                     ng-click="ngModel.selectedObject = parent" | ||||
|                                     class="location-item"> | ||||
|                 </mct-representation> | ||||
|             </span> | ||||
| @@ -51,8 +49,6 @@ | ||||
|                   ng-class="{ last:($index + 1) === primaryParents.length }"> | ||||
|                 <mct-representation key="'label'" | ||||
|                                     mct-object="parent" | ||||
|                                     ng-model="ngModel" | ||||
|                                     ng-click="ngModel.selectedObject = parent" | ||||
|                                     class="location-item"> | ||||
|                 </mct-representation> | ||||
|             </span> | ||||
|   | ||||
| @@ -121,7 +121,8 @@ define([ | ||||
|                     "key": "ElementsController", | ||||
|                     "implementation": ElementsController, | ||||
|                     "depends": [ | ||||
|                         "$scope" | ||||
|                         "$scope", | ||||
|                         "openmct" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
| @@ -299,9 +300,6 @@ define([ | ||||
|                 { | ||||
|                     "key": "edit-elements", | ||||
|                     "template": elementsTemplate, | ||||
|                     "uses": [ | ||||
|                         "composition" | ||||
|                     ], | ||||
|                     "gestures": [ | ||||
|                         "drop" | ||||
|                     ] | ||||
| @@ -385,7 +383,10 @@ define([ | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "implementation": EditToolbarRepresenter | ||||
|                     "implementation": EditToolbarRepresenter, | ||||
|                     "depends": [ | ||||
|                         "openmct" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             "constants": [ | ||||
|   | ||||
| @@ -61,7 +61,12 @@ | ||||
|             <mct-representation key="representation.selected.key" | ||||
|                                 mct-object="representation.selected.key && domainObject" | ||||
|                                 class="abs flex-elem grows object-holder-main scroll" | ||||
|                                 toolbar="toolbar"> | ||||
|                                 toolbar="toolbar" | ||||
|                                 mct-selectable="{ | ||||
|                                     item: domainObject.useCapability('adapter'), | ||||
|                                     oldItem: domainObject | ||||
|                                 }" | ||||
|                                 mct-init-select> | ||||
|             </mct-representation> | ||||
|         </div><!--/ l-object-wrapper-inner --> | ||||
|     </div> | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|                  ng-model="filterBy"> | ||||
|     </mct-include> | ||||
|     <div class="flex-elem grows vscroll"> | ||||
|         <ul class="tree"> | ||||
|         <ul class="tree" ng-if="composition.length > 0"> | ||||
|             <li ng-repeat="containedObject in composition | filter:searchElements"> | ||||
|                 <span class="tree-item"> | ||||
|                     <mct-representation | ||||
| @@ -36,5 +36,6 @@ | ||||
|                 </span> | ||||
|             </li> | ||||
|         </ul> | ||||
|         <div ng-if="composition.length === 0">No contained elements</div>     | ||||
|     </div> | ||||
| </div> | ||||
|   | ||||
| @@ -28,16 +28,6 @@ define( | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         function isDirty(domainObject) { | ||||
|             var navigatedObject = domainObject, | ||||
|                 editorCapability = navigatedObject && | ||||
|                     navigatedObject.getCapability("editor"); | ||||
|  | ||||
|             return editorCapability && | ||||
|                 editorCapability.isEditContextRoot() && | ||||
|                 editorCapability.dirty(); | ||||
|         } | ||||
|  | ||||
|         function cancelEditing(domainObject) { | ||||
|             var navigatedObject = domainObject, | ||||
|                 editorCapability = navigatedObject && | ||||
| @@ -59,10 +49,7 @@ define( | ||||
|  | ||||
|             var removeCheck = navigationService | ||||
|                 .checkBeforeNavigation(function () { | ||||
|                     if (isDirty(domainObject)) { | ||||
|                         return "Continuing will cause the loss of any unsaved changes."; | ||||
|                     } | ||||
|                     return false; | ||||
|                     return "Continuing will cause the loss of any unsaved changes."; | ||||
|                 }); | ||||
|  | ||||
|             $scope.$on('$destroy', function () { | ||||
|   | ||||
| @@ -29,7 +29,11 @@ define( | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function ElementsController($scope) { | ||||
|         function ElementsController($scope, openmct) { | ||||
|             this.scope = $scope; | ||||
|             this.scope.composition = []; | ||||
|             var self = this; | ||||
|  | ||||
|             function filterBy(text) { | ||||
|                 if (typeof text === 'undefined') { | ||||
|                     return $scope.searchText; | ||||
| @@ -47,10 +51,54 @@ define( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             function setSelection(selection) { | ||||
|                 if (!selection[0]) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (self.mutationListener) { | ||||
|                     self.mutationListener(); | ||||
|                     delete self.mutationListener; | ||||
|                 } | ||||
|  | ||||
|                 var domainObject = selection[0].context.oldItem; | ||||
|                 self.refreshComposition(domainObject); | ||||
|  | ||||
|                 if (domainObject) { | ||||
|                     self.mutationListener = domainObject.getCapability('mutation') | ||||
|                         .listen(self.refreshComposition.bind(self, domainObject)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             $scope.filterBy = filterBy; | ||||
|             $scope.searchElements = searchElements; | ||||
|  | ||||
|             openmct.selection.on('change', setSelection); | ||||
|             setSelection(openmct.selection.get()); | ||||
|  | ||||
|             $scope.$on("$destroy", function () { | ||||
|                 openmct.selection.off("change", setSelection); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Gets the composition for the selected object and populates the scope with it. | ||||
|          * | ||||
|          * @param domainObject the selected object | ||||
|          * @private | ||||
|          */ | ||||
|         ElementsController.prototype.refreshComposition = function (domainObject) { | ||||
|             var selectedObjectComposition = domainObject && domainObject.useCapability('composition'); | ||||
|  | ||||
|             if (selectedObjectComposition) { | ||||
|                 selectedObjectComposition.then(function (composition) { | ||||
|                     this.scope.composition = composition; | ||||
|                 }.bind(this)); | ||||
|             } else { | ||||
|                 this.scope.composition = []; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return ElementsController; | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -38,7 +38,7 @@ define( | ||||
|          * @constructor | ||||
|          * @implements {Representer} | ||||
|          */ | ||||
|         function EditToolbarRepresenter(scope, element, attrs) { | ||||
|         function EditToolbarRepresenter(openmct, scope, element, attrs) { | ||||
|             var self = this; | ||||
|  | ||||
|             // Mark changes as ready to persist | ||||
| @@ -109,6 +109,7 @@ define( | ||||
|             this.updateSelection = updateSelection; | ||||
|             this.toolbar = undefined; | ||||
|             this.toolbarObject = {}; | ||||
|             this.openmct = openmct; | ||||
|  | ||||
|             // If this representation exposes a toolbar, set up watches | ||||
|             // to synchronize with it. | ||||
| @@ -146,7 +147,7 @@ define( | ||||
|             // Expose the toolbar object to the parent scope | ||||
|             initialize(definition); | ||||
|             // Create a selection scope | ||||
|             this.setSelection(new EditToolbarSelection()); | ||||
|             this.setSelection(new EditToolbarSelection(this.openmct)); | ||||
|             // Initialize toolbar to an empty selection | ||||
|             this.updateSelection([]); | ||||
|         }; | ||||
|   | ||||
| @@ -38,10 +38,24 @@ define( | ||||
|          * @memberof platform/commonUI/edit | ||||
|          * @constructor | ||||
|          */ | ||||
|         function EditToolbarSelection() { | ||||
|         function EditToolbarSelection(openmct) { | ||||
|             this.selection = [{}]; | ||||
|             this.selecting = false; | ||||
|             this.selectedObj = undefined; | ||||
|  | ||||
|             openmct.selection.on('change', function (selection) { | ||||
|                 var selected = selection[0]; | ||||
|  | ||||
|                 if (selected && selected.context.toolbar) { | ||||
|                     this.select(selected.context.toolbar); | ||||
|                 } else { | ||||
|                     this.deselect(); | ||||
|                 } | ||||
|  | ||||
|                 if (selected && selected.context.viewProxy) { | ||||
|                     this.proxy(selected.context.viewProxy); | ||||
|                 } | ||||
|             }.bind(this)); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -104,10 +104,10 @@ define( | ||||
|                 mockEditorCapability.isEditContextRoot.andReturn(false); | ||||
|                 mockEditorCapability.dirty.andReturn(false); | ||||
|  | ||||
|                 expect(checkFn()).toBe(false); | ||||
|                 expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes."); | ||||
|  | ||||
|                 mockEditorCapability.isEditContextRoot.andReturn(true); | ||||
|                 expect(checkFn()).toBe(false); | ||||
|                 expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes."); | ||||
|  | ||||
|                 mockEditorCapability.dirty.andReturn(true); | ||||
|                 expect(checkFn()) | ||||
|   | ||||
| @@ -27,11 +27,47 @@ define( | ||||
|  | ||||
|         describe("The Elements Pane controller", function () { | ||||
|             var mockScope, | ||||
|                 mockOpenMCT, | ||||
|                 mockSelection, | ||||
|                 mockDomainObject, | ||||
|                 mockMutationCapability, | ||||
|                 mockUnlisten, | ||||
|                 selectable = [], | ||||
|                 controller; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockScope = jasmine.createSpy("$scope"); | ||||
|                 controller = new ElementsController(mockScope); | ||||
|                 mockUnlisten = jasmine.createSpy('unlisten'); | ||||
|                 mockMutationCapability = jasmine.createSpyObj("mutationCapability", [ | ||||
|                     "listen" | ||||
|                 ]); | ||||
|                 mockMutationCapability.listen.andReturn(mockUnlisten); | ||||
|                 mockDomainObject = jasmine.createSpyObj("domainObject", [ | ||||
|                     "getCapability", | ||||
|                     "useCapability" | ||||
|                 ]); | ||||
|                 mockDomainObject.useCapability.andCallThrough(); | ||||
|                 mockDomainObject.getCapability.andReturn(mockMutationCapability); | ||||
|  | ||||
|                 mockScope = jasmine.createSpyObj("$scope", ['$on']); | ||||
|                 mockSelection = jasmine.createSpyObj("selection", [ | ||||
|                     'on', | ||||
|                     'off', | ||||
|                     'get' | ||||
|                 ]); | ||||
|                 mockSelection.get.andReturn([]); | ||||
|                 mockOpenMCT = { | ||||
|                     selection: mockSelection | ||||
|                 }; | ||||
|  | ||||
|                 selectable[0] = { | ||||
|                     context: { | ||||
|                         oldItem: mockDomainObject | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 spyOn(ElementsController.prototype, 'refreshComposition'); | ||||
|  | ||||
|                 controller = new ElementsController(mockScope, mockOpenMCT); | ||||
|             }); | ||||
|  | ||||
|             function getModel(model) { | ||||
| @@ -63,6 +99,44 @@ define( | ||||
|                 expect(objects.filter(mockScope.searchElements).length).toBe(4); | ||||
|             }); | ||||
|  | ||||
|             it("refreshes composition on selection", function () { | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(ElementsController.prototype.refreshComposition).toHaveBeenCalledWith(mockDomainObject); | ||||
|             }); | ||||
|  | ||||
|             it("listens on mutation and refreshes composition", function () { | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(mockDomainObject.getCapability).toHaveBeenCalledWith('mutation'); | ||||
|                 expect(mockMutationCapability.listen).toHaveBeenCalled(); | ||||
|                 expect(ElementsController.prototype.refreshComposition.calls.length).toBe(1); | ||||
|  | ||||
|                 mockMutationCapability.listen.mostRecentCall.args[0](mockDomainObject); | ||||
|  | ||||
|                 expect(ElementsController.prototype.refreshComposition.calls.length).toBe(2); | ||||
|             }); | ||||
|  | ||||
|             it("cleans up mutation listener when selection changes", function () { | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(mockMutationCapability.listen).toHaveBeenCalled(); | ||||
|  | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(mockUnlisten).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("does not listen on mutation for element proxy selectable", function () { | ||||
|                 selectable[0] = { | ||||
|                     context: { | ||||
|                         elementProxy: {} | ||||
|                     } | ||||
|                 }; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(mockDomainObject.getCapability).not.toHaveBeenCalledWith('mutation'); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -29,7 +29,9 @@ define( | ||||
|                 mockElement, | ||||
|                 testAttrs, | ||||
|                 mockUnwatch, | ||||
|                 representer; | ||||
|                 representer, | ||||
|                 mockOpenMCT, | ||||
|                 mockSelection; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockScope = jasmine.createSpyObj( | ||||
| @@ -46,7 +48,18 @@ define( | ||||
|  | ||||
|                 mockScope.$parent.$watchCollection.andReturn(mockUnwatch); | ||||
|  | ||||
|                 mockSelection = jasmine.createSpyObj("selection", [ | ||||
|                     'on', | ||||
|                     'off', | ||||
|                     'get' | ||||
|                 ]); | ||||
|                 mockSelection.get.andReturn([]); | ||||
|                 mockOpenMCT = { | ||||
|                     selection: mockSelection | ||||
|                 }; | ||||
|  | ||||
|                 representer = new EditToolbarRepresenter( | ||||
|                     mockOpenMCT, | ||||
|                     mockScope, | ||||
|                     mockElement, | ||||
|                     testAttrs | ||||
|   | ||||
| @@ -28,13 +28,25 @@ define( | ||||
|             var testProxy, | ||||
|                 testElement, | ||||
|                 otherElement, | ||||
|                 selection; | ||||
|                 selection, | ||||
|                 mockSelection, | ||||
|                 mockOpenMCT; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testProxy = { someKey: "some value" }; | ||||
|                 testElement = { someOtherKey: "some other value" }; | ||||
|                 otherElement = { yetAnotherKey: 42 }; | ||||
|                 selection = new EditToolbarSelection(); | ||||
|                 mockSelection = jasmine.createSpyObj("selection", [ | ||||
|                     // 'select', | ||||
|                     'on', | ||||
|                     'off', | ||||
|                     'get' | ||||
|                 ]); | ||||
|                 mockSelection.get.andReturn([]); | ||||
|                 mockOpenMCT = { | ||||
|                     selection: mockSelection | ||||
|                 }; | ||||
|                 selection = new EditToolbarSelection(mockOpenMCT); | ||||
|                 selection.proxy(testProxy); | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -121,6 +121,9 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     UTCTimeFormat.prototype.parse = function (text) { | ||||
|         if (typeof text === 'number') { | ||||
|             return text; | ||||
|         } | ||||
|         return moment.utc(text, DATE_FORMATS).valueOf(); | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -41,6 +41,7 @@ define([ | ||||
|     "./src/controllers/BannerController", | ||||
|     "./src/directives/MCTContainer", | ||||
|     "./src/directives/MCTDrag", | ||||
|     "./src/directives/MCTSelectable", | ||||
|     "./src/directives/MCTClickElsewhere", | ||||
|     "./src/directives/MCTResize", | ||||
|     "./src/directives/MCTPopup", | ||||
| @@ -90,6 +91,7 @@ define([ | ||||
|     BannerController, | ||||
|     MCTContainer, | ||||
|     MCTDrag, | ||||
|     MCTSelectable, | ||||
|     MCTClickElsewhere, | ||||
|     MCTResize, | ||||
|     MCTPopup, | ||||
| @@ -328,6 +330,13 @@ define([ | ||||
|                         "$document" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "mctSelectable", | ||||
|                     "implementation": MCTSelectable, | ||||
|                     "depends": [ | ||||
|                         "openmct" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     "key": "mctClickElsewhere", | ||||
|                     "implementation": MCTClickElsewhere, | ||||
|   | ||||
| @@ -25,6 +25,7 @@ | ||||
| 	} | ||||
|  | ||||
| 	.l-fixed-position-item { | ||||
|         border-width: 1px; | ||||
| 		position: absolute; | ||||
| 		&.s-not-selected { | ||||
| 			opacity: 0.8; | ||||
|   | ||||
| @@ -80,23 +80,32 @@ | ||||
|  | ||||
|     // Editing Grids | ||||
|     .l-grid-holder { | ||||
|         display: block; | ||||
|         .l-grid { | ||||
|             &.l-grid-x { @include bgTicks($colorGridLines, 'x'); } | ||||
|             &.l-grid-y { @include bgTicks($colorGridLines, 'y'); } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Prevent nested frames from showing their grids | ||||
|     .t-frame-outer .l-grid-holder { display: none !important; } | ||||
|  | ||||
|     // Prevent nested elements from showing s-hover-border | ||||
|     .t-frame-outer .s-hover-border { | ||||
|         border: none !important; | ||||
|     // Display grid when selected or selection parent. | ||||
|     .s-selected .l-grid-holder, | ||||
|     .s-selected-parent .l-grid-holder { | ||||
|         display: block; | ||||
|     } | ||||
|  | ||||
|     // Prevent nested frames from being selectable until we have proper sub-object editing | ||||
|     .t-frame-outer .t-frame-outer { | ||||
|         pointer-events: none; | ||||
|     // Display in nested frames... | ||||
|     .t-frame-outer { | ||||
|         // ...when drilled in or selection parent... | ||||
|         &.s-drilled-in, &.s-selected-parent { | ||||
|             .l-grid-holder { | ||||
|                 display: block; | ||||
|             } | ||||
|             .t-frame-outer:not(.s-drilled-in) .l-grid-holder { | ||||
|                 display: none; | ||||
|             } | ||||
|         } | ||||
|         // ...but hide otherwise. | ||||
|         .l-grid-holder { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,12 +26,10 @@ | ||||
|         z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above | ||||
|         &:not(.no-frame) { | ||||
|             background: $colorBodyBg; | ||||
|             border: 1px solid $bc; | ||||
|             &:hover { | ||||
|                 border-color: lighten($bc, 10%); | ||||
|             } | ||||
|             border-color: $bc; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .object-browse-bar { | ||||
|         font-size: 0.75em; | ||||
|         height: $ohH; | ||||
| @@ -92,9 +90,9 @@ | ||||
|  | ||||
|     &.no-frame { | ||||
|         background: transparent !important; | ||||
|         border: none !important; | ||||
|         border-color: transparent; | ||||
|         .object-browse-bar .right { | ||||
|             $m: 0; // $interiorMarginSm; | ||||
|             $m: 0; | ||||
|             background: rgba(black, 0.3); | ||||
|             border-radius: $basicCr; | ||||
|             padding: $interiorMarginSm; | ||||
| @@ -104,7 +102,7 @@ | ||||
|         } | ||||
|         &.t-frame-outer > .t-rep-frame { | ||||
|             &.contents { | ||||
|                 $m: 2px; | ||||
|                 $m: 0px; | ||||
|                 top: $m; | ||||
|                 right: $m; | ||||
|                 bottom: $m; | ||||
| @@ -115,6 +113,7 @@ | ||||
|                     display: none; | ||||
|                 } | ||||
|                 > .object-holder.abs { | ||||
|                     overflow: hidden; | ||||
|                     top: 0 !important; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -20,35 +20,52 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| .s-hover-border { | ||||
|     border: 1px dotted transparent; | ||||
|     border: 1px solid transparent; | ||||
|     &:hover { | ||||
|         border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .s-status-editing { | ||||
|     // Limit to editing mode until we have sub-object selection | ||||
|     // Limit to editing mode | ||||
|     $o: 0.5; | ||||
|     $oHover: 0.8; | ||||
|     $bc: $colorSelectableSelectedPrimary; | ||||
|     .s-hover-border { | ||||
|         // Show a border by default so user can see object bounds and empty objects | ||||
|         border: 1px dotted rgba($colorSelectableSelectedPrimary, 0.3) !important; | ||||
|         border-color: rgba($bc, $o) !important; | ||||
|         border-style: dotted !important; | ||||
|  | ||||
|         &:hover { | ||||
|             border-color: rgba($colorSelectableSelectedPrimary, 0.7) !important; | ||||
|             border-color: rgba($bc, $oHover) !important; | ||||
|         } | ||||
|  | ||||
|         &.t-object-type-layout { | ||||
|             border-style: dashed !important; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .s-selected > .s-hover-border, | ||||
|     .s-selected.s-hover-border { | ||||
|         // Styles for a selected object. Also used by legacy Fixed Position/Panel objects. | ||||
|         border-color: $colorSelectableSelectedPrimary !important; | ||||
|         @include boxShdwLarge(); | ||||
|         // Show edit-corners if you got 'em | ||||
|         .edit-corner { | ||||
|             display: block; | ||||
|             &:hover { | ||||
|                 background-color: rgba($colorKey, 1); | ||||
|     .s-selected { | ||||
|         &.s-moveable { | ||||
|             &:not(.s-drilled-in) { | ||||
|                 cursor: move; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|     .s-selected > .s-moveable, | ||||
|     .s-selected.s-moveable { | ||||
|         cursor: move; | ||||
| .s-selected > .s-hover-border, | ||||
| .s-selected.s-hover-border { | ||||
|     // Styles for a selected object. Also used by legacy Fixed Position/Panel objects. | ||||
|     border-color: $colorSelectableSelectedPrimary !important; | ||||
|     @include boxShdwLarge(); | ||||
|     // Show edit-corners if you got 'em | ||||
|     .edit-corner { | ||||
|         display: block; | ||||
|         &:hover { | ||||
|             background-color: rgba($colorKey, 1); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <span ng-controller="DateTimeFieldController"> | ||||
|     <input type="text" | ||||
|     <input type="text" autocorrect="off" spellcheck="false" | ||||
|            ng-model="textValue" | ||||
|            ng-blur="restoreTextValue(); ngBlur()" | ||||
|            ng-mouseup="ngMouseup()" | ||||
|   | ||||
| @@ -40,7 +40,7 @@ define( | ||||
|  | ||||
|             // Gets an array of the contextual parents/ancestors of the selected object | ||||
|             function getContextualPath() { | ||||
|                 var currentObj = $scope.ngModel.selectedObject, | ||||
|                 var currentObj = $scope.domainObject, | ||||
|                     currentParent, | ||||
|                     parents = []; | ||||
|  | ||||
| @@ -68,7 +68,7 @@ define( | ||||
|  | ||||
|                 // If this the the initial call of this recursive function | ||||
|                 if (!current) { | ||||
|                     current = $scope.ngModel.selectedObject; | ||||
|                     current = $scope.domainObject; | ||||
|                     $scope.primaryParents = []; | ||||
|                 } | ||||
|  | ||||
| @@ -87,16 +87,16 @@ define( | ||||
|  | ||||
|             // Gets the metadata for the selected object | ||||
|             function getMetadata() { | ||||
|                 $scope.metadata = $scope.ngModel.selectedObject && | ||||
|                     $scope.ngModel.selectedObject.hasCapability('metadata') && | ||||
|                     $scope.ngModel.selectedObject.useCapability('metadata'); | ||||
|                 $scope.metadata = $scope.domainObject && | ||||
|                     $scope.domainObject.hasCapability('metadata') && | ||||
|                     $scope.domainObject.useCapability('metadata'); | ||||
|             } | ||||
|  | ||||
|             // Set scope variables when the selected object changes | ||||
|             $scope.$watch('ngModel.selectedObject', function () { | ||||
|                 $scope.isLink = $scope.ngModel.selectedObject && | ||||
|                     $scope.ngModel.selectedObject.hasCapability('location') && | ||||
|                     $scope.ngModel.selectedObject.getCapability('location').isLink(); | ||||
|             $scope.$watch('domainObject', function () { | ||||
|                 $scope.isLink = $scope.domainObject && | ||||
|                     $scope.domainObject.hasCapability('location') && | ||||
|                     $scope.domainObject.getCapability('location').isLink(); | ||||
|  | ||||
|                 if ($scope.isLink) { | ||||
|                     getPrimaryPath(); | ||||
| @@ -109,7 +109,7 @@ define( | ||||
|                 getMetadata(); | ||||
|             }); | ||||
|  | ||||
|             var mutation = $scope.ngModel.selectedObject.getCapability('mutation'); | ||||
|             var mutation = $scope.domainObject.getCapability('mutation'); | ||||
|             var unlisten = mutation.listen(getMetadata); | ||||
|             $scope.$on('$destroy', unlisten); | ||||
|         } | ||||
|   | ||||
							
								
								
									
										60
									
								
								platform/commonUI/general/src/directives/MCTSelectable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								platform/commonUI/general/src/directives/MCTSelectable.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         /** | ||||
|          * The mct-selectable directive allows selection functionality | ||||
|          * (click) to be attached to specific elements. | ||||
|          * | ||||
|          * @memberof platform/commonUI/general | ||||
|          * @constructor | ||||
|          */ | ||||
|         function MCTSelectable(openmct) { | ||||
|  | ||||
|             // Link; install event handlers. | ||||
|             function link(scope, element, attrs) { | ||||
|                 var removeSelectable = openmct.selection.selectable( | ||||
|                     element[0], | ||||
|                     scope.$eval(attrs.mctSelectable), | ||||
|                     attrs.hasOwnProperty('mctInitSelect') && scope.$eval(attrs.mctInitSelect) !== false | ||||
|                 ); | ||||
|  | ||||
|                 scope.$on("$destroy", function () { | ||||
|                     removeSelectable(); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 // mct-selectable only makes sense as an attribute | ||||
|                 restrict: "A", | ||||
|                 // Link function, to install event handlers | ||||
|                 link: link | ||||
|             }; | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return MCTSelectable; | ||||
|     } | ||||
| ); | ||||
| @@ -41,16 +41,6 @@ define( | ||||
|                     "$scope", | ||||
|                     ["$watch", "$on"] | ||||
|                 ); | ||||
|                 mockScope.ngModel = {}; | ||||
|                 mockScope.ngModel.selectedObject = { | ||||
|                     getCapability: function () { | ||||
|                         return { | ||||
|                             listen: function () { | ||||
|                                 return true; | ||||
|                             } | ||||
|                         }; | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 mockObjectService = jasmine.createSpyObj( | ||||
|                     "objectService", | ||||
| @@ -77,22 +67,27 @@ define( | ||||
|                     "location capability", | ||||
|                     ["isLink"] | ||||
|                 ); | ||||
|  | ||||
|                 mockDomainObject.getCapability.andCallFake(function (param) { | ||||
|                     if (param === 'location') { | ||||
|                         return mockLocationCapability; | ||||
|                     } else if (param === 'context') { | ||||
|                         return mockContextCapability; | ||||
|                     } else if (param === 'mutation') { | ||||
|                         return { | ||||
|                             listen: function () { | ||||
|                                 return true; | ||||
|                             } | ||||
|                         }; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|                 controller = new ObjectInspectorController(mockScope, mockObjectService); | ||||
|  | ||||
|                 // Change the selected object to trigger the watch call | ||||
|                 mockScope.ngModel.selectedObject = mockDomainObject; | ||||
|             }); | ||||
|  | ||||
|             it("watches for changes to the selected object", function () { | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith('ngModel.selectedObject', jasmine.any(Function)); | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("looks for contextual parent objects", function () { | ||||
|   | ||||
| @@ -65,6 +65,10 @@ define( | ||||
|             options = Object.create(OPTIONS); | ||||
|             options.marginX = -bubbleSpaceLR; | ||||
|  | ||||
|             // prevent bubble from appearing right under pointer, | ||||
|             // which causes hover callback to be called multiple times | ||||
|             options.offsetX = 1; | ||||
|  | ||||
|             // On a phone, bubble takes up more screen real estate, | ||||
|             // so position it differently (toward the bottom) | ||||
|             if (this.agentService.isPhone()) { | ||||
|   | ||||
| @@ -38,7 +38,8 @@ define([ | ||||
|                     "implementation": InspectorController, | ||||
|                     "depends": [ | ||||
|                         "$scope", | ||||
|                         "policyService" | ||||
|                         "openmct", | ||||
|                         "$document" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|   | ||||
| @@ -21,44 +21,73 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['../../browse/src/InspectorRegion'], | ||||
|     function (InspectorRegion) { | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         /** | ||||
|          * The InspectorController adds region data for a domain object's type | ||||
|          * to the scope. | ||||
|          * The InspectorController listens for the selection changes and adds the selection | ||||
|          * object to the scope. | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function InspectorController($scope, policyService) { | ||||
|             var domainObject = $scope.domainObject, | ||||
|                 typeCapability = domainObject.getCapability('type'), | ||||
|                 statusListener; | ||||
|         function InspectorController($scope, openmct, $document) { | ||||
|             var self = this; | ||||
|             self.$scope = $scope; | ||||
|  | ||||
|             /** | ||||
|              * Filters region parts to only those allowed by region policies | ||||
|              * @param regions | ||||
|              * @returns {{}} | ||||
|              * Callback handler for the selection change event. | ||||
|              * Adds the selection object to the scope. If the selected item has an inspector view, | ||||
|              * it puts the key in the scope. If provider view exists, it shows the view. | ||||
|              */ | ||||
|             function filterRegions(inspector) { | ||||
|                 //Dupe so we're not modifying the type definition. | ||||
|                 return inspector.regions && inspector.regions.filter(function (region) { | ||||
|                     return policyService.allow('region', region, domainObject); | ||||
|                 }); | ||||
|             function setSelection(selection) { | ||||
|                 if (selection[0]) { | ||||
|                     var view = openmct.inspectorViews.get(selection); | ||||
|                     var container = $document[0].querySelectorAll('.inspector-provider-view')[0]; | ||||
|                     container.innerHTML = ""; | ||||
|  | ||||
|                     if (view) { | ||||
|                         self.providerView = true; | ||||
|                         view.show(container); | ||||
|                     } else { | ||||
|                         self.providerView = false; | ||||
|                         var selectedItem = selection[0].context.oldItem; | ||||
|  | ||||
|                         if (selectedItem) { | ||||
|                             $scope.inspectorKey = selectedItem.getCapability("type").typeDef.inspector; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 self.$scope.selection = selection; | ||||
|             } | ||||
|  | ||||
|             function setRegions() { | ||||
|                 $scope.regions = filterRegions(typeCapability.getDefinition().inspector || new InspectorRegion()); | ||||
|             } | ||||
|             openmct.selection.on("change", setSelection); | ||||
|  | ||||
|             setSelection(openmct.selection.get()); | ||||
|  | ||||
|             statusListener = domainObject.getCapability("status").listen(setRegions); | ||||
|             $scope.$on("$destroy", function () { | ||||
|                 statusListener(); | ||||
|                 openmct.selection.off("change", setSelection); | ||||
|             }); | ||||
|  | ||||
|             setRegions(); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Gets the selected item. | ||||
|          * | ||||
|          * @returns a domain object | ||||
|          */ | ||||
|         InspectorController.prototype.selectedItem = function () { | ||||
|             return this.$scope.selection[0].context.oldItem; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Checks if a provider view exists. | ||||
|          * | ||||
|          * @returns 'true' if provider view exists, 'false' otherwise | ||||
|          */ | ||||
|         InspectorController.prototype.hasProviderView = function () { | ||||
|             return this.providerView; | ||||
|         }; | ||||
|  | ||||
|         return InspectorController; | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -27,82 +27,93 @@ define( | ||||
|         describe("The inspector controller ", function () { | ||||
|             var mockScope, | ||||
|                 mockDomainObject, | ||||
|                 mockTypeCapability, | ||||
|                 mockTypeDefinition, | ||||
|                 mockPolicyService, | ||||
|                 mockStatusCapability, | ||||
|                 capabilities = {}, | ||||
|                 controller; | ||||
|                 mockOpenMCT, | ||||
|                 mockSelection, | ||||
|                 mockInspectorViews, | ||||
|                 mockTypeDef, | ||||
|                 controller, | ||||
|                 container, | ||||
|                 $document = [], | ||||
|                 selectable = []; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockTypeDefinition = { | ||||
|                     inspector: | ||||
|                         { | ||||
|                             'regions': [ | ||||
|                                 {'name': 'Part One'}, | ||||
|                                 {'name': 'Part Two'} | ||||
|                             ] | ||||
|                         } | ||||
|                 mockTypeDef = { | ||||
|                     typeDef: { | ||||
|                         inspector: "some-key" | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 mockTypeCapability = jasmine.createSpyObj('typeCapability', [ | ||||
|                     'getDefinition' | ||||
|                 ]); | ||||
|                 mockTypeCapability.getDefinition.andReturn(mockTypeDefinition); | ||||
|                 capabilities.type = mockTypeCapability; | ||||
|  | ||||
|                 mockStatusCapability = jasmine.createSpyObj('statusCapability', [ | ||||
|                     'listen' | ||||
|                 ]); | ||||
|                 capabilities.status = mockStatusCapability; | ||||
|  | ||||
|                 mockDomainObject = jasmine.createSpyObj('domainObject', [ | ||||
|                     'getCapability' | ||||
|                 ]); | ||||
|                 mockDomainObject.getCapability.andCallFake(function (name) { | ||||
|                     return capabilities[name]; | ||||
|                 }); | ||||
|  | ||||
|                 mockPolicyService = jasmine.createSpyObj('policyService', [ | ||||
|                    'allow' | ||||
|                 ]); | ||||
|                 mockDomainObject.getCapability.andReturn(mockTypeDef); | ||||
|  | ||||
|                 mockScope = jasmine.createSpyObj('$scope', | ||||
|                     ['$on'] | ||||
|                     ['$on', 'selection'] | ||||
|                 ); | ||||
|  | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|                 selectable[0] = { | ||||
|                     context: { | ||||
|                         oldItem: mockDomainObject | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 mockSelection = jasmine.createSpyObj("selection", [ | ||||
|                     'on', | ||||
|                     'off', | ||||
|                     'get' | ||||
|                 ]); | ||||
|                 mockSelection.get.andReturn(selectable); | ||||
|  | ||||
|                 mockInspectorViews = jasmine.createSpyObj('inspectorViews', ['get']); | ||||
|                 mockOpenMCT = { | ||||
|                     selection: mockSelection, | ||||
|                     inspectorViews: mockInspectorViews | ||||
|                 }; | ||||
|  | ||||
|                 container = jasmine.createSpy('container', ['innerHTML']); | ||||
|                 $document[0] = jasmine.createSpyObj("$document", ['querySelectorAll']); | ||||
|                 $document[0].querySelectorAll.andReturn([container]); | ||||
|  | ||||
|                 controller = new InspectorController(mockScope, mockOpenMCT, $document); | ||||
|             }); | ||||
|  | ||||
|             it("filters out regions disallowed by region policy", function () { | ||||
|                 mockPolicyService.allow.andReturn(false); | ||||
|                 controller = new InspectorController(mockScope, mockPolicyService); | ||||
|                 expect(mockScope.regions.length).toBe(0); | ||||
|             it("listens for selection change event", function () { | ||||
|                 expect(mockOpenMCT.selection.on).toHaveBeenCalledWith( | ||||
|                     'change', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|  | ||||
|                 expect(controller.selectedItem()).toEqual(mockDomainObject); | ||||
|  | ||||
|                 var mockItem = jasmine.createSpyObj('domainObject', [ | ||||
|                     'getCapability' | ||||
|                 ]); | ||||
|                 mockItem.getCapability.andReturn(mockTypeDef); | ||||
|                 selectable[0].context.oldItem = mockItem; | ||||
|  | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(controller.selectedItem()).toEqual(mockItem); | ||||
|             }); | ||||
|  | ||||
|             it("does not filter out regions allowed by region policy", function () { | ||||
|                 mockPolicyService.allow.andReturn(true); | ||||
|                 controller = new InspectorController(mockScope, mockPolicyService); | ||||
|                 expect(mockScope.regions.length).toBe(2); | ||||
|             it("cleans up on scope destroy", function () { | ||||
|                 expect(mockScope.$on).toHaveBeenCalledWith( | ||||
|                     '$destroy', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|  | ||||
|                 mockScope.$on.calls[0].args[1](); | ||||
|  | ||||
|                 expect(mockOpenMCT.selection.off).toHaveBeenCalledWith( | ||||
|                     'change', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("Responds to status changes", function () { | ||||
|                 mockPolicyService.allow.andReturn(true); | ||||
|                 controller = new InspectorController(mockScope, mockPolicyService); | ||||
|                 expect(mockScope.regions.length).toBe(2); | ||||
|                 expect(mockStatusCapability.listen).toHaveBeenCalled(); | ||||
|                 mockPolicyService.allow.andReturn(false); | ||||
|                 mockStatusCapability.listen.mostRecentCall.args[0](); | ||||
|                 expect(mockScope.regions.length).toBe(0); | ||||
|             }); | ||||
|  | ||||
|             it("Unregisters status listener", function () { | ||||
|                 var mockListener = jasmine.createSpy('listener'); | ||||
|                 mockStatusCapability.listen.andReturn(mockListener); | ||||
|                 controller = new InspectorController(mockScope, mockPolicyService); | ||||
|                 expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function)); | ||||
|                 mockScope.$on.mostRecentCall.args[1](); | ||||
|                 expect(mockListener).toHaveBeenCalled(); | ||||
|             it("adds selection object to scope", function () { | ||||
|                 expect(mockScope.selection).toEqual(selectable); | ||||
|                 expect(controller.selectedItem()).toEqual(mockDomainObject); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -1,54 +0,0 @@ | ||||
| define([ | ||||
|     'text!./res/templates/autoflow-tabular.html', | ||||
|     './src/AutoflowTabularController', | ||||
|     './src/MCTAutoflowTable' | ||||
| ], function ( | ||||
|     autoflowTabularTemplate, | ||||
|     AutoflowTabularController, | ||||
|     MCTAutoflowTable | ||||
| ) { | ||||
|     return function (options) { | ||||
|         return function (openmct) { | ||||
|             openmct.legacyRegistry.register("platform/features/autoflow", { | ||||
|                 "name": "WARP Telemetry Adapter", | ||||
|                 "description": "Retrieves telemetry from the WARP Server and provides related types and views.", | ||||
|                 "resources": "res", | ||||
|                 "extensions": { | ||||
|                     "views": [ | ||||
|                         { | ||||
|                             "key": "autoflow", | ||||
|                             "name": "Autoflow Tabular", | ||||
|                             "cssClass": "icon-packet", | ||||
|                             "description": "A tabular view of packet contents.", | ||||
|                             "template": autoflowTabularTemplate, | ||||
|                             "type": options && options.type, | ||||
|                             "needs": [ | ||||
|                                 "telemetry" | ||||
|                             ], | ||||
|                             "delegation": true | ||||
|                         } | ||||
|                     ], | ||||
|                     "controllers": [ | ||||
|                         { | ||||
|                             "key": "AutoflowTabularController", | ||||
|                             "implementation": AutoflowTabularController, | ||||
|                             "depends": [ | ||||
|                                 "$scope", | ||||
|                                 "$timeout", | ||||
|                                 "telemetrySubscriber" | ||||
|                             ] | ||||
|                         } | ||||
|                     ], | ||||
|                     "directives": [ | ||||
|                         { | ||||
|                             "key": "mctAutoflowTable", | ||||
|                             "implementation": MCTAutoflowTable | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             }); | ||||
|             openmct.legacyRegistry.enable("platform/features/autoflow"); | ||||
|         }; | ||||
|     }; | ||||
| }); | ||||
|  | ||||
| @@ -1,26 +0,0 @@ | ||||
| <div class="items-holder abs contents autoflow obj-value-format" | ||||
|      ng-controller="AutoflowTabularController as autoflow"> | ||||
|     <div class="abs l-flex-row holder t-autoflow-header l-autoflow-header"> | ||||
|         <mct-include key="'input-filter'" | ||||
|                      ng-model="autoflow.filter" | ||||
|                      class="flex-elem"> | ||||
|         </mct-include> | ||||
|         <div class="flex-elem grows t-last-update" title="Last Update">{{autoflow.updated()}}</div> | ||||
|         <a title="Change column width" | ||||
|             class="s-button flex-elem icon-arrows-right-left change-column-width" | ||||
|             ng-click="autoflow.increaseColumnWidth()"></a> | ||||
|     </div> | ||||
|     <div class="abs t-autoflow-items l-autoflow-items" | ||||
|          mct-resize="autoflow.setBounds(bounds)" | ||||
|          mct-resize-interval="50"> | ||||
|         <mct-autoflow-table values="autoflow.rangeValues()" | ||||
|                             objects="autoflow.getTelemetryObjects()" | ||||
|                             rows="autoflow.getRows()" | ||||
|                             classes="autoflow.classes()" | ||||
|                             updated="autoflow.updated()" | ||||
|                             column-width="autoflow.columnWidth()" | ||||
|                             counter="autoflow.counter()" | ||||
|                             > | ||||
|         </mct-autoflow-table> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -1,169 +0,0 @@ | ||||
| /*global angular*/ | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|  | ||||
|         /** | ||||
|          * The link step for the `mct-autoflow-table` directive; | ||||
|          * watches scope and updates the DOM appropriately. | ||||
|          * See documentation in `MCTAutoflowTable.js` for the rationale | ||||
|          * for including this directive, as well as for an explanation | ||||
|          * of which values are placed in scope. | ||||
|          * | ||||
|          * @constructor | ||||
|          * @param {Scope} scope the scope for this usage of the directive | ||||
|          * @param element the jqLite-wrapped element which used this directive | ||||
|          */ | ||||
|         function AutoflowTableLinker(scope, element) { | ||||
|             var objects, // Domain objects at last structure refresh | ||||
|                 rows, // Number of rows from last structure refresh | ||||
|                 priorClasses = {}, | ||||
|                 valueSpans = {}; // Span elements to put data values in | ||||
|  | ||||
|             // Create a new name-value pair in the specified column | ||||
|             function createListItem(domainObject, ul) { | ||||
|                 // Create a new li, and spans to go in it. | ||||
|                 var li = angular.element('<li>'), | ||||
|                     titleSpan = angular.element('<span>'), | ||||
|                     valueSpan = angular.element('<span>'); | ||||
|  | ||||
|                 // Place spans in the li, and li into the column. | ||||
|                 // valueSpan must precede titleSpan in the DOM due to new CSS float approach | ||||
|                 li.append(valueSpan).append(titleSpan); | ||||
|                 ul.append(li); | ||||
|  | ||||
|                 // Style appropriately | ||||
|                 li.addClass('l-autoflow-row'); | ||||
|                 titleSpan.addClass('l-autoflow-item l'); | ||||
|                 valueSpan.addClass('l-autoflow-item r l-obj-val-format'); | ||||
|  | ||||
|                 // Set text/tooltip for the name-value row | ||||
|                 titleSpan.text(domainObject.getModel().name); | ||||
|                 titleSpan.attr("title", domainObject.getModel().name); | ||||
|  | ||||
|                 // Keep a reference to the span which will hold the | ||||
|                 // data value, to populate in the next refreshValues call | ||||
|                 valueSpans[domainObject.getId()] = valueSpan; | ||||
|  | ||||
|                 return li; | ||||
|             } | ||||
|  | ||||
|             // Create a new column of name-value pairs in this table. | ||||
|             function createColumn(el) { | ||||
|                 // Create a ul | ||||
|                 var ul = angular.element('<ul>'); | ||||
|  | ||||
|                 // Add it into the mct-autoflow-table | ||||
|                 el.append(ul); | ||||
|  | ||||
|                 // Style appropriately | ||||
|                 ul.addClass('l-autoflow-col'); | ||||
|  | ||||
|                 // Get the current col width and apply at time of column creation | ||||
|                 // Important to do this here, as new columns could be created after | ||||
|                 // the user has changed the width. | ||||
|                 ul.css('width', scope.columnWidth + 'px'); | ||||
|  | ||||
|                 // Return it, so some li elements can be added | ||||
|                 return ul; | ||||
|             } | ||||
|  | ||||
|             // Change the width of the columns when user clicks the resize button. | ||||
|             function resizeColumn() { | ||||
|                 element.find('ul').css('width', scope.columnWidth + 'px'); | ||||
|             } | ||||
|  | ||||
|             // Rebuild the DOM associated with this table. | ||||
|             function rebuild(domainObjects, rowCount) { | ||||
|                 var activeColumn; | ||||
|  | ||||
|                 // Empty out our cached span elements | ||||
|                 valueSpans = {}; | ||||
|  | ||||
|                 // Start with an empty DOM beneath this directive | ||||
|                 element.html(""); | ||||
|  | ||||
|                 // Add DOM elements for each domain object being displayed | ||||
|                 // in this table. | ||||
|                 domainObjects.forEach(function (object, index) { | ||||
|                     // Start a new column if we'd run out of room | ||||
|                     if (index % rowCount === 0) { | ||||
|                         activeColumn = createColumn(element); | ||||
|                     } | ||||
|                     // Add the DOM elements for that object to whichever | ||||
|                     // column (a `ul` element) is current. | ||||
|                     createListItem(object, activeColumn); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // Update spans with values, as made available via the | ||||
|             // `values` attribute of this directive. | ||||
|             function refreshValues() { | ||||
|                 // Get the available values | ||||
|                 var values = scope.values || {}, | ||||
|                     classes = scope.classes || {}; | ||||
|  | ||||
|                 // Populate all spans with those values (or clear | ||||
|                 // those spans if no value is available) | ||||
|                 (objects || []).forEach(function (object) { | ||||
|                     var id = object.getId(), | ||||
|                         span = valueSpans[id], | ||||
|                         value; | ||||
|  | ||||
|                     if (span) { | ||||
|                         // Look up the value... | ||||
|                         value = values[id]; | ||||
|                         // ...and convert to empty string if it's undefined | ||||
|                         value = value === undefined ? "" : value; | ||||
|                         span.attr("data-value", value); | ||||
|  | ||||
|                         // Update the span | ||||
|                         span.text(value); | ||||
|                         span.attr("title", value); | ||||
|                         span.removeClass(priorClasses[id]); | ||||
|                         span.addClass(classes[id]); | ||||
|                         priorClasses[id] = classes[id]; | ||||
|                     } | ||||
|                     // Also need stale/alert/ok class | ||||
|                     // on span | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // Refresh the DOM for this table, if necessary | ||||
|             function refreshStructure() { | ||||
|                 // Only rebuild if number of rows or set of objects | ||||
|                 // has changed; otherwise, our structure is still valid. | ||||
|                 if (scope.objects !== objects || | ||||
|                         scope.rows !== rows) { | ||||
|  | ||||
|                     // Track those values to support future refresh checks | ||||
|                     objects = scope.objects; | ||||
|                     rows = scope.rows; | ||||
|  | ||||
|                     // Rebuild the DOM | ||||
|                     rebuild(objects || [], rows || 1); | ||||
|  | ||||
|                     // Refresh all data values shown | ||||
|                     refreshValues(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Changing the domain objects in use or the number | ||||
|             // of rows should trigger a structure change (DOM rebuild) | ||||
|             scope.$watch("objects", refreshStructure); | ||||
|             scope.$watch("rows", refreshStructure); | ||||
|  | ||||
|             // When the current column width has been changed, resize the column | ||||
|             scope.$watch('columnWidth', resizeColumn); | ||||
|  | ||||
|             // When the last-updated time ticks, | ||||
|             scope.$watch("updated", refreshValues); | ||||
|  | ||||
|             // Update displayed values when the counter changes. | ||||
|             scope.$watch("counter", refreshValues); | ||||
|  | ||||
|         } | ||||
|  | ||||
|         return AutoflowTableLinker; | ||||
|     } | ||||
| ); | ||||
| @@ -1,324 +0,0 @@ | ||||
|  | ||||
| define( | ||||
|     ['moment'], | ||||
|     function (moment) { | ||||
|  | ||||
|         var ROW_HEIGHT = 16, | ||||
|             SLIDER_HEIGHT = 10, | ||||
|             INITIAL_COLUMN_WIDTH = 225, | ||||
|             MAX_COLUMN_WIDTH = 525, | ||||
|             COLUMN_WIDTH_STEP = 25, | ||||
|             DEBOUNCE_INTERVAL = 100, | ||||
|             DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z", | ||||
|             NOT_UPDATED = "No updates", | ||||
|             EMPTY_ARRAY = []; | ||||
|  | ||||
|         /** | ||||
|          * Responsible for supporting the autoflow tabular view. | ||||
|          * Implements the all-over logic which drives that view, | ||||
|          * mediating between template-provided areas, the included | ||||
|          * `mct-autoflow-table` directive, and the underlying | ||||
|          * domain object model. | ||||
|          * @constructor | ||||
|          */ | ||||
|         function AutflowTabularController( | ||||
|             $scope, | ||||
|             $timeout, | ||||
|             telemetrySubscriber | ||||
|         ) { | ||||
|             var filterValue = "", | ||||
|                 filterValueLowercase = "", | ||||
|                 subscription, | ||||
|                 filteredObjects = [], | ||||
|                 lastUpdated = {}, | ||||
|                 updateText = NOT_UPDATED, | ||||
|                 rangeValues = {}, | ||||
|                 classes = {}, | ||||
|                 limits = {}, | ||||
|                 updatePending = false, | ||||
|                 lastBounce = Number.NEGATIVE_INFINITY, | ||||
|                 columnWidth = INITIAL_COLUMN_WIDTH, | ||||
|                 rows = 1, | ||||
|                 counter = 0; | ||||
|  | ||||
|             // Trigger an update of the displayed table by incrementing | ||||
|             // the counter that it watches. | ||||
|             function triggerDisplayUpdate() { | ||||
|                 counter += 1; | ||||
|             } | ||||
|  | ||||
|             // Check whether or not an object's name matches the | ||||
|             // user-entered filter value. | ||||
|             function filterObject(domainObject) { | ||||
|                 return (domainObject.getModel().name || "") | ||||
|                     .toLowerCase() | ||||
|                     .indexOf(filterValueLowercase) !== -1; | ||||
|             } | ||||
|  | ||||
|             // Comparator for sorting points back into packet order | ||||
|             function compareObject(objectA, objectB) { | ||||
|                 var indexA = objectA.getModel().index || 0, | ||||
|                     indexB = objectB.getModel().index || 0; | ||||
|                 return indexA - indexB; | ||||
|             } | ||||
|  | ||||
|             // Update the list of currently-displayed objects; these | ||||
|             // will be the subset of currently subscribed-to objects | ||||
|             // which match a user-entered filter. | ||||
|             function doUpdateFilteredObjects() { | ||||
|                 // Generate the list | ||||
|                 filteredObjects = ( | ||||
|                     subscription ? | ||||
|                             subscription.getTelemetryObjects() : | ||||
|                             [] | ||||
|                 ).filter(filterObject).sort(compareObject); | ||||
|  | ||||
|                 // Clear the pending flag | ||||
|                 updatePending = false; | ||||
|  | ||||
|                 // Track when this occurred, so that we can wait | ||||
|                 // a whole before updating again. | ||||
|                 lastBounce = Date.now(); | ||||
|  | ||||
|                 triggerDisplayUpdate(); | ||||
|             } | ||||
|  | ||||
|             // Request an update to the list of current objects; this may | ||||
|             // run on a timeout to avoid excessive calls, e.g. while the user | ||||
|             // is typing a filter. | ||||
|             function updateFilteredObjects() { | ||||
|                 // Don't do anything if an update is already scheduled | ||||
|                 if (!updatePending) { | ||||
|                     if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) { | ||||
|                         // Update immediately if it's been long enough | ||||
|                         doUpdateFilteredObjects(); | ||||
|                     } else { | ||||
|                         // Otherwise, update later, and track that we have | ||||
|                         // an update pending so that subsequent calls can | ||||
|                         // be ignored. | ||||
|                         updatePending = true; | ||||
|                         $timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Track the latest data values for this domain object | ||||
|             function recordData(telemetryObject) { | ||||
|                 // Get latest domain/range values for this object. | ||||
|                 var id = telemetryObject.getId(), | ||||
|                     domainValue = subscription.getDomainValue(telemetryObject), | ||||
|                     rangeValue = subscription.getRangeValue(telemetryObject); | ||||
|  | ||||
|                 // Track the most recent timestamp change observed... | ||||
|                 if (domainValue !== undefined && domainValue !== lastUpdated[id]) { | ||||
|                     lastUpdated[id] = domainValue; | ||||
|                     // ... and update the displayable text for that timestamp | ||||
|                     updateText = isNaN(domainValue) ? "" : | ||||
|                             moment.utc(domainValue).format(DATE_FORMAT); | ||||
|                 } | ||||
|  | ||||
|                 // Store data values into the rangeValues structure, which | ||||
|                 // will be used to populate the table itself. | ||||
|                 // Note that we want full precision here. | ||||
|                 rangeValues[id] = rangeValue; | ||||
|  | ||||
|                 // Update limit states as well | ||||
|                 classes[id] = limits[id] && (limits[id].evaluate({ | ||||
|                     // This relies on external knowledge that the | ||||
|                     // range value of a telemetry point is encoded | ||||
|                     // in its datum as "value." | ||||
|                     value: rangeValue | ||||
|                 }) || {}).cssClass; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             // Look at telemetry objects from the subscription; this is watched | ||||
|             // to detect changes from the subscription. | ||||
|             function subscribedTelemetry() { | ||||
|                 return subscription ? | ||||
|                         subscription.getTelemetryObjects() : EMPTY_ARRAY; | ||||
|             } | ||||
|  | ||||
|             // Update the data values which will be used to populate the table | ||||
|             function updateValues() { | ||||
|                 subscribedTelemetry().forEach(recordData); | ||||
|                 triggerDisplayUpdate(); | ||||
|             } | ||||
|  | ||||
|             // Getter-setter function for user-entered filter text. | ||||
|             function filter(value) { | ||||
|                 // If value was specified, we're a setter | ||||
|                 if (value !== undefined) { | ||||
|                     // Store the new value | ||||
|                     filterValue = value; | ||||
|                     filterValueLowercase = value.toLowerCase(); | ||||
|                     // Change which objects appear in the table | ||||
|                     updateFilteredObjects(); | ||||
|                 } | ||||
|  | ||||
|                 // Always act as a getter | ||||
|                 return filterValue; | ||||
|             } | ||||
|  | ||||
|             // Update the bounds (width and height) of this view; | ||||
|             // called from the mct-resize directive. Recalculates how | ||||
|             // many rows should appear in the contained table. | ||||
|             function setBounds(bounds) { | ||||
|                 var availableSpace = bounds.height - SLIDER_HEIGHT; | ||||
|                 rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT)); | ||||
|             } | ||||
|  | ||||
|             // Increment the current column width, up to the defined maximum. | ||||
|             // When the max is hit, roll back to the default. | ||||
|             function increaseColumnWidth() { | ||||
|                 columnWidth += COLUMN_WIDTH_STEP; | ||||
|                 // Cycle down to the initial width instead of exceeding max | ||||
|                 columnWidth = columnWidth > MAX_COLUMN_WIDTH ? | ||||
|                         INITIAL_COLUMN_WIDTH : columnWidth; | ||||
|             } | ||||
|  | ||||
|             // Get displayable text for last-updated value | ||||
|             function updated() { | ||||
|                 return updateText; | ||||
|             } | ||||
|  | ||||
|             // Unsubscribe, if a subscription is active. | ||||
|             function releaseSubscription() { | ||||
|                 if (subscription) { | ||||
|                     subscription.unsubscribe(); | ||||
|                     subscription = undefined; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Update set of telemetry objects managed by this view | ||||
|             function updateTelemetryObjects(telemetryObjects) { | ||||
|                 updateFilteredObjects(); | ||||
|                 limits = {}; | ||||
|                 telemetryObjects.forEach(function (telemetryObject) { | ||||
|                     var id = telemetryObject.getId(); | ||||
|                     limits[id] = telemetryObject.getCapability('limit'); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // Create a subscription for the represented domain object. | ||||
|             // This will resolve capability delegation as necessary. | ||||
|             function makeSubscription(domainObject) { | ||||
|                 // Unsubscribe, if there is an existing subscription | ||||
|                 releaseSubscription(); | ||||
|  | ||||
|                 // Clear updated timestamp | ||||
|                 lastUpdated = {}; | ||||
|                 updateText = NOT_UPDATED; | ||||
|  | ||||
|                 // Create a new subscription; telemetrySubscriber gets | ||||
|                 // to do the meaningful work here. | ||||
|                 subscription = domainObject && telemetrySubscriber.subscribe( | ||||
|                     domainObject, | ||||
|                     updateValues | ||||
|                 ); | ||||
|  | ||||
|                 // Our set of in-view telemetry objects may have changed, | ||||
|                 // so update the set that is being passed down to the table. | ||||
|                 updateFilteredObjects(); | ||||
|             } | ||||
|  | ||||
|             // Watch for changes to the set of objects which have telemetry | ||||
|             $scope.$watch(subscribedTelemetry, updateTelemetryObjects); | ||||
|  | ||||
|             // Watch for the represented domainObject (this field will | ||||
|             // be populated by mct-representation) | ||||
|             $scope.$watch("domainObject", makeSubscription); | ||||
|  | ||||
|             // Make sure we unsubscribe when this view is destroyed. | ||||
|             $scope.$on("$destroy", releaseSubscription); | ||||
|  | ||||
|             return { | ||||
|                 /** | ||||
|                  * Get the number of rows which should be shown in this table. | ||||
|                  * @return {number} the number of rows to show | ||||
|                  */ | ||||
|                 getRows: function () { | ||||
|                     return rows; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Get the objects which should currently be displayed in | ||||
|                  * this table. This will be watched, so the return value | ||||
|                  * should be stable when this list is unchanging. Only | ||||
|                  * objects which match the user-entered filter value should | ||||
|                  * be returned here. | ||||
|                  * @return {DomainObject[]} the domain objects to include in | ||||
|                  *         this table. | ||||
|                  */ | ||||
|                 getTelemetryObjects: function () { | ||||
|                     return filteredObjects; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Set the bounds (width/height) of this autoflow tabular view. | ||||
|                  * The template must ensure that these bounds are tracked on | ||||
|                  * the table area only. | ||||
|                  * @param bounds the bounds; and object with `width` and | ||||
|                  *        `height` properties, both as numbers, in pixels. | ||||
|                  */ | ||||
|                 setBounds: setBounds, | ||||
|                 /** | ||||
|                  * Increments the width of the autoflow column. | ||||
|                  * Setting does not yet persist. | ||||
|                  */ | ||||
|                 increaseColumnWidth: increaseColumnWidth, | ||||
|                 /** | ||||
|                  * Get-or-set the user-supplied filter value. | ||||
|                  * @param {string} [value] the new filter value; omit to use | ||||
|                  *        as a getter | ||||
|                  * @returns {string} the user-supplied filter value | ||||
|                  */ | ||||
|                 filter: filter, | ||||
|                 /** | ||||
|                  * Get all range values for use in this table. These will be | ||||
|                  * returned as an object of key-value pairs, where keys are | ||||
|                  * domain object IDs, and values are the most recently observed | ||||
|                  * data values associated with those objects, formatted for | ||||
|                  * display. | ||||
|                  * @returns {object.<string,string>} most recent values | ||||
|                  */ | ||||
|                 rangeValues: function () { | ||||
|                     return rangeValues; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Get CSS classes to apply to specific rows, representing limit | ||||
|                  * states and/or stale states. These are returned as key-value | ||||
|                  * pairs where keys are domain object IDs, and values are CSS | ||||
|                  * classes to display for domain objects with those IDs. | ||||
|                  * @returns {object.<string,string>} CSS classes | ||||
|                  */ | ||||
|                 classes: function () { | ||||
|                     return classes; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Get the "last updated" text for this view; this will be | ||||
|                  * the most recent timestamp observed for any telemetry- | ||||
|                  * providing object, formatted for display. | ||||
|                  * @returns {string} the time of the most recent update | ||||
|                  */ | ||||
|                 updated: updated, | ||||
|                 /** | ||||
|                  * Get the current column width, in pixels. | ||||
|                  * @returns {number} column width | ||||
|                  */ | ||||
|                 columnWidth: function () { | ||||
|                     return columnWidth; | ||||
|                 }, | ||||
|                 /** | ||||
|                  * Keep a counter and increment this whenever the display | ||||
|                  * should be updated; this will be watched by the | ||||
|                  * `mct-autoflow-table`. | ||||
|                  * @returns {number} a counter value | ||||
|                  */ | ||||
|                 counter: function () { | ||||
|                     return counter; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return AutflowTabularController; | ||||
|     } | ||||
| ); | ||||
| @@ -1,60 +0,0 @@ | ||||
|  | ||||
| define( | ||||
|     ["./AutoflowTableLinker"], | ||||
|     function (AutoflowTableLinker) { | ||||
|  | ||||
|         /** | ||||
|          * The `mct-autoflow-table` directive specifically supports | ||||
|          * autoflow tabular views; it is not intended for use outside | ||||
|          * of that view. | ||||
|          * | ||||
|          * This directive is responsible for creating the structure | ||||
|          * of the table in this view, and for updating its values. | ||||
|          * While this is achievable using a regular Angular template, | ||||
|          * this is undesirable from the perspective of performance | ||||
|          * due to the number of watches that can be involved for large | ||||
|          * tables. Instead, this directive will maintain a small number | ||||
|          * of watches, rebuilding table structure only when necessary, | ||||
|          * and updating displayed values in the more common case of | ||||
|          * new data arriving. | ||||
|          * | ||||
|          * @constructor | ||||
|          */ | ||||
|         function MCTAutoflowTable() { | ||||
|             return { | ||||
|                 // Only applicable at the element level | ||||
|                 restrict: "E", | ||||
|  | ||||
|                 // The link function; handles DOM update/manipulation | ||||
|                 link: AutoflowTableLinker, | ||||
|  | ||||
|                 // Parameters to pass from attributes into scope | ||||
|                 scope: { | ||||
|                     // Set of domain objects to show in the table | ||||
|                     objects: "=", | ||||
|  | ||||
|                     // Values for those objects, by ID | ||||
|                     values: "=", | ||||
|  | ||||
|                     // CSS classes to show for objects, by ID | ||||
|                     classes: "=", | ||||
|  | ||||
|                     // Number of rows to show before autoflowing | ||||
|                     rows: "=", | ||||
|  | ||||
|                     // Time of last update; watched to refresh values | ||||
|                     updated: "=", | ||||
|  | ||||
|                     // Current width of the autoflow column | ||||
|                     columnWidth: "=", | ||||
|  | ||||
|                     // A counter used to trigger display updates | ||||
|                     counter: "=" | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return MCTAutoflowTable; | ||||
|  | ||||
|     } | ||||
| ); | ||||
| @@ -1,178 +0,0 @@ | ||||
|  | ||||
| define( | ||||
|     ["../src/AutoflowTableLinker"], | ||||
|     function (AutoflowTableLinker) { | ||||
|  | ||||
|         describe("The mct-autoflow-table linker", function () { | ||||
|             var cachedAngular, | ||||
|                 mockAngular, | ||||
|                 mockScope, | ||||
|                 mockElement, | ||||
|                 mockElements, | ||||
|                 linker; | ||||
|  | ||||
|             // Utility function to generate more mock elements | ||||
|             function createMockElement(html) { | ||||
|                 var mockEl = jasmine.createSpyObj( | ||||
|                     "element-" + html, | ||||
|                     [ | ||||
|                         "append", | ||||
|                         "addClass", | ||||
|                         "removeClass", | ||||
|                         "text", | ||||
|                         "attr", | ||||
|                         "html", | ||||
|                         "css", | ||||
|                         "find" | ||||
|                     ] | ||||
|                 ); | ||||
|                 mockEl.testHtml = html; | ||||
|                 mockEl.append.andReturn(mockEl); | ||||
|                 mockElements.push(mockEl); | ||||
|                 return mockEl; | ||||
|             } | ||||
|  | ||||
|             function createMockDomainObject(id) { | ||||
|                 var mockDomainObject = jasmine.createSpyObj( | ||||
|                     "domainObject-" + id, | ||||
|                     ["getId", "getModel"] | ||||
|                 ); | ||||
|                 mockDomainObject.getId.andReturn(id); | ||||
|                 mockDomainObject.getModel.andReturn({name: id.toUpperCase()}); | ||||
|                 return mockDomainObject; | ||||
|             } | ||||
|  | ||||
|             function fireWatch(watchExpression, value) { | ||||
|                 mockScope.$watch.calls.forEach(function (call) { | ||||
|                     if (call.args[0] === watchExpression) { | ||||
|                         call.args[1](value); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // AutoflowTableLinker accesses Angular in the global | ||||
|             // scope, since it is not injectable; we simulate that | ||||
|             // here by adding/removing it to/from the window object. | ||||
|             beforeEach(function () { | ||||
|                 mockElements = []; | ||||
|  | ||||
|                 mockAngular = jasmine.createSpyObj("angular", ["element"]); | ||||
|                 mockScope = jasmine.createSpyObj("scope", ["$watch"]); | ||||
|                 mockElement = createMockElement('<div>'); | ||||
|  | ||||
|                 mockAngular.element.andCallFake(createMockElement); | ||||
|  | ||||
|                 if (window.angular !== undefined) { | ||||
|                     cachedAngular = window.angular; | ||||
|                 } | ||||
|                 window.angular = mockAngular; | ||||
|  | ||||
|                 linker = new AutoflowTableLinker(mockScope, mockElement); | ||||
|             }); | ||||
|  | ||||
|             afterEach(function () { | ||||
|                 if (cachedAngular !== undefined) { | ||||
|                     window.angular = cachedAngular; | ||||
|                 } else { | ||||
|                     delete window.angular; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             it("watches for changes in inputs", function () { | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith( | ||||
|                     "objects", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith( | ||||
|                     "rows", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith( | ||||
|                     "counter", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("changes structure when domain objects change", function () { | ||||
|                 // Set up scope | ||||
|                 mockScope.rows = 4; | ||||
|                 mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f'] | ||||
|                     .map(createMockDomainObject); | ||||
|  | ||||
|                 // Fire an update to the set of objects | ||||
|                 fireWatch("objects"); | ||||
|  | ||||
|                 // Should have rebuilt with two columns of | ||||
|                 // four and two rows each; first, by clearing... | ||||
|                 expect(mockElement.html).toHaveBeenCalledWith(""); | ||||
|  | ||||
|                 // Should have appended two columns... | ||||
|                 expect(mockElement.append.calls.length).toEqual(2); | ||||
|  | ||||
|                 // ...which should have received two and four rows each | ||||
|                 expect(mockElement.append.calls[0].args[0].append.calls.length) | ||||
|                     .toEqual(4); | ||||
|                 expect(mockElement.append.calls[1].args[0].append.calls.length) | ||||
|                     .toEqual(2); | ||||
|             }); | ||||
|  | ||||
|             it("updates values", function () { | ||||
|                 var mockSpans; | ||||
|  | ||||
|                 mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f'] | ||||
|                     .map(createMockDomainObject); | ||||
|                 mockScope.values = { a: 0 }; | ||||
|  | ||||
|                 // Fire an update to the set of values | ||||
|                 fireWatch("objects"); | ||||
|                 fireWatch("updated"); | ||||
|  | ||||
|                 // Get all created spans | ||||
|                 mockSpans = mockElements.filter(function (mockElem) { | ||||
|                     return mockElem.testHtml === '<span>'; | ||||
|                 }); | ||||
|  | ||||
|                 // First span should be a, should have gotten this value. | ||||
|                 // This test detects, in particular, WTD-749 | ||||
|                 expect(mockSpans[0].text).toHaveBeenCalledWith('A'); | ||||
|                 expect(mockSpans[1].text).toHaveBeenCalledWith(0); | ||||
|             }); | ||||
|  | ||||
|             it("listens for changes in column width", function () { | ||||
|                 var mockUL = createMockElement("<ul>"); | ||||
|                 mockElement.find.andReturn(mockUL); | ||||
|                 mockScope.columnWidth = 200; | ||||
|                 fireWatch("columnWidth", mockScope.columnWidth); | ||||
|                 expect(mockUL.css).toHaveBeenCalledWith("width", "200px"); | ||||
|             }); | ||||
|  | ||||
|             it("updates CSS classes", function () { | ||||
|                 var mockSpans; | ||||
|  | ||||
|                 mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f'] | ||||
|                     .map(createMockDomainObject); | ||||
|                 mockScope.values = { a: "a value to find" }; | ||||
|                 mockScope.classes = { a: 'class-a' }; | ||||
|  | ||||
|                 // Fire an update to the set of values | ||||
|                 fireWatch("objects"); | ||||
|                 fireWatch("updated"); | ||||
|  | ||||
|                 // Figure out which span holds the relevant value... | ||||
|                 mockSpans = mockElements.filter(function (mockElem) { | ||||
|                     return mockElem.testHtml === '<span>'; | ||||
|                 }).filter(function (mockSpan) { | ||||
|                     var attrCalls = mockSpan.attr.calls; | ||||
|                     return attrCalls.some(function (call) { | ||||
|                         return call.args[0] === 'title' && | ||||
|                                 call.args[1] === mockScope.values.a; | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 // ...and make sure it also has had its class applied | ||||
|                 expect(mockSpans[0].addClass) | ||||
|                     .toHaveBeenCalledWith(mockScope.classes.a); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,341 +0,0 @@ | ||||
|  | ||||
| define( | ||||
|     ["../src/AutoflowTabularController"], | ||||
|     function (AutoflowTabularController) { | ||||
|  | ||||
|         describe("The autoflow tabular controller", function () { | ||||
|             var mockScope, | ||||
|                 mockTimeout, | ||||
|                 mockSubscriber, | ||||
|                 mockDomainObject, | ||||
|                 mockSubscription, | ||||
|                 controller; | ||||
|  | ||||
|             // Fire watches that are registered as functions. | ||||
|             function fireFnWatches() { | ||||
|                 mockScope.$watch.calls.forEach(function (call) { | ||||
|                     if (typeof call.args[0] === 'function') { | ||||
|                         call.args[1](call.args[0]()); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockScope = jasmine.createSpyObj( | ||||
|                     "$scope", | ||||
|                     ["$on", "$watch"] | ||||
|                 ); | ||||
|                 mockTimeout = jasmine.createSpy("$timeout"); | ||||
|                 mockSubscriber = jasmine.createSpyObj( | ||||
|                     "telemetrySubscriber", | ||||
|                     ["subscribe"] | ||||
|                 ); | ||||
|                 mockDomainObject = jasmine.createSpyObj( | ||||
|                     "domainObject", | ||||
|                     ["getId", "getModel", "getCapability"] | ||||
|                 ); | ||||
|                 mockSubscription = jasmine.createSpyObj( | ||||
|                     "subscription", | ||||
|                     [ | ||||
|                         "unsubscribe", | ||||
|                         "getTelemetryObjects", | ||||
|                         "getDomainValue", | ||||
|                         "getRangeValue" | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 mockSubscriber.subscribe.andReturn(mockSubscription); | ||||
|                 mockDomainObject.getModel.andReturn({name: "something"}); | ||||
|  | ||||
|                 controller = new AutoflowTabularController( | ||||
|                     mockScope, | ||||
|                     mockTimeout, | ||||
|                     mockSubscriber | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("listens for the represented domain object", function () { | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith( | ||||
|                     "domainObject", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("provides a getter-setter function for filtering", function () { | ||||
|                 expect(controller.filter()).toEqual(""); | ||||
|                 controller.filter("something"); | ||||
|                 expect(controller.filter()).toEqual("something"); | ||||
|             }); | ||||
|  | ||||
|             it("tracks bounds and adjust number of rows accordingly", function () { | ||||
|                 // Rows are 15px high, and need room for an 10px slider | ||||
|                 controller.setBounds({ width: 700, height: 120 }); | ||||
|                 expect(controller.getRows()).toEqual(6); // 110 usable height / 16px | ||||
|                 controller.setBounds({ width: 700, height: 240 }); | ||||
|                 expect(controller.getRows()).toEqual(14); // 230 usable height / 16px | ||||
|             }); | ||||
|  | ||||
|             it("subscribes to a represented object's telemetry", function () { | ||||
|                 // Set up subscription, scope | ||||
|                 mockSubscription.getTelemetryObjects | ||||
|                     .andReturn([mockDomainObject]); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|  | ||||
|                 // Invoke the watcher with represented domain object | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|  | ||||
|                 // Should have subscribed to it | ||||
|                 expect(mockSubscriber.subscribe).toHaveBeenCalledWith( | ||||
|                     mockDomainObject, | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|  | ||||
|                 // Should report objects as reported from subscription | ||||
|                 expect(controller.getTelemetryObjects()) | ||||
|                     .toEqual([mockDomainObject]); | ||||
|             }); | ||||
|  | ||||
|             it("releases subscriptions on destroy", function () { | ||||
|                 // Set up subscription... | ||||
|                 mockSubscription.getTelemetryObjects | ||||
|                     .andReturn([mockDomainObject]); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|  | ||||
|                 // Verify precondition | ||||
|                 expect(mockSubscription.unsubscribe).not.toHaveBeenCalled(); | ||||
|  | ||||
|                 // Make sure we're listening for $destroy | ||||
|                 expect(mockScope.$on).toHaveBeenCalledWith( | ||||
|                     "$destroy", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|  | ||||
|                 // Fire a destroy event | ||||
|                 mockScope.$on.mostRecentCall.args[1](); | ||||
|  | ||||
|                 // Should have unsubscribed | ||||
|                 expect(mockSubscription.unsubscribe).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("presents latest values and latest update state", function () { | ||||
|                 // Make sure values are available | ||||
|                 mockSubscription.getDomainValue.andReturn(402654321123); | ||||
|                 mockSubscription.getRangeValue.andReturn(789); | ||||
|                 mockDomainObject.getId.andReturn('testId'); | ||||
|  | ||||
|                 // Set up subscription... | ||||
|                 mockSubscription.getTelemetryObjects | ||||
|                     .andReturn([mockDomainObject]); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|  | ||||
|                 // Fire subscription callback | ||||
|                 mockSubscriber.subscribe.mostRecentCall.args[1](); | ||||
|  | ||||
|                 // ...and exposed the results for template to consume | ||||
|                 expect(controller.updated()).toEqual("1982-278 08:25:21.123Z"); | ||||
|                 expect(controller.rangeValues().testId).toEqual(789); | ||||
|             }); | ||||
|  | ||||
|             it("sorts domain objects by index", function () { | ||||
|                 var testIndexes = { a: 2, b: 1, c: 3, d: 0 }, | ||||
|                     mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) { | ||||
|                         var mockDomainObj = jasmine.createSpyObj( | ||||
|                             "domainObject", | ||||
|                             ["getId", "getModel"] | ||||
|                         ); | ||||
|  | ||||
|                         mockDomainObj.getId.andReturn(id); | ||||
|                         mockDomainObj.getModel.andReturn({ index: testIndexes[id] }); | ||||
|  | ||||
|                         return mockDomainObj; | ||||
|                     }); | ||||
|  | ||||
|                 // Expose those domain objects... | ||||
|                 mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|  | ||||
|                 // Fire subscription callback | ||||
|                 mockSubscriber.subscribe.mostRecentCall.args[1](); | ||||
|  | ||||
|                 // Controller should expose same objects, but sorted by index from model | ||||
|                 expect(controller.getTelemetryObjects()).toEqual([ | ||||
|                     mockDomainObjects[3], // d, index=0 | ||||
|                     mockDomainObjects[1], // b, index=1 | ||||
|                     mockDomainObjects[0], // a, index=2 | ||||
|                     mockDomainObjects[2]  // c, index=3 | ||||
|                 ]); | ||||
|             }); | ||||
|  | ||||
|             it("uses a timeout to throttle update", function () { | ||||
|                 // Set up subscription... | ||||
|                 mockSubscription.getTelemetryObjects | ||||
|                     .andReturn([mockDomainObject]); | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|  | ||||
|                 // Set the object in view; should not need a timeout | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|                 expect(mockTimeout.calls.length).toEqual(0); | ||||
|  | ||||
|                 // Next call should schedule an update on a timeout | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|                 expect(mockTimeout.calls.length).toEqual(1); | ||||
|  | ||||
|                 // ...but this last one should not, since existing | ||||
|                 // timeout will cover it | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|                 expect(mockTimeout.calls.length).toEqual(1); | ||||
|             }); | ||||
|  | ||||
|             it("allows changing column width", function () { | ||||
|                 var initialWidth = controller.columnWidth(); | ||||
|                 controller.increaseColumnWidth(); | ||||
|                 expect(controller.columnWidth()).toBeGreaterThan(initialWidth); | ||||
|             }); | ||||
|  | ||||
|             describe("filter", function () { | ||||
|                 var doFilter, | ||||
|                     filteredObjects, | ||||
|                     filteredObjectNames; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     var telemetryObjects, | ||||
|                         updateFilteredObjects; | ||||
|  | ||||
|                     telemetryObjects = [ | ||||
|                         'DEF123', | ||||
|                         'abc789', | ||||
|                         '456abc', | ||||
|                         '4ab3cdef', | ||||
|                         'hjs[12].*(){}^\\' | ||||
|                     ].map(function (objectName, index) { | ||||
|                         var mockTelemetryObject = jasmine.createSpyObj( | ||||
|                             objectName, | ||||
|                             ["getId", "getModel"] | ||||
|                         ); | ||||
|  | ||||
|                         mockTelemetryObject.getId.andReturn(objectName); | ||||
|                         mockTelemetryObject.getModel.andReturn({ | ||||
|                             name: objectName, | ||||
|                             index: index | ||||
|                         }); | ||||
|  | ||||
|                         return mockTelemetryObject; | ||||
|                     }); | ||||
|  | ||||
|                     mockSubscription | ||||
|                         .getTelemetryObjects | ||||
|                         .andReturn(telemetryObjects); | ||||
|  | ||||
|                     // Trigger domainObject change to create subscription. | ||||
|                     mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|  | ||||
|                     updateFilteredObjects = function () { | ||||
|                         filteredObjects = controller.getTelemetryObjects(); | ||||
|                         filteredObjectNames = filteredObjects.map(function (o) { | ||||
|                             return o.getModel().name; | ||||
|                         }); | ||||
|                     }; | ||||
|  | ||||
|                     doFilter = function (term) { | ||||
|                         controller.filter(term); | ||||
|                         // Filter is debounced so we have to force it to occur. | ||||
|                         mockTimeout.mostRecentCall.args[0](); | ||||
|                         updateFilteredObjects(); | ||||
|                     }; | ||||
|  | ||||
|                     updateFilteredObjects(); | ||||
|                 }); | ||||
|  | ||||
|                 it("initially shows all objects", function () { | ||||
|                     expect(filteredObjectNames).toEqual([ | ||||
|                         'DEF123', | ||||
|                         'abc789', | ||||
|                         '456abc', | ||||
|                         '4ab3cdef', | ||||
|                         'hjs[12].*(){}^\\' | ||||
|                     ]); | ||||
|                 }); | ||||
|  | ||||
|                 it("by blank string matches all objects", function () { | ||||
|                     doFilter(''); | ||||
|                     expect(filteredObjectNames).toEqual([ | ||||
|                         'DEF123', | ||||
|                         'abc789', | ||||
|                         '456abc', | ||||
|                         '4ab3cdef', | ||||
|                         'hjs[12].*(){}^\\' | ||||
|                     ]); | ||||
|                 }); | ||||
|  | ||||
|                 it("exactly matches an object name", function () { | ||||
|                     doFilter('4ab3cdef'); | ||||
|                     expect(filteredObjectNames).toEqual(['4ab3cdef']); | ||||
|                 }); | ||||
|  | ||||
|                 it("partially matches object names", function () { | ||||
|                     doFilter('abc'); | ||||
|                     expect(filteredObjectNames).toEqual([ | ||||
|                         'abc789', | ||||
|                         '456abc' | ||||
|                     ]); | ||||
|                 }); | ||||
|  | ||||
|                 it("matches case insensitive names", function () { | ||||
|                     doFilter('def'); | ||||
|                     expect(filteredObjectNames).toEqual([ | ||||
|                         'DEF123', | ||||
|                         '4ab3cdef' | ||||
|                     ]); | ||||
|                 }); | ||||
|  | ||||
|                 it("works as expected with special characters", function () { | ||||
|                     doFilter('[12]'); | ||||
|                     expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']); | ||||
|                     doFilter('.*'); | ||||
|                     expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']); | ||||
|                     doFilter('.*()'); | ||||
|                     expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']); | ||||
|                     doFilter('.*?'); | ||||
|                     expect(filteredObjectNames).toEqual([]); | ||||
|                     doFilter('.+'); | ||||
|                     expect(filteredObjectNames).toEqual([]); | ||||
|                 }); | ||||
|  | ||||
|                 it("exposes CSS classes from limits", function () { | ||||
|                     var id = mockDomainObject.getId(), | ||||
|                         testClass = "some-css-class", | ||||
|                         mockLimitCapability = | ||||
|                             jasmine.createSpyObj('limit', ['evaluate']); | ||||
|  | ||||
|                     mockDomainObject.getCapability.andCallFake(function (key) { | ||||
|                         return key === 'limit' && mockLimitCapability; | ||||
|                     }); | ||||
|                     mockLimitCapability.evaluate | ||||
|                         .andReturn({ cssClass: testClass }); | ||||
|  | ||||
|                     mockSubscription.getTelemetryObjects | ||||
|                         .andReturn([mockDomainObject]); | ||||
|  | ||||
|                     fireFnWatches(); | ||||
|                     mockSubscriber.subscribe.mostRecentCall.args[1](); | ||||
|  | ||||
|                     expect(controller.classes()[id]).toEqual(testClass); | ||||
|                 }); | ||||
|  | ||||
|                 it("exposes a counter that changes with each update", function () { | ||||
|                     var i, prior; | ||||
|  | ||||
|                     for (i = 0; i < 10; i += 1) { | ||||
|                         prior = controller.counter(); | ||||
|                         expect(controller.counter()).toEqual(prior); | ||||
|                         mockSubscriber.subscribe.mostRecentCall.args[1](); | ||||
|                         expect(controller.counter()).not.toEqual(prior); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,39 +0,0 @@ | ||||
|  | ||||
| define( | ||||
|     ["../src/MCTAutoflowTable"], | ||||
|     function (MCTAutoflowTable) { | ||||
|  | ||||
|         describe("The mct-autoflow-table directive", function () { | ||||
|             var mctAutoflowTable; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mctAutoflowTable = new MCTAutoflowTable(); | ||||
|             }); | ||||
|  | ||||
|             // Real functionality is contained/tested in the linker, | ||||
|             // so just check to make sure we're exposing the directive | ||||
|             // appropriately. | ||||
|             it("is applicable at the element level", function () { | ||||
|                 expect(mctAutoflowTable.restrict).toEqual("E"); | ||||
|             }); | ||||
|  | ||||
|             it("two-ways binds needed scope variables", function () { | ||||
|                 expect(mctAutoflowTable.scope).toEqual({ | ||||
|                     objects: "=", | ||||
|                     values: "=", | ||||
|                     rows: "=", | ||||
|                     updated: "=", | ||||
|                     classes: "=", | ||||
|                     columnWidth: "=", | ||||
|                     counter: "=" | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("provides a link function", function () { | ||||
|                 expect(mctAutoflowTable.link).toEqual(jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -45,7 +45,7 @@ define( | ||||
|  | ||||
|         FollowIndicator.prototype.getText = function () { | ||||
|             var timer = this.timerService.getTimer(); | ||||
|             return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER; | ||||
|             return timer ? ('Following timer ' + timer.name) : NO_TIMER; | ||||
|         }; | ||||
|  | ||||
|         FollowIndicator.prototype.getDescription = function () { | ||||
|   | ||||
| @@ -42,18 +42,15 @@ define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) { | ||||
|         }); | ||||
|  | ||||
|         describe("when a timer is set", function () { | ||||
|             var testModel; | ||||
|             var mockDomainObject; | ||||
|             var testObject; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testModel = { name: "some timer!" }; | ||||
|                 mockDomainObject = jasmine.createSpyObj('timer', ['getModel']); | ||||
|                 mockDomainObject.getModel.andReturn(testModel); | ||||
|                 mockTimerService.getTimer.andReturn(mockDomainObject); | ||||
|                 testObject = { name: "some timer!" }; | ||||
|                 mockTimerService.getTimer.andReturn(testObject); | ||||
|             }); | ||||
|  | ||||
|             it("displays the timer's name", function () { | ||||
|                 expect(indicator.getText().indexOf(testModel.name)) | ||||
|                 expect(indicator.getText().indexOf(testObject.name)) | ||||
|                     .not.toEqual(-1); | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -260,7 +260,9 @@ define([ | ||||
|                     "key": "LayoutController", | ||||
|                     "implementation": LayoutController, | ||||
|                     "depends": [ | ||||
|                         "$scope" | ||||
|                         "$scope", | ||||
|                         "$element", | ||||
|                         "openmct" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
| @@ -270,7 +272,8 @@ define([ | ||||
|                         "$scope", | ||||
|                         "$q", | ||||
|                         "dialogService", | ||||
|                         "openmct" | ||||
|                         "openmct", | ||||
|                         "$element" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|      ng-controller="FixedController as controller"> | ||||
|  | ||||
|     <!-- Background grid --> | ||||
|     <div class="l-grid-holder" ng-click="controller.clearSelection()"> | ||||
|     <div class="l-grid-holder" ng-click="controller.bypassSelection($event)"> | ||||
|         <div class="l-grid l-grid-x" | ||||
|              ng-if="!controller.getGridSize()[0] < 3" | ||||
|              ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div> | ||||
| @@ -35,33 +35,28 @@ | ||||
|     <!-- Fixed position elements --> | ||||
|     <div ng-repeat="element in controller.getElements()" | ||||
|          class="l-fixed-position-item s-selectable s-moveable s-hover-border" | ||||
|          ng-class="{ | ||||
|             's-not-selected': controller.selected() && !controller.selected(element), | ||||
|             's-selected': controller.selected(element) | ||||
|          }" | ||||
|          ng-style="element.style" | ||||
|          ng-click="controller.select(element)"> | ||||
|          mct-selectable="controller.getContext(element)" | ||||
|          mct-init-select="controller.shouldSelect(element)"> | ||||
|         <mct-include key="element.template" | ||||
|                      parameters="{ gridSize: controller.getGridSize() }" | ||||
|                      ng-model="element"> | ||||
|         </mct-include> | ||||
|         </mct-include>        | ||||
|     </div> | ||||
|  | ||||
|     <!-- Selection highlight, handles --> | ||||
|     <span class="s-selected s-moveable" ng-if="controller.selected()"> | ||||
|     <span class="s-selected s-moveable" ng-if="controller.isElementSelected()"> | ||||
|         <div class="l-fixed-position-item t-edit-handle-holder" | ||||
|              mct-drag-down="controller.moveHandle().startDrag(controller.selected())" | ||||
|              mct-drag-down="controller.moveHandle().startDrag()" | ||||
|              mct-drag="controller.moveHandle().continueDrag(delta)" | ||||
|              mct-drag-up="controller.moveHandle().endDrag()" | ||||
|              ng-style="controller.selected().style"> | ||||
|              mct-drag-up="controller.endDrag()" | ||||
|              ng-style="controller.getSelectedElementStyle()"> | ||||
|         </div> | ||||
|         <div ng-repeat="handle in controller.handles()" | ||||
|              class="l-fixed-position-item-handle edit-corner" | ||||
|              ng-style="handle.style()" | ||||
|              mct-drag-down="handle.startDrag()" | ||||
|              mct-drag="handle.continueDrag(delta)" | ||||
|              mct-drag-up="handle.endDrag()"> | ||||
|              mct-drag-up="controller.endDrag(handle)"> | ||||
|         </div> | ||||
|     </span> | ||||
|  | ||||
| </div> | ||||
|   | ||||
| @@ -22,10 +22,12 @@ | ||||
|  | ||||
| <div class="abs l-layout" | ||||
|      ng-controller="LayoutController as controller" | ||||
|      ng-click="controller.clearSelection()"> | ||||
|      ng-click="controller.bypassSelection($event)"> | ||||
|  | ||||
|     <!-- Background grid --> | ||||
|     <div class="l-grid-holder" ng-click="controller.clearSelection()"> | ||||
|     <div class="l-grid-holder" | ||||
|          ng-show="!controller.drilledIn" | ||||
|          ng-click="controller.bypassSelection($event)"> | ||||
|         <div class="l-grid l-grid-x" | ||||
|              ng-if="!controller.getGridSize()[0] < 3" | ||||
|              ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div> | ||||
| @@ -34,10 +36,13 @@ | ||||
|              ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div> | ||||
|     </div> | ||||
|  | ||||
|     <div class='abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border' | ||||
|          ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-selected':controller.selected(childObject) }" | ||||
|     <div class="abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border t-object-type-{{ childObject.getModel().type }}" | ||||
|          data-layout-id="{{childObject.getId() + '-' + $id}}" | ||||
|          ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }" | ||||
|          ng-repeat="childObject in composition" | ||||
|          ng-click="controller.select($event, childObject.getId())" | ||||
|          ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)" | ||||
|          mct-selectable="controller.getContext(childObject, true)" | ||||
|          ng-dblclick="controller.drill($event, childObject)" | ||||
|          ng-style="controller.getFrameStyle(childObject.getId())"> | ||||
|  | ||||
|         <mct-representation key="'frame'" | ||||
| @@ -45,7 +50,7 @@ | ||||
|                             mct-object="childObject"> | ||||
|         </mct-representation> | ||||
|         <!-- Drag handles --> | ||||
|         <span class="abs t-edit-handle-holder s-hover-border" ng-if="controller.selected(childObject)"> | ||||
|         <span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)"> | ||||
|             <span class="edit-handle edit-move" | ||||
|                   mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])" | ||||
|                   mct-drag="controller.continueDrag(delta)" | ||||
| @@ -73,7 +78,6 @@ | ||||
|                   mct-drag-up="controller.endDrag()"> | ||||
|             </span> | ||||
|         </span> | ||||
|  | ||||
|     </div> | ||||
|  | ||||
| </div> | ||||
|   | ||||
| @@ -47,7 +47,7 @@ define( | ||||
|          * @constructor | ||||
|          * @param {Scope} $scope the controller's Angular scope | ||||
|          */ | ||||
|         function FixedController($scope, $q, dialogService, openmct) { | ||||
|         function FixedController($scope, $q, dialogService, openmct, $element) { | ||||
|             this.names = {}; // Cache names by ID | ||||
|             this.values = {}; // Cache values by ID | ||||
|             this.elementProxiesById = {}; | ||||
| @@ -55,9 +55,11 @@ define( | ||||
|             this.telemetryObjects = []; | ||||
|             this.subscriptions = []; | ||||
|             this.openmct = openmct; | ||||
|             this.$element = $element; | ||||
|             this.$scope = $scope; | ||||
|  | ||||
|             this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid; | ||||
|             this.fixedViewSelectable = false; | ||||
|  | ||||
|             var self = this; | ||||
|             [ | ||||
| @@ -87,9 +89,8 @@ define( | ||||
|  | ||||
|             // Update the style for a selected element | ||||
|             function updateSelectionStyle() { | ||||
|                 var element = self.selection && self.selection.get(); | ||||
|                 if (element) { | ||||
|                     element.style = convertPosition(element); | ||||
|                 if (self.selectedElementProxy) { | ||||
|                     self.selectedElementProxy.style = convertPosition(self.selectedElementProxy); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -136,25 +137,19 @@ define( | ||||
|  | ||||
|             // Decorate elements in the current configuration | ||||
|             function refreshElements() { | ||||
|                 // Cache selection; we are instantiating new proxies | ||||
|                 // so we may want to restore this. | ||||
|                 var selected = self.selection && self.selection.get(), | ||||
|                     elements = (($scope.configuration || {}).elements || []), | ||||
|                     index = -1; // Start with a 'not-found' value | ||||
|  | ||||
|                 // Find the selection in the new array | ||||
|                 if (selected !== undefined) { | ||||
|                     index = elements.indexOf(selected.element); | ||||
|                 } | ||||
|                 var elements = (($scope.configuration || {}).elements || []); | ||||
|  | ||||
|                 // Create the new proxies... | ||||
|                 self.elementProxies = elements.map(makeProxyElement); | ||||
|  | ||||
|                 // Clear old selection, and restore if appropriate | ||||
|                 if (self.selection) { | ||||
|                     self.selection.deselect(); | ||||
|                     if (index > -1) { | ||||
|                         self.select(self.elementProxies[index]); | ||||
|                 // If selection is not in array, select parent. | ||||
|                 // Otherwise, set the element to select after refresh. | ||||
|                 if (self.selectedElementProxy) { | ||||
|                     var index = elements.indexOf(self.selectedElementProxy.element); | ||||
|                     if (index === -1) { | ||||
|                         self.$element[0].click(); | ||||
|                     } else if (!self.elementToSelectAfterRefresh) { | ||||
|                         self.elementToSelectAfterRefresh = self.elementProxies[index].element; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
| @@ -224,12 +219,12 @@ define( | ||||
|                     $scope.configuration.elements || []; | ||||
|                 // Store the position of this element. | ||||
|                 $scope.configuration.elements.push(element); | ||||
|  | ||||
|                 self.elementToSelectAfterRefresh = element; | ||||
|  | ||||
|                 // Refresh displayed elements | ||||
|                 refreshElements(); | ||||
|                 // Select the newly-added element | ||||
|                 self.select( | ||||
|                     self.elementProxies[self.elementProxies.length - 1] | ||||
|                 ); | ||||
|  | ||||
|                 // Mark change as persistable | ||||
|                 if ($scope.commit) { | ||||
|                     $scope.commit("Dropped an element."); | ||||
| @@ -263,21 +258,36 @@ define( | ||||
|                 self.getTelemetry($scope.domainObject); | ||||
|             } | ||||
|  | ||||
|             // Sets the selectable object in response to the selection change event. | ||||
|             function setSelection(selectable) { | ||||
|                 var selection = selectable[0]; | ||||
|  | ||||
|                 if (!selection) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (selection.context.elementProxy) { | ||||
|                     self.selectedElementProxy = selection.context.elementProxy; | ||||
|                     self.mvHandle = self.generateDragHandle(self.selectedElementProxy); | ||||
|                     self.resizeHandles = self.generateDragHandles(self.selectedElementProxy); | ||||
|                 } else { | ||||
|                     // Make fixed view selectable if it's not already. | ||||
|                     if (!self.fixedViewSelectable && selectable.length === 1) { | ||||
|                         self.fixedViewSelectable = true; | ||||
|                         selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService); | ||||
|                         self.openmct.selection.select(selection); | ||||
|                     } | ||||
|  | ||||
|                     self.resizeHandles = []; | ||||
|                     self.mvHandle = undefined; | ||||
|                     self.selectedElementProxy = undefined; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.elementProxies = []; | ||||
|             this.generateDragHandle = generateDragHandle; | ||||
|             this.generateDragHandles = generateDragHandles; | ||||
|  | ||||
|             // Track current selection state | ||||
|             $scope.$watch("selection", function (selection) { | ||||
|                 this.selection = selection; | ||||
|  | ||||
|                 // Expose the view's selection proxy | ||||
|                 if (this.selection) { | ||||
|                     this.selection.proxy( | ||||
|                         new FixedProxy(addElement, $q, dialogService) | ||||
|                     ); | ||||
|                 } | ||||
|             }.bind(this)); | ||||
|             this.updateSelectionStyle = updateSelectionStyle; | ||||
|  | ||||
|             // Detect changes to grid size | ||||
|             $scope.$watch("model.layoutGrid", updateElementPositions); | ||||
| @@ -298,10 +308,13 @@ define( | ||||
|             $scope.$on("$destroy", function () { | ||||
|                 self.unsubscribe(); | ||||
|                 self.openmct.time.off("bounds", updateDisplayBounds); | ||||
|                 self.openmct.selection.off("change", setSelection); | ||||
|             }); | ||||
|  | ||||
|             // Respond to external bounds changes | ||||
|             this.openmct.time.on("bounds", updateDisplayBounds); | ||||
|             this.openmct.selection.on('change', setSelection); | ||||
|             this.$element.on('click', this.bypassSelection.bind(this)); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -492,38 +505,56 @@ define( | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the element is currently selected, or (if no | ||||
|          * argument is supplied) get the currently selected element. | ||||
|          * @returns {boolean} true if selected | ||||
|          * Checks if the element should be selected or not. | ||||
|          * | ||||
|          * @param elementProxy the element to check | ||||
|          * @returns {boolean} true if the element should be selected. | ||||
|          */ | ||||
|         FixedController.prototype.selected = function (element) { | ||||
|             var selection = this.selection; | ||||
|             return selection && ((arguments.length > 0) ? | ||||
|                     selection.selected(element) : selection.get()); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Set the active user selection in this view. | ||||
|          * @param element the element to select | ||||
|          */ | ||||
|         FixedController.prototype.select = function select(element) { | ||||
|             if (this.selection) { | ||||
|                 // Update selection... | ||||
|                 this.selection.select(element); | ||||
|                 // ...as well as move, resize handles | ||||
|                 this.mvHandle = this.generateDragHandle(element); | ||||
|                 this.resizeHandles = this.generateDragHandles(element); | ||||
|         FixedController.prototype.shouldSelect = function (elementProxy) { | ||||
|             if (elementProxy.element === this.elementToSelectAfterRefresh) { | ||||
|                 delete this.elementToSelectAfterRefresh; | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Clear the current user selection. | ||||
|          * Checks if an element is currently selected. | ||||
|          * | ||||
|          * @returns {boolean} true if an element is selected. | ||||
|          */ | ||||
|         FixedController.prototype.clearSelection = function () { | ||||
|             if (this.selection) { | ||||
|                 this.selection.deselect(); | ||||
|                 this.resizeHandles = []; | ||||
|                 this.mvHandle = undefined; | ||||
|         FixedController.prototype.isElementSelected = function () { | ||||
|             return (this.selectedElementProxy) ? true : false; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Gets the style for the selected element. | ||||
|          * | ||||
|          * @returns {string} element style | ||||
|          */ | ||||
|         FixedController.prototype.getSelectedElementStyle = function () { | ||||
|             return (this.selectedElementProxy) ? this.selectedElementProxy.style : undefined; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Gets the selected element. | ||||
|          * | ||||
|          * @returns the selected element | ||||
|          */ | ||||
|         FixedController.prototype.getSelectedElement = function () { | ||||
|             return this.selectedElementProxy; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Prevents the event from bubbling up if drag is in progress. | ||||
|          */ | ||||
|         FixedController.prototype.bypassSelection = function ($event) { | ||||
|             if (this.dragInProgress) { | ||||
|                 if ($event) { | ||||
|                     $event.stopPropagation(); | ||||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -544,6 +575,38 @@ define( | ||||
|             return this.mvHandle; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Gets the selection context. | ||||
|          * | ||||
|          * @param elementProxy the element proxy | ||||
|          * @returns {object} the context object which includes elementProxy and toolbar | ||||
|          */ | ||||
|         FixedController.prototype.getContext = function (elementProxy) { | ||||
|             return { | ||||
|                 elementProxy: elementProxy, | ||||
|                 toolbar: elementProxy | ||||
|             }; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * End drag. | ||||
|          * | ||||
|          * @param handle the resize handle | ||||
|          */ | ||||
|         FixedController.prototype.endDrag = function (handle) { | ||||
|             this.dragInProgress = true; | ||||
|  | ||||
|             setTimeout(function () { | ||||
|                 this.dragInProgress = false; | ||||
|             }.bind(this), 0); | ||||
|  | ||||
|             if (handle) { | ||||
|                 handle.endDrag(); | ||||
|             } else { | ||||
|                 this.moveHandle().endDrag(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return FixedController; | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -65,7 +65,7 @@ define( | ||||
|          * Start a drag gesture. This should be called when a drag | ||||
|          * begins to track initial state. | ||||
|          */ | ||||
|         FixedDragHandle.prototype.startDrag = function startDrag() { | ||||
|         FixedDragHandle.prototype.startDrag = function () { | ||||
|             // Cache initial x/y positions | ||||
|             this.dragging = { | ||||
|                 x: this.elementHandle.x(), | ||||
|   | ||||
| @@ -27,9 +27,11 @@ | ||||
|  */ | ||||
| define( | ||||
|     [ | ||||
|         'zepto', | ||||
|         './LayoutDrag' | ||||
|     ], | ||||
|     function ( | ||||
|         $, | ||||
|         LayoutDrag | ||||
|     ) { | ||||
|  | ||||
| @@ -50,10 +52,12 @@ define( | ||||
|          * @constructor | ||||
|          * @param {Scope} $scope the controller's Angular scope | ||||
|          */ | ||||
|         function LayoutController($scope) { | ||||
|         function LayoutController($scope, $element, openmct) { | ||||
|             var self = this, | ||||
|                 callbackCount = 0; | ||||
|  | ||||
|             this.$element = $element; | ||||
|  | ||||
|             // Update grid size when it changed | ||||
|             function updateGridSize(layoutGrid) { | ||||
|                 var oldSize = self.gridSize; | ||||
| @@ -123,12 +127,11 @@ define( | ||||
|                         self.layoutPanels(ids); | ||||
|                         self.setFrames(ids); | ||||
|  | ||||
|                         // If there is a newly-dropped object, select it. | ||||
|                         if (self.droppedIdToSelectAfterRefresh) { | ||||
|                             self.select(null, self.droppedIdToSelectAfterRefresh); | ||||
|                             delete self.droppedIdToSelectAfterRefresh; | ||||
|                         } else if (composition.indexOf(self.selectedId) === -1) { | ||||
|                             self.clearSelection(); | ||||
|                         if (self.selectedId && | ||||
|                             self.selectedId !== $scope.domainObject.getId() && | ||||
|                             composition.indexOf(self.selectedId) === -1) { | ||||
|                             // Click triggers selection of layout parent. | ||||
|                             self.$element[0].click(); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| @@ -160,22 +163,39 @@ define( | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             // Sets the selectable object in response to the selection change event. | ||||
|             function setSelection(selectable) { | ||||
|                 var selection = selectable[0]; | ||||
|  | ||||
|                 if (!selection) { | ||||
|                     delete self.selectedId; | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 self.selectedId = selection.context.oldItem.getId(); | ||||
|                 self.drilledIn = undefined; | ||||
|                 self.selectable = selectable; | ||||
|             } | ||||
|  | ||||
|             this.positions = {}; | ||||
|             this.rawPositions = {}; | ||||
|             this.gridSize = DEFAULT_GRID_SIZE; | ||||
|             this.$scope = $scope; | ||||
|             this.drilledIn = undefined; | ||||
|             this.openmct = openmct; | ||||
|  | ||||
|             // Watch for changes to the grid size in the model | ||||
|             $scope.$watch("model.layoutGrid", updateGridSize); | ||||
|  | ||||
|             $scope.$watch("selection", function (selection) { | ||||
|                 this.selection = selection; | ||||
|             }.bind(this)); | ||||
|  | ||||
|             // Update composed objects on screen, and position panes | ||||
|             $scope.$watchCollection("model.composition", refreshComposition); | ||||
|  | ||||
|             // Position panes where they are dropped | ||||
|             openmct.selection.on('change', setSelection); | ||||
|  | ||||
|             $scope.$on("$destroy", function () { | ||||
|                 openmct.selection.off("change", setSelection); | ||||
|             }); | ||||
|  | ||||
|             $scope.$on("mctDrop", handleDrop); | ||||
|         } | ||||
|  | ||||
| @@ -357,37 +377,14 @@ define( | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the object is currently selected. | ||||
|          * Checks if the object is currently selected. | ||||
|          * | ||||
|          * @param {string} obj the object to check for selection | ||||
|          * @returns {boolean} true if selected, otherwise false | ||||
|          */ | ||||
|         LayoutController.prototype.selected = function (obj) { | ||||
|             return !!this.selectedId && this.selectedId === obj.getId(); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Set the active user selection in this view. | ||||
|          * | ||||
|          * @param event the mouse event | ||||
|          * @param {string} id the object id | ||||
|          */ | ||||
|         LayoutController.prototype.select = function (event, id) { | ||||
|             if (event) { | ||||
|                 event.stopPropagation(); | ||||
|                 if (this.selection) { | ||||
|                     event.preventDefault(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.selectedId = id; | ||||
|  | ||||
|             var selectedObj = {}; | ||||
|             selectedObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id); | ||||
|  | ||||
|             if (this.selection) { | ||||
|                 this.selection.select(selectedObj); | ||||
|             } | ||||
|             var sobj = this.openmct.selection.get()[0]; | ||||
|             return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
| @@ -396,7 +393,7 @@ define( | ||||
|          * @param {string} id the object id | ||||
|          * @private | ||||
|          */ | ||||
|         LayoutController.prototype.toggleFrame = function (id) { | ||||
|         LayoutController.prototype.toggleFrame = function (id, domainObject) { | ||||
|             var configuration = this.$scope.configuration; | ||||
|  | ||||
|             if (!configuration.panels[id]) { | ||||
| @@ -404,21 +401,75 @@ define( | ||||
|             } | ||||
|  | ||||
|             this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id]; | ||||
|             this.select(undefined, id); // reselect so toolbar updates | ||||
|  | ||||
|             var selection = this.openmct.selection.get(); | ||||
|             selection[0].context.toolbar = this.getToolbar(id, domainObject); | ||||
|             this.openmct.selection.select(selection);  // reselect so toolbar updates | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Clear the current user selection. | ||||
|          * Gets the toolbar object for the given domain object. | ||||
|          * | ||||
|          * @param id the domain object id | ||||
|          * @param domainObject the domain object | ||||
|          * @returns {object} | ||||
|          * @private | ||||
|          */ | ||||
|         LayoutController.prototype.clearSelection = function () { | ||||
|         LayoutController.prototype.getToolbar = function (id, domainObject) { | ||||
|             var toolbarObj = {}; | ||||
|             toolbarObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id, domainObject); | ||||
|             return toolbarObj; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Bypasses selection if drag is in progress. | ||||
|          * | ||||
|          * @param event the angular event object | ||||
|          */ | ||||
|         LayoutController.prototype.bypassSelection = function (event) { | ||||
|             if (this.dragInProgress) { | ||||
|                 if (event) { | ||||
|                     event.stopPropagation(); | ||||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Checks if the domain object is drilled in. | ||||
|          * | ||||
|          * @param domainObject the domain object | ||||
|          * @return true if the object is drilled in, false otherwise | ||||
|          */ | ||||
|         LayoutController.prototype.isDrilledIn = function (domainObject) { | ||||
|             return this.drilledIn === domainObject.getId(); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Puts the given object in the drilled-in mode. | ||||
|          * | ||||
|          * @param event the angular event object | ||||
|          * @param domainObject the domain object | ||||
|          */ | ||||
|         LayoutController.prototype.drill = function (event, domainObject) { | ||||
|             if (event) { | ||||
|                 event.stopPropagation(); | ||||
|             } | ||||
|  | ||||
|             if (!domainObject.getCapability('editor').inEditContext()) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.selection) { | ||||
|                 this.selection.deselect(); | ||||
|                 delete this.selectedId; | ||||
|             if (!domainObject.hasCapability('composition')) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Disable since fixed position doesn't use the selection API yet | ||||
|             if (domainObject.getModel().type === 'telemetry.fixed') { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.drilledIn = domainObject.getId(); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
| @@ -440,6 +491,36 @@ define( | ||||
|             return this.gridSize; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Gets the selection context. | ||||
|          * | ||||
|          * @param domainObject the domain object | ||||
|          * @returns {object} the context object which includes | ||||
|          *                  item, oldItem and toolbar | ||||
|          */ | ||||
|         LayoutController.prototype.getContext = function (domainObject, toolbar) { | ||||
|             return { | ||||
|                 item: domainObject.useCapability('adapter'), | ||||
|                 oldItem: domainObject, | ||||
|                 toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined | ||||
|             }; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Selects a newly-dropped object. | ||||
|          * | ||||
|          * @param classSelector the css class selector | ||||
|          * @param domainObject the domain object | ||||
|          */ | ||||
|         LayoutController.prototype.selectIfNew = function (selector, domainObject) { | ||||
|             if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) { | ||||
|                 setTimeout(function () { | ||||
|                     $('[data-layout-id="' + selector + '"]')[0].click(); | ||||
|                     delete this.droppedIdToSelectAfterRefresh; | ||||
|                 }.bind(this), 0); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return LayoutController; | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -55,8 +55,8 @@ define( | ||||
|          * @param element the fixed position element, as stored in its | ||||
|          *        configuration | ||||
|          * @param index the element's index within its array | ||||
|          * @param {number[]} gridSize the current layout grid size in [x,y] from | ||||
|          * @param {Array} elements the full array of elements | ||||
|          * @param {number[]} gridSize the current layout grid size in [x,y] from | ||||
|          */ | ||||
|         function ElementProxy(element, index, elements, gridSize) { | ||||
|             /** | ||||
|   | ||||
| @@ -21,8 +21,14 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../src/FixedController"], | ||||
|     function (FixedController) { | ||||
|     [ | ||||
|         "../src/FixedController", | ||||
|         "zepto" | ||||
|     ], | ||||
|     function ( | ||||
|         FixedController, | ||||
|         $ | ||||
|     ) { | ||||
|  | ||||
|         describe("The Fixed Position controller", function () { | ||||
|             var mockScope, | ||||
| @@ -46,6 +52,9 @@ define( | ||||
|                 mockMetadata, | ||||
|                 mockTimeSystem, | ||||
|                 mockLimitEvaluator, | ||||
|                 mockSelection, | ||||
|                 $element = [], | ||||
|                 selectable = [], | ||||
|                 controller; | ||||
|  | ||||
|             // Utility function; find a watch for a given expression | ||||
| @@ -180,17 +189,30 @@ define( | ||||
|  | ||||
|                 mockScope.model = testModel; | ||||
|                 mockScope.configuration = testConfiguration; | ||||
|                 mockScope.selection = jasmine.createSpyObj( | ||||
|                     'selection', | ||||
|                     ['select', 'get', 'selected', 'deselect', 'proxy'] | ||||
|                 ); | ||||
|  | ||||
|                 selectable[0] = { | ||||
|                     context: { | ||||
|                         oldItem: mockDomainObject | ||||
|                     } | ||||
|                 }; | ||||
|                 mockSelection = jasmine.createSpyObj("selection", [ | ||||
|                     'select', | ||||
|                     'on', | ||||
|                     'off', | ||||
|                     'get' | ||||
|                 ]); | ||||
|                 mockSelection.get.andCallThrough(); | ||||
|  | ||||
|                 mockOpenMCT = { | ||||
|                     time: mockConductor, | ||||
|                     telemetry: mockTelemetryAPI, | ||||
|                     composition: mockCompositionAPI | ||||
|                     composition: mockCompositionAPI, | ||||
|                     selection: mockSelection | ||||
|                 }; | ||||
|  | ||||
|                 $element = $('<div></div>'); | ||||
|                 spyOn($element[0], 'click'); | ||||
|  | ||||
|                 mockMetadata = jasmine.createSpyObj('mockMetadata', [ | ||||
|                     'valuesForHints', | ||||
|                     'value', | ||||
| @@ -226,11 +248,11 @@ define( | ||||
|                     mockScope, | ||||
|                     mockQ, | ||||
|                     mockDialogService, | ||||
|                     mockOpenMCT | ||||
|                     mockOpenMCT, | ||||
|                     $element | ||||
|                 ); | ||||
|  | ||||
|                 findWatch("model.layoutGrid")(testModel.layoutGrid); | ||||
|                 findWatch("selection")(mockScope.selection); | ||||
|             }); | ||||
|  | ||||
|             it("subscribes when a domain object is available", function () { | ||||
| @@ -306,41 +328,41 @@ define( | ||||
|             }); | ||||
|  | ||||
|             it("allows elements to be selected", function () { | ||||
|                 var elements; | ||||
|  | ||||
|                 testModel.modified = 1; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|  | ||||
|                 elements = controller.getElements(); | ||||
|                 controller.select(elements[1]); | ||||
|                 expect(mockScope.selection.select) | ||||
|                     .toHaveBeenCalledWith(elements[1]); | ||||
|                 selectable[0].context.elementProxy = controller.getElements()[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(controller.isElementSelected()).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             it("allows selection retrieval", function () { | ||||
|                 // selected with no arguments should give the current | ||||
|                 // selection | ||||
|                 var elements; | ||||
|  | ||||
|                 testModel.modified = 1; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|  | ||||
|                 elements = controller.getElements(); | ||||
|                 controller.select(elements[1]); | ||||
|                 mockScope.selection.get.andReturn(elements[1]); | ||||
|                 expect(controller.selected()).toEqual(elements[1]); | ||||
|                 selectable[0].context.elementProxy = elements[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(controller.getSelectedElement()).toEqual(elements[1]); | ||||
|             }); | ||||
|  | ||||
|             it("allows selections to be cleared", function () { | ||||
|                 var elements; | ||||
|  | ||||
|             it("selects the parent view when selected element is removed", function () { | ||||
|                 testModel.modified = 1; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|  | ||||
|                 elements = controller.getElements(); | ||||
|                 controller.select(elements[1]); | ||||
|                 controller.clearSelection(); | ||||
|                 expect(controller.selected(elements[1])).toBeFalsy(); | ||||
|                 var elements = controller.getElements(); | ||||
|                 selectable[0].context.elementProxy = elements[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 elements[1].remove(); | ||||
|                 testModel.modified = 2; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|  | ||||
|                 expect($element[0].click).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("retains selections during refresh", function () { | ||||
| @@ -352,23 +374,21 @@ define( | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|  | ||||
|                 elements = controller.getElements(); | ||||
|                 controller.select(elements[1]); | ||||
|                 selectable[0].context.elementProxy = elements[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 // Verify precondition | ||||
|                 expect(mockScope.selection.select.calls.length).toEqual(1); | ||||
|  | ||||
|                 // Mimic selection behavior | ||||
|                 mockScope.selection.get.andReturn(elements[1]); | ||||
|                 expect(controller.getSelectedElement()).toEqual(elements[1]); | ||||
|  | ||||
|                 elements[2].remove(); | ||||
|                 testModel.modified = 2; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|  | ||||
|                 elements = controller.getElements(); | ||||
|  | ||||
|                 // Verify removal, as test assumes this | ||||
|                 expect(elements.length).toEqual(2); | ||||
|  | ||||
|                 expect(mockScope.selection.select.calls.length).toEqual(2); | ||||
|                 expect(controller.shouldSelect(elements[1])).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             it("Displays received values for telemetry elements", function () { | ||||
| @@ -505,21 +525,25 @@ define( | ||||
|             }); | ||||
|  | ||||
|             it("exposes a view-level selection proxy", function () { | ||||
|                 expect(mockScope.selection.proxy).toHaveBeenCalledWith( | ||||
|                     jasmine.any(Object) | ||||
|                 ); | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|                 var selection = mockOpenMCT.selection.select.mostRecentCall.args[0]; | ||||
|  | ||||
|                 expect(mockOpenMCT.selection.select).toHaveBeenCalled(); | ||||
|                 expect(selection.context.viewProxy).toBeDefined(); | ||||
|             }); | ||||
|  | ||||
|             it("exposes drag handles", function () { | ||||
|                 var handles; | ||||
|  | ||||
|                 // Select something so that drag handles are expected | ||||
|                 testModel.modified = 1; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|                 controller.select(controller.getElements()[1]); | ||||
|  | ||||
|                 selectable[0].context.elementProxy = controller.getElements()[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 // Should have a non-empty array of handles | ||||
|                 handles = controller.handles(); | ||||
|  | ||||
|                 expect(handles).toEqual(jasmine.any(Array)); | ||||
|                 expect(handles.length).not.toEqual(0); | ||||
|  | ||||
| @@ -532,15 +556,14 @@ define( | ||||
|             }); | ||||
|  | ||||
|             it("exposes a move handle", function () { | ||||
|                 var handle; | ||||
|  | ||||
|                 // Select something so that drag handles are expected | ||||
|                 testModel.modified = 1; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|                 controller.select(controller.getElements()[1]); | ||||
|  | ||||
|                 selectable[0].context.elementProxy = controller.getElements()[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 // Should have a move handle | ||||
|                 handle = controller.moveHandle(); | ||||
|                 var handle = controller.moveHandle(); | ||||
|  | ||||
|                 // And it should have start/continue/end drag methods | ||||
|                 expect(handle.startDrag).toEqual(jasmine.any(Function)); | ||||
| @@ -551,26 +574,40 @@ define( | ||||
|             it("updates selection style during drag", function () { | ||||
|                 var oldStyle; | ||||
|  | ||||
|                 // Select something so that drag handles are expected | ||||
|                 testModel.modified = 1; | ||||
|                 findWatch("model.modified")(testModel.modified); | ||||
|                 controller.select(controller.getElements()[1]); | ||||
|                 mockScope.selection.get.andReturn(controller.getElements()[1]); | ||||
|  | ||||
|                 selectable[0].context.elementProxy = controller.getElements()[1]; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 // Get style | ||||
|                 oldStyle = controller.selected().style; | ||||
|                 oldStyle = controller.getSelectedElementStyle(); | ||||
|  | ||||
|                 // Start a drag gesture | ||||
|                 controller.moveHandle().startDrag(); | ||||
|  | ||||
|                 // Haven't moved yet; style shouldn't have updated yet | ||||
|                 expect(controller.selected().style).toEqual(oldStyle); | ||||
|                 expect(controller.getSelectedElementStyle()).toEqual(oldStyle); | ||||
|  | ||||
|                 // Drag a little | ||||
|                 controller.moveHandle().continueDrag([1000, 100]); | ||||
|  | ||||
|                 // Style should have been updated | ||||
|                 expect(controller.selected().style).not.toEqual(oldStyle); | ||||
|                 expect(controller.getSelectedElementStyle()).not.toEqual(oldStyle); | ||||
|             }); | ||||
|  | ||||
|             it("cleans up slection on scope destroy", function () { | ||||
|                 expect(mockScope.$on).toHaveBeenCalledWith( | ||||
|                     '$destroy', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|  | ||||
|                 mockScope.$on.mostRecentCall.args[1](); | ||||
|  | ||||
|                 expect(mockOpenMCT.selection.off).toHaveBeenCalledWith( | ||||
|                     'change', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             describe("on display bounds changes", function () { | ||||
| @@ -702,6 +739,14 @@ define( | ||||
|                         expect(controller.getElements()[0].cssClass).toEqual("alarm-a"); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("listens for selection change events", function () { | ||||
|                     expect(mockOpenMCT.selection.on).toHaveBeenCalledWith( | ||||
|                         'change', | ||||
|                         jasmine.any(Function) | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -21,8 +21,14 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ["../src/LayoutController"], | ||||
|     function (LayoutController) { | ||||
|     [ | ||||
|         "../src/LayoutController", | ||||
|         "zepto" | ||||
|     ], | ||||
|     function ( | ||||
|         LayoutController, | ||||
|         $ | ||||
|     ) { | ||||
|  | ||||
|         describe("The Layout controller", function () { | ||||
|             var mockScope, | ||||
| @@ -32,7 +38,12 @@ define( | ||||
|                 controller, | ||||
|                 mockCompositionCapability, | ||||
|                 mockComposition, | ||||
|                 mockCompositionObjects; | ||||
|                 mockCompositionObjects, | ||||
|                 mockOpenMCT, | ||||
|                 mockSelection, | ||||
|                 mockDomainObjectCapability, | ||||
|                 $element = [], | ||||
|                 selectable = []; | ||||
|  | ||||
|             function mockPromise(value) { | ||||
|                 return { | ||||
| @@ -58,21 +69,18 @@ define( | ||||
|                         } else { | ||||
|                             return {}; | ||||
|                         } | ||||
|                     }, | ||||
|                     getCapability: function () { | ||||
|                         return mockDomainObjectCapability; | ||||
|                     }, | ||||
|                     hasCapability: function (param) { | ||||
|                         if (param === 'composition') { | ||||
|                             return id !== 'b'; | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             // Utility function to find a watch for a given expression | ||||
|             function findWatch(expr) { | ||||
|                 var watch; | ||||
|                 mockScope.$watch.calls.forEach(function (call) { | ||||
|                     if (call.args[0] === expr) { | ||||
|                         watch = call.args[1]; | ||||
|                     } | ||||
|                 }); | ||||
|                 return watch; | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockScope = jasmine.createSpyObj( | ||||
|                     "$scope", | ||||
| @@ -88,7 +96,6 @@ define( | ||||
|                 mockComposition = ["a", "b", "c"]; | ||||
|                 mockCompositionObjects = mockComposition.map(mockDomainObject); | ||||
|  | ||||
|  | ||||
|                 testConfiguration = { | ||||
|                     panels: { | ||||
|                         a: { | ||||
| @@ -97,27 +104,70 @@ define( | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 mockDomainObjectCapability = jasmine.createSpyObj('capability', | ||||
|                     ['inEditContext'] | ||||
|                 ); | ||||
|                 mockCompositionCapability = mockPromise(mockCompositionObjects); | ||||
|  | ||||
|                 mockScope.domainObject = mockDomainObject("mockDomainObject"); | ||||
|                 mockScope.model = testModel; | ||||
|                 mockScope.configuration = testConfiguration; | ||||
|                 mockScope.selection = jasmine.createSpyObj( | ||||
|                     'selection', | ||||
|                     ['select', 'get', 'selected', 'deselect'] | ||||
|                 ); | ||||
|  | ||||
|                 selectable[0] = { | ||||
|                     context: { | ||||
|                         oldItem: mockScope.domainObject | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 mockSelection = jasmine.createSpyObj("selection", [ | ||||
|                     'select', | ||||
|                     'on', | ||||
|                     'off', | ||||
|                     'get' | ||||
|                 ]); | ||||
|                 mockSelection.get.andReturn(selectable); | ||||
|                 mockOpenMCT = { | ||||
|                     selection: mockSelection | ||||
|                 }; | ||||
|  | ||||
|                 $element = $('<div></div>'); | ||||
|                 $(document).find('body').append($element); | ||||
|                 spyOn($element[0], 'click'); | ||||
|  | ||||
|                 spyOn(mockScope.domainObject, "useCapability").andCallThrough(); | ||||
|  | ||||
|                 controller = new LayoutController(mockScope); | ||||
|                 controller = new LayoutController(mockScope, $element, mockOpenMCT); | ||||
|                 spyOn(controller, "layoutPanels").andCallThrough(); | ||||
|  | ||||
|                 findWatch("selection")(mockScope.selection); | ||||
|  | ||||
|                 jasmine.Clock.useMock(); | ||||
|             }); | ||||
|  | ||||
|             afterEach(function () { | ||||
|                 $element.remove(); | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             it("listens for selection change events", function () { | ||||
|                 expect(mockOpenMCT.selection.on).toHaveBeenCalledWith( | ||||
|                     'change', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("cleans up on scope destroy", function () { | ||||
|                 expect(mockScope.$on).toHaveBeenCalledWith( | ||||
|                     '$destroy', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|  | ||||
|                 mockScope.$on.calls[0].args[1](); | ||||
|  | ||||
|                 expect(mockOpenMCT.selection.off).toHaveBeenCalledWith( | ||||
|                     'change', | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             // Model changes will indicate that panel positions | ||||
|             // may have changed, for instance. | ||||
|             it("watches for changes to composition", function () { | ||||
| @@ -320,67 +370,35 @@ define( | ||||
|                     .not.toEqual(oldStyle); | ||||
|             }); | ||||
|  | ||||
|             it("allows panels to be selected", function () { | ||||
|             it("allows objects to be selected", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|                 selectable[0].context.oldItem = childObj; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             it("prevents event bubbling while drag is in progress", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|  | ||||
|                 controller.select(mockEvent, childObj.getId()); | ||||
|                 // Do a drag | ||||
|                 controller.startDrag(childObj.getId(), [1, 1], [0, 0]); | ||||
|                 controller.continueDrag([100, 100]); | ||||
|                 controller.endDrag(); | ||||
|  | ||||
|                 // Because mouse position could cause the parent object to be selected, this should be ignored. | ||||
|                 controller.bypassSelection(mockEvent); | ||||
|  | ||||
|                 expect(mockEvent.stopPropagation).toHaveBeenCalled(); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             it("allows selection to be cleared", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|  | ||||
|                 controller.select(null, childObj.getId()); | ||||
|                 controller.clearSelection(); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBeFalsy(); | ||||
|             }); | ||||
|  | ||||
|             it("prevents clearing selection while drag is in progress", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|                 var id = childObj.getId(); | ||||
|  | ||||
|                 controller.select(mockEvent, id); | ||||
|  | ||||
|                 // Do a drag | ||||
|                 controller.startDrag(id, [1, 1], [0, 0]); | ||||
|                 controller.continueDrag([100, 100]); | ||||
|                 controller.endDrag(); | ||||
|  | ||||
|                 // Because mouse position could cause clearSelection to be called, this should be ignored. | ||||
|                 controller.clearSelection(); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBe(true); | ||||
|  | ||||
|                 // Shoud be able to clear the selection after dragging is done. | ||||
|                 // Shoud be able to select another object when dragging is done. | ||||
|                 jasmine.Clock.tick(0); | ||||
|                 controller.clearSelection(); | ||||
|                 mockEvent.stopPropagation.reset(); | ||||
|                 controller.bypassSelection(mockEvent); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBe(false); | ||||
|             }); | ||||
|  | ||||
|             it("clears selection after moving/resizing", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|                 var id = childObj.getId(); | ||||
|  | ||||
|                 controller.select(mockEvent, id); | ||||
|  | ||||
|                 // Do a drag | ||||
|                 controller.startDrag(id, [1, 1], [0, 0]); | ||||
|                 controller.continueDrag([100, 100]); | ||||
|                 controller.endDrag(); | ||||
|  | ||||
|                 jasmine.Clock.tick(0); | ||||
|                 controller.clearSelection(); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBe(false); | ||||
|                 expect(mockEvent.stopPropagation).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("shows frames by default", function () { | ||||
| @@ -398,43 +416,74 @@ define( | ||||
|             it("hides frame when selected object has frame ", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|                 controller.select(mockEvent, childObj.getId()); | ||||
|  | ||||
|                 expect(mockScope.selection.select).toHaveBeenCalled(); | ||||
|  | ||||
|                 var selectedObj = mockScope.selection.select.mostRecentCall.args[0]; | ||||
|                 selectable[0].context.oldItem = childObj; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|                 var toolbarObj = controller.getToolbar(childObj.getId(), childObj); | ||||
|  | ||||
|                 expect(controller.hasFrame(childObj)).toBe(true); | ||||
|                 expect(selectedObj.hideFrame).toBeDefined(); | ||||
|                 expect(selectedObj.hideFrame).toEqual(jasmine.any(Function)); | ||||
|                 expect(toolbarObj.hideFrame).toBeDefined(); | ||||
|                 expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("shows frame when selected object has no frame", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|  | ||||
|                 var childObj = mockCompositionObjects[1]; | ||||
|                 controller.select(mockEvent, childObj.getId()); | ||||
|  | ||||
|                 expect(mockScope.selection.select).toHaveBeenCalled(); | ||||
|  | ||||
|                 var selectedObj = mockScope.selection.select.mostRecentCall.args[0]; | ||||
|                 selectable[0].context.oldItem = childObj; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|                 var toolbarObj = controller.getToolbar(childObj.getId(), childObj); | ||||
|  | ||||
|                 expect(controller.hasFrame(childObj)).toBe(false); | ||||
|                 expect(selectedObj.showFrame).toBeDefined(); | ||||
|                 expect(selectedObj.showFrame).toEqual(jasmine.any(Function)); | ||||
|                 expect(toolbarObj.showFrame).toBeDefined(); | ||||
|                 expect(toolbarObj.showFrame).toEqual(jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             it("deselects the object that is no longer in the composition", function () { | ||||
|             it("selects the parent object when selected object is removed", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|                 controller.select(mockEvent, childObj.getId()); | ||||
|                 selectable[0].context.oldItem = childObj; | ||||
|                 mockOpenMCT.selection.on.mostRecentCall.args[1](selectable); | ||||
|  | ||||
|                 var composition = ["b", "c"]; | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](composition); | ||||
|  | ||||
|                 expect(controller.selected(childObj)).toBe(false); | ||||
|                 expect($element[0].click).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("allows objects to be drilled-in only when editing", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[0]; | ||||
|                 childObj.getCapability().inEditContext.andReturn(false); | ||||
|                 controller.drill(mockEvent, childObj); | ||||
|  | ||||
|                 expect(controller.isDrilledIn(childObj)).toBe(false); | ||||
|             }); | ||||
|  | ||||
|             it("allows objects to be drilled-in only if it has sub objects", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 var childObj = mockCompositionObjects[1]; | ||||
|                 childObj.getCapability().inEditContext.andReturn(true); | ||||
|                 controller.drill(mockEvent, childObj); | ||||
|  | ||||
|                 expect(controller.isDrilledIn(childObj)).toBe(false); | ||||
|             }); | ||||
|  | ||||
|             it("selects a newly-dropped object", function () { | ||||
|                 mockScope.$on.mostRecentCall.args[1]( | ||||
|                     mockEvent, | ||||
|                     'd', | ||||
|                     { x: 300, y: 100 } | ||||
|                 ); | ||||
|  | ||||
|                 var childObj = mockDomainObject("d"); | ||||
|                 var testElement = $("<div data-layout-id='some-id'></div>"); | ||||
|                 $element.append(testElement); | ||||
|                 spyOn(testElement[0], 'click'); | ||||
|  | ||||
|                 controller.selectIfNew('some-id', childObj); | ||||
|                 jasmine.Clock.tick(0); | ||||
|  | ||||
|                 expect(testElement[0].click).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -415,7 +415,7 @@ define( | ||||
|         PlotController.prototype.exportPNG = function () { | ||||
|             var self = this; | ||||
|             self.hideExportButtons = true; | ||||
|             self.exportImageService.exportPNG(self.$element[0], "plot.png").finally(function () { | ||||
|             self.exportImageService.exportPNG(self.$element[0], "plot.png", 'white').finally(function () { | ||||
|                 self.hideExportButtons = false; | ||||
|             }); | ||||
|         }; | ||||
| @@ -426,7 +426,7 @@ define( | ||||
|         PlotController.prototype.exportJPG = function () { | ||||
|             var self = this; | ||||
|             self.hideExportButtons = true; | ||||
|             self.exportImageService.exportJPG(self.$element[0], "plot.jpg").finally(function () { | ||||
|             self.exportImageService.exportJPG(self.$element[0], "plot.jpg", 'white').finally(function () { | ||||
|                 self.hideExportButtons = false; | ||||
|             }); | ||||
|         }; | ||||
|   | ||||
| @@ -43,7 +43,7 @@ define( | ||||
|          * @param {constant} EXPORT_IMAGE_TIMEOUT time in milliseconds before a timeout error is returned | ||||
|          * @constructor | ||||
|          */ | ||||
|         function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader) { | ||||
|         function ExportImageService($q, $timeout, $log, EXPORT_IMAGE_TIMEOUT, injHtml2Canvas, injSaveAs, injFileReader, injChangeBackgroundColor) { | ||||
|             self.$q = $q; | ||||
|             self.$timeout = $timeout; | ||||
|             self.$log = $log; | ||||
| @@ -51,6 +51,7 @@ define( | ||||
|             self.html2canvas = injHtml2Canvas || html2canvas; | ||||
|             self.saveAs = injSaveAs || saveAs; | ||||
|             self.reader = injFileReader || new FileReader(); | ||||
|             self.changeBackgroundColor = injChangeBackgroundColor || self.changeBackgroundColor; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -60,16 +61,25 @@ define( | ||||
|          * @param {string} type of image to convert the element to | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         function renderElement(element, type) { | ||||
|         function renderElement(element, type, color) { | ||||
|             var defer = self.$q.defer(), | ||||
|                 validTypes = ["png", "jpg", "jpeg"], | ||||
|                 renderTimeout; | ||||
|                 renderTimeout, | ||||
|                 originalColor; | ||||
|  | ||||
|             if (validTypes.indexOf(type) === -1) { | ||||
|                 self.$log.error("Invalid type requested. Try: (" + validTypes.join(",") + ")"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (color) { | ||||
|                 // Save color to be restored later | ||||
|                 originalColor = element.style.backgroundColor || ''; | ||||
|  | ||||
|                 // Defaulting to white so we can see the chart when printed | ||||
|                 self.changeBackgroundColor(element, color); | ||||
|             } | ||||
|  | ||||
|             renderTimeout = self.$timeout(function () { | ||||
|                 defer.reject("html2canvas timed out"); | ||||
|                 self.$log.warn("html2canvas timed out"); | ||||
| @@ -78,13 +88,15 @@ define( | ||||
|             try { | ||||
|                 self.html2canvas(element, { | ||||
|                     onrendered: function (canvas) { | ||||
|                         if (color) { | ||||
|                             self.changeBackgroundColor(element, originalColor); | ||||
|                         } | ||||
|  | ||||
|                         switch (type.toLowerCase()) { | ||||
|                             case "png": | ||||
|                                 canvas.toBlob(defer.resolve, "image/png"); | ||||
|                                 break; | ||||
|  | ||||
|                             default: | ||||
|                             case "jpg": | ||||
|                             case "jpeg": | ||||
|                                 canvas.toBlob(defer.resolve, "image/jpeg"); | ||||
|                                 break; | ||||
| @@ -96,7 +108,13 @@ define( | ||||
|                 self.$log.warn("html2canvas failed with error: " + e); | ||||
|             } | ||||
|  | ||||
|             defer.promise.finally(renderTimeout.cancel); | ||||
|             defer.promise.finally(function () { | ||||
|                 renderTimeout.cancel(); | ||||
|  | ||||
|                 if (color) { | ||||
|                     self.changeBackgroundColor(element, originalColor); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return defer.promise; | ||||
|         } | ||||
| @@ -125,14 +143,21 @@ define( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * @private | ||||
|          */ | ||||
|         self.changeBackgroundColor = function (element, color) { | ||||
|             element.style.backgroundColor = color; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Takes a screenshot of a DOM node and exports to JPG. | ||||
|          * @param {node} element to be exported | ||||
|          * @param {string} filename the exported image | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportJPG = function (element, filename) { | ||||
|             return renderElement(element, "jpeg").then(function (img) { | ||||
|         ExportImageService.prototype.exportJPG = function (element, filename, color) { | ||||
|             return renderElement(element, "jpeg", color).then(function (img) { | ||||
|                 self.saveAs(img, filename); | ||||
|             }); | ||||
|         }; | ||||
| @@ -143,8 +168,8 @@ define( | ||||
|          * @param {string} filename the exported image | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportPNG = function (element, filename) { | ||||
|             return renderElement(element, "png").then(function (img) { | ||||
|         ExportImageService.prototype.exportPNG = function (element, filename, color) { | ||||
|             return renderElement(element, "png", color).then(function (img) { | ||||
|                 self.saveAs(img, filename); | ||||
|             }); | ||||
|         }; | ||||
|   | ||||
| @@ -37,7 +37,8 @@ define( | ||||
|             mockFileReader, | ||||
|             mockExportTimeoutConstant, | ||||
|             testElement, | ||||
|             exportImageService; | ||||
|             exportImageService, | ||||
|             mockChangeBackgroundColor; | ||||
|  | ||||
|         describe("ExportImageService", function () { | ||||
|             beforeEach(function () { | ||||
| @@ -83,7 +84,9 @@ define( | ||||
|                     ["readAsDataURL", "onloadend"] | ||||
|                 ); | ||||
|                 mockExportTimeoutConstant = 0; | ||||
|                 testElement = {}; | ||||
|                 testElement = {style: {backgroundColor: 'black'}}; | ||||
|  | ||||
|                 mockChangeBackgroundColor = jasmine.createSpy('changeBackgroundColor'); | ||||
|  | ||||
|                 exportImageService = new ExportImageService( | ||||
|                     mockQ, | ||||
| @@ -92,7 +95,8 @@ define( | ||||
|                     mockExportTimeoutConstant, | ||||
|                     mockHtml2Canvas, | ||||
|                     mockSaveAs, | ||||
|                     mockFileReader | ||||
|                     mockFileReader, | ||||
|                     mockChangeBackgroundColor | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
| @@ -115,6 +119,28 @@ define( | ||||
|                 expect(mockSaveAs).toHaveBeenCalled(); | ||||
|                 expect(mockPromise.finally).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("changes background color to white and returns color back to original after snapshot, for better visibility of plot lines on print", function () { | ||||
|                 exportImageService.exportPNG(testElement, "plot.png", 'white'); | ||||
|  | ||||
|                 expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white'); | ||||
|                 expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black'); | ||||
|  | ||||
|                 exportImageService.exportJPG(testElement, "plot.jpg", 'white'); | ||||
|  | ||||
|                 expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white'); | ||||
|                 expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black'); | ||||
|             }); | ||||
|  | ||||
|             it("does not change background color when color is not specified in parameters", function () { | ||||
|                 exportImageService.exportPNG(testElement, "plot.png"); | ||||
|  | ||||
|                 expect(mockChangeBackgroundColor).not.toHaveBeenCalled(); | ||||
|  | ||||
|                 exportImageService.exportJPG(testElement, "plot.jpg"); | ||||
|  | ||||
|                 expect(mockChangeBackgroundColor).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -69,7 +69,7 @@ define([ | ||||
|                     "delegates": [ | ||||
|                         "telemetry" | ||||
|                     ], | ||||
|                     "inspector": tableInspector, | ||||
|                     "inspector": "table-options-edit", | ||||
|                     "contains": [ | ||||
|                         { | ||||
|                             "has": "telemetry" | ||||
|   | ||||
| @@ -19,7 +19,10 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div ng-controller="TableOptionsController" class="l-controls-first flex-elem grows l-inspector-part"> | ||||
|  | ||||
| <div ng-if="domainObject.getCapability('editor').inEditContext()" | ||||
|      ng-controller="TableOptionsController" | ||||
|      class="l-controls-first flex-elem grows l-inspector-part"> | ||||
|     <em class="t-inspector-part-header" title="Display properties for this object">Table Options</em> | ||||
|     <mct-form | ||||
|             ng-model="configuration.table.columns" | ||||
|   | ||||
| @@ -32,6 +32,7 @@ define( | ||||
|          */ | ||||
|         function TelemetryCollection() { | ||||
|             EventEmitter.call(this, arguments); | ||||
|             this.dupeCheck = false; | ||||
|             this.telemetry = []; | ||||
|             this.highBuffer = []; | ||||
|             this.sortField = undefined; | ||||
| @@ -161,7 +162,7 @@ define( | ||||
|                 var startIx = _.sortedIndex(array, item, this.sortField); | ||||
|                 var endIx; | ||||
|  | ||||
|                 if (startIx !== array.length) { | ||||
|                 if (this.dupeCheck && startIx !== array.length) { | ||||
|                     endIx = _.sortedLastIndex(array, item, this.sortField); | ||||
|  | ||||
|                     // Create an array of potential dupes, based on having the | ||||
| @@ -189,6 +190,7 @@ define( | ||||
|         TelemetryCollection.prototype.add = function (items) { | ||||
|             var added = items.filter(this.addOne); | ||||
|             this.emit('added', added); | ||||
|             this.dupeCheck = true; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -436,9 +436,31 @@ define( | ||||
|          * @param {Object} searchElement Object to find the insertion point for | ||||
|          */ | ||||
|         MCTTableController.prototype.findInsertionPoint = function (searchArray, searchElement) { | ||||
|             //First, use a binary search to find the correct insertion point | ||||
|             var index = this.binarySearch(searchArray, searchElement, 0, searchArray.length - 1); | ||||
|             var testIndex = index; | ||||
|             var index; | ||||
|             var testIndex; | ||||
|             var first = searchArray[0]; | ||||
|             var last = searchArray[searchArray.length - 1]; | ||||
|  | ||||
|             if (first) { | ||||
|                 first = first[this.$scope.sortColumn].text; | ||||
|             } | ||||
|             if (last) { | ||||
|                 last = last[this.$scope.sortColumn].text; | ||||
|             } | ||||
|             // Shortcut check for append/prepend | ||||
|             if (first && this.sortComparator(first, searchElement) >= 0) { | ||||
|                 index = testIndex = 0; | ||||
|             } else if (last && this.sortComparator(last, searchElement) <= 0) { | ||||
|                 index = testIndex = searchArray.length; | ||||
|             } else { | ||||
|                 // use a binary search to find the correct insertion point | ||||
|                 index = testIndex =  this.binarySearch( | ||||
|                     searchArray, | ||||
|                     searchElement, | ||||
|                     0, | ||||
|                     searchArray.length - 1 | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             //It's possible that the insertion point is a duplicate of the element to be inserted | ||||
|             var isDupe = function () { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="s-timeline l-timeline-holder split-layout vertical splitter-sm" | ||||
|      ng-click="$event.stopPropagation()" | ||||
|      ng-controller="TimelineController as timelineController"> | ||||
|  | ||||
| <mct-split-pane anchor="left" class="abs" position="pane.x"> | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -28,7 +28,8 @@ define([ | ||||
|     './selection/Selection', | ||||
|     './api/objects/object-utils', | ||||
|     './plugins/plugins', | ||||
|     './ui/ViewRegistry' | ||||
|     './ui/ViewRegistry', | ||||
|     './ui/InspectorViewRegistry' | ||||
| ], function ( | ||||
|     EventEmitter, | ||||
|     legacyRegistry, | ||||
| @@ -37,7 +38,8 @@ define([ | ||||
|     Selection, | ||||
|     objectUtils, | ||||
|     plugins, | ||||
|     ViewRegistry | ||||
|     ViewRegistry, | ||||
|     InspectorViewRegistry | ||||
| ) { | ||||
|     /** | ||||
|      * Open MCT is an extensible web application for building mission | ||||
| @@ -112,15 +114,13 @@ define([ | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the Inspector area. | ||||
|          * These views will be chosen based on selection state, so | ||||
|          * providers should be prepared to test arbitrary objects for | ||||
|          * viewability. | ||||
|          * These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @type {module:openmct.InspectorViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name inspectors | ||||
|          * @name inspectorViews | ||||
|          */ | ||||
|         this.inspectors = new ViewRegistry(); | ||||
|         this.inspectorViews = new InspectorViewRegistry(); | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in Edit Properties | ||||
| @@ -196,7 +196,6 @@ define([ | ||||
|  | ||||
|         this.Dialog = api.Dialog; | ||||
|  | ||||
|         this.on('navigation', this.selection.clear.bind(this.selection)); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
|   | ||||
							
								
								
									
										269
									
								
								src/api/composition/CompositionAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								src/api/composition/CompositionAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,269 @@ | ||||
| define([ | ||||
|     './CompositionAPI', | ||||
|     './CompositionCollection' | ||||
| ], function ( | ||||
|     CompositionAPI, | ||||
|     CompositionCollection | ||||
| ) { | ||||
|  | ||||
|     describe('The Composition API', function () { | ||||
|         var publicAPI; | ||||
|         var compositionAPI; | ||||
|         var topicService; | ||||
|         var mutationTopic; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|  | ||||
|             mutationTopic = jasmine.createSpyObj('mutationTopic', [ | ||||
|                 'listen' | ||||
|             ]); | ||||
|             topicService = jasmine.createSpy('topicService'); | ||||
|             topicService.andReturn(mutationTopic); | ||||
|             publicAPI = {}; | ||||
|             publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [ | ||||
|                 'get' | ||||
|             ]); | ||||
|             publicAPI.objects.get.andCallFake(function (identifier) { | ||||
|                 return Promise.resolve({identifier: identifier}); | ||||
|             }); | ||||
|             publicAPI.$injector = jasmine.createSpyObj('$injector', [ | ||||
|                 'get' | ||||
|             ]); | ||||
|             publicAPI.$injector.get.andReturn(topicService); | ||||
|             compositionAPI = new CompositionAPI(publicAPI); | ||||
|         }); | ||||
|  | ||||
|         it('returns falsy if an object does not support composition', function () { | ||||
|             expect(compositionAPI.get({})).toBeFalsy(); | ||||
|         }); | ||||
|  | ||||
|         describe('default composition', function () { | ||||
|             var domainObject; | ||||
|             var composition; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 domainObject = { | ||||
|                     name: 'test folder', | ||||
|                     identifier: { | ||||
|                         namespace: 'test', | ||||
|                         key: '1' | ||||
|                     }, | ||||
|                     composition: [ | ||||
|                         { | ||||
|                             namespace: 'test', | ||||
|                             key: 'a' | ||||
|                         } | ||||
|                     ] | ||||
|                 }; | ||||
|                 composition = compositionAPI.get(domainObject); | ||||
|             }); | ||||
|  | ||||
|             it('returns composition collection', function () { | ||||
|                 expect(composition).toBeDefined(); | ||||
|                 expect(composition).toEqual(jasmine.any(CompositionCollection)); | ||||
|             }); | ||||
|  | ||||
|             it('loads composition from domain object', function () { | ||||
|                 var listener = jasmine.createSpy('addListener'); | ||||
|                 var loaded = false; | ||||
|                 composition.on('add', listener); | ||||
|                 composition.load() | ||||
|                     .then(function () { | ||||
|                         loaded = true; | ||||
|                     }); | ||||
|                 waitsFor(function () { | ||||
|                     return loaded; | ||||
|                 }); | ||||
|                 runs(function () { | ||||
|                     expect(listener.calls.length).toBe(1); | ||||
|                     expect(listener).toHaveBeenCalledWith({ | ||||
|                         identifier: {namespace: 'test', key: 'a'} | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             // TODO: Implement add/removal in new default provider. | ||||
|             xit('synchronizes changes between instances', function () { | ||||
|                 var otherComposition = compositionAPI.get(domainObject); | ||||
|                 var addListener = jasmine.createSpy('addListener'); | ||||
|                 var removeListener = jasmine.createSpy('removeListener'); | ||||
|                 var otherAddListener = jasmine.createSpy('otherAddListener'); | ||||
|                 var otherRemoveListener = jasmine.createSpy('otherRemoveListener'); | ||||
|                 composition.on('add', addListener); | ||||
|                 composition.on('remove', removeListener); | ||||
|                 otherComposition.on('add', otherAddListener); | ||||
|                 otherComposition.on('remove', otherRemoveListener); | ||||
|                 var loaded = false; | ||||
|                 Promise.all([composition.load(), otherComposition.load()]) | ||||
|                     .then(function () { | ||||
|                         loaded = true; | ||||
|                     }); | ||||
|                 waitsFor(function () { | ||||
|                     return loaded; | ||||
|                 }); | ||||
|                 runs(function () { | ||||
|                     expect(addListener).toHaveBeenCalled(); | ||||
|                     expect(otherAddListener).toHaveBeenCalled(); | ||||
|                     expect(removeListener).not.toHaveBeenCalled(); | ||||
|                     expect(otherRemoveListener).not.toHaveBeenCalled(); | ||||
|  | ||||
|                     var object = addListener.mostRecentCall.args[0]; | ||||
|                     composition.remove(object); | ||||
|                     expect(removeListener).toHaveBeenCalled(); | ||||
|                     expect(otherRemoveListener).toHaveBeenCalled(); | ||||
|  | ||||
|                     addListener.reset(); | ||||
|                     otherAddListener.reset(); | ||||
|                     composition.add(object); | ||||
|                     expect(addListener).toHaveBeenCalled(); | ||||
|                     expect(otherAddListener).toHaveBeenCalled(); | ||||
|  | ||||
|                     removeListener.reset(); | ||||
|                     otherRemoveListener.reset(); | ||||
|                     otherComposition.remove(object); | ||||
|                     expect(removeListener).toHaveBeenCalled(); | ||||
|                     expect(otherRemoveListener).toHaveBeenCalled(); | ||||
|  | ||||
|                     addListener.reset(); | ||||
|                     otherAddListener.reset(); | ||||
|                     otherComposition.add(object); | ||||
|                     expect(addListener).toHaveBeenCalled(); | ||||
|                     expect(otherAddListener).toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('static custom composition', function () { | ||||
|             var customProvider; | ||||
|             var domainObject; | ||||
|             var composition; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 // A simple custom provider, returns the same composition for | ||||
|                 // all objects of a given type. | ||||
|                 customProvider = { | ||||
|                     appliesTo: function (object) { | ||||
|                         return object.type === 'custom-object-type'; | ||||
|                     }, | ||||
|                     load: function (object) { | ||||
|                         return Promise.resolve([ | ||||
|                             { | ||||
|                                 namespace: 'custom', | ||||
|                                 key: 'thing' | ||||
|                             } | ||||
|                         ]); | ||||
|                     } | ||||
|                 }; | ||||
|                 domainObject = { | ||||
|                     identifier: { | ||||
|                         namespace: 'test', | ||||
|                         key: '1' | ||||
|                     }, | ||||
|                     type: 'custom-object-type' | ||||
|                 }; | ||||
|                 compositionAPI.addProvider(customProvider); | ||||
|                 composition = compositionAPI.get(domainObject); | ||||
|             }); | ||||
|  | ||||
|             it('supports listening and loading', function () { | ||||
|                 var listener = jasmine.createSpy('addListener'); | ||||
|                 var loaded = false; | ||||
|                 composition.on('add', listener); | ||||
|                 composition.load() | ||||
|                     .then(function () { | ||||
|                         loaded = true; | ||||
|                     }); | ||||
|                 waitsFor(function () { | ||||
|                     return loaded; | ||||
|                 }); | ||||
|                 runs(function () { | ||||
|                     expect(listener.calls.length).toBe(1); | ||||
|                     expect(listener).toHaveBeenCalledWith({ | ||||
|                         identifier: {namespace: 'custom', key: 'thing'} | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('dynamic custom composition', function () { | ||||
|             var customProvider; | ||||
|             var domainObject; | ||||
|             var composition; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 // A dynamic provider, loads an empty composition and exposes | ||||
|                 // listener functions. | ||||
|                 customProvider = jasmine.createSpyObj('dynamicProvider', [ | ||||
|                     'appliesTo', | ||||
|                     'load', | ||||
|                     'on', | ||||
|                     'off' | ||||
|                 ]); | ||||
|  | ||||
|                 customProvider.appliesTo.andReturn('true'); | ||||
|                 customProvider.load.andReturn(Promise.resolve([])); | ||||
|  | ||||
|                 domainObject = { | ||||
|                     identifier: { | ||||
|                         namespace: 'test', | ||||
|                         key: '1' | ||||
|                     }, | ||||
|                     type: 'custom-object-type' | ||||
|                 }; | ||||
|                 compositionAPI.addProvider(customProvider); | ||||
|                 composition = compositionAPI.get(domainObject); | ||||
|             }); | ||||
|  | ||||
|             it('supports listening and loading', function () { | ||||
|                 var addListener = jasmine.createSpy('addListener'); | ||||
|                 var removeListener = jasmine.createSpy('removeListener'); | ||||
|                 var loaded = false; | ||||
|                 composition.on('add', addListener); | ||||
|                 composition.on('remove', removeListener); | ||||
|                 expect(customProvider.on).toHaveBeenCalledWith( | ||||
|                     domainObject, | ||||
|                     'add', | ||||
|                     jasmine.any(Function), | ||||
|                     jasmine.any(CompositionCollection) | ||||
|                 ); | ||||
|                 expect(customProvider.on).toHaveBeenCalledWith( | ||||
|                     domainObject, | ||||
|                     'remove', | ||||
|                     jasmine.any(Function), | ||||
|                     jasmine.any(CompositionCollection) | ||||
|                 ); | ||||
|                 var add = customProvider.on.calls[0].args[2]; | ||||
|                 var remove = customProvider.on.calls[1].args[2]; | ||||
|                 composition.load() | ||||
|                     .then(function () { | ||||
|                         loaded = true; | ||||
|                     }); | ||||
|                 waitsFor(function () { | ||||
|                     return loaded; | ||||
|                 }); | ||||
|                 runs(function () { | ||||
|                     expect(addListener).not.toHaveBeenCalled(); | ||||
|                     expect(removeListener).not.toHaveBeenCalled(); | ||||
|                     add({namespace: 'custom', key: 'thing'}); | ||||
|                 }); | ||||
|                 waitsFor(function () { | ||||
|                     return addListener.calls.length > 0; | ||||
|                 }); | ||||
|                 runs(function () { | ||||
|                     expect(addListener).toHaveBeenCalledWith({ | ||||
|                         identifier: {namespace: 'custom', key: 'thing'} | ||||
|                     }); | ||||
|                     remove(addListener.mostRecentCall.args[0]); | ||||
|                 }); | ||||
|                 waitsFor(function () { | ||||
|                     return removeListener.calls.length > 0; | ||||
|                 }); | ||||
|                 runs(function () { | ||||
|                     expect(removeListener).toHaveBeenCalledWith({ | ||||
|                         identifier: {namespace: 'custom', key: 'thing'} | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -76,20 +76,22 @@ define([ | ||||
|             throw new Error('Event not supported by composition: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (event === 'add') { | ||||
|             this.provider.on( | ||||
|                 this.domainObject, | ||||
|                 'add', | ||||
|                 this.onProviderAdd, | ||||
|                 this | ||||
|             ); | ||||
|         } if (event === 'remove') { | ||||
|             this.provider.on( | ||||
|                 this.domainObject, | ||||
|                 'remove', | ||||
|                 this.onProviderRemove, | ||||
|                 this | ||||
|             ); | ||||
|         if (this.provider.on && this.provider.off) { | ||||
|             if (event === 'add') { | ||||
|                 this.provider.on( | ||||
|                     this.domainObject, | ||||
|                     'add', | ||||
|                     this.onProviderAdd, | ||||
|                     this | ||||
|                 ); | ||||
|             } if (event === 'remove') { | ||||
|                 this.provider.on( | ||||
|                     this.domainObject, | ||||
|                     'remove', | ||||
|                     this.onProviderRemove, | ||||
|                     this | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.listeners[event].push({ | ||||
| @@ -124,20 +126,22 @@ define([ | ||||
|         if (this.listeners[event].length === 0) { | ||||
|             // Remove provider listener if this is the last callback to | ||||
|             // be removed. | ||||
|             if (event === 'add') { | ||||
|                 this.provider.off( | ||||
|                     this.domainObject, | ||||
|                     'add', | ||||
|                     this.onProviderAdd, | ||||
|                     this | ||||
|                 ); | ||||
|             } else if (event === 'remove') { | ||||
|                 this.provider.off( | ||||
|                     this.domainObject, | ||||
|                     'remove', | ||||
|                     this.onProviderRemove, | ||||
|                     this | ||||
|                 ); | ||||
|             if (this.provider.off && this.provider.on) { | ||||
|                 if (event === 'add') { | ||||
|                     this.provider.off( | ||||
|                         this.domainObject, | ||||
|                         'add', | ||||
|                         this.onProviderAdd, | ||||
|                         this | ||||
|                     ); | ||||
|                 } else if (event === 'remove') { | ||||
|                     this.provider.off( | ||||
|                         this.domainObject, | ||||
|                         'remove', | ||||
|                         this.onProviderRemove, | ||||
|                         this | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|   | ||||
| @@ -48,6 +48,7 @@ define([ | ||||
|         this.unlisteners.forEach(function (unlisten) { | ||||
|             unlisten(); | ||||
|         }); | ||||
|         this.unlisteners = []; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -41,8 +41,6 @@ define([ | ||||
|         }; | ||||
|  | ||||
|         this.valueMetadata = valueMetadata; | ||||
|         this.parseCache = new WeakMap(); | ||||
|         this.formatCache = new WeakMap(); | ||||
|         try { | ||||
|             this.formatter = formatService | ||||
|                 .getFormat(valueMetadata.format, valueMetadata); | ||||
| @@ -72,26 +70,14 @@ define([ | ||||
|  | ||||
|     TelemetryValueFormatter.prototype.parse = function (datum) { | ||||
|         if (_.isObject(datum)) { | ||||
|             if (!this.parseCache.has(datum)) { | ||||
|                 this.parseCache.set( | ||||
|                     datum, | ||||
|                     this.formatter.parse(datum[this.valueMetadata.source]) | ||||
|                 ); | ||||
|             } | ||||
|             return this.parseCache.get(datum); | ||||
|             return this.formatter.parse(datum[this.valueMetadata.source]); | ||||
|         } | ||||
|         return this.formatter.parse(datum); | ||||
|     }; | ||||
|  | ||||
|     TelemetryValueFormatter.prototype.format = function (datum) { | ||||
|         if (_.isObject(datum)) { | ||||
|             if (!this.formatCache.has(datum)) { | ||||
|                 this.formatCache.set( | ||||
|                     datum, | ||||
|                     this.formatter.format(datum[this.valueMetadata.source]) | ||||
|                 ); | ||||
|             } | ||||
|             return this.formatCache.get(datum); | ||||
|             return this.formatter.format(datum[this.valueMetadata.source]); | ||||
|         } | ||||
|         return this.formatter.format(datum); | ||||
|     }; | ||||
|   | ||||
							
								
								
									
										34
									
								
								src/plugins/autoflow/AutoflowTabularConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/plugins/autoflow/AutoflowTabularConstants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|     /** | ||||
|      * Constant values used by the Autoflow Tabular View. | ||||
|      */ | ||||
|     return { | ||||
|         ROW_HEIGHT: 16, | ||||
|         SLIDER_HEIGHT: 10, | ||||
|         INITIAL_COLUMN_WIDTH: 225, | ||||
|         MAX_COLUMN_WIDTH: 525, | ||||
|         COLUMN_WIDTH_STEP: 25 | ||||
|     }; | ||||
| }); | ||||
							
								
								
									
										121
									
								
								src/plugins/autoflow/AutoflowTabularController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/plugins/autoflow/AutoflowTabularController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './AutoflowTabularRowController' | ||||
| ], function (AutoflowTabularRowController) { | ||||
|     /** | ||||
|      * Controller for an Autoflow Tabular View. Subscribes to telemetry | ||||
|      * associated with children of the domain object and passes that | ||||
|      * information on to the view. | ||||
|      * | ||||
|      * @param {DomainObject} domainObject the object being viewed | ||||
|      * @param {*} data the view data | ||||
|      * @param openmct a reference to the openmct application | ||||
|      */ | ||||
|     function AutoflowTabularController(domainObject, data, openmct) { | ||||
|         this.composition = openmct.composition.get(domainObject); | ||||
|         this.data = data; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.rows = {}; | ||||
|         this.controllers = {}; | ||||
|  | ||||
|         this.addRow = this.addRow.bind(this); | ||||
|         this.removeRow = this.removeRow.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the "Last Updated" value to be displayed. | ||||
|      * @param {String} value the value to display | ||||
|      * @private | ||||
|      */ | ||||
|     AutoflowTabularController.prototype.trackLastUpdated = function (value) { | ||||
|         this.data.updated = value; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Respond to an `add` event from composition by adding a new row. | ||||
|      * @private | ||||
|      */ | ||||
|     AutoflowTabularController.prototype.addRow = function (childObject) { | ||||
|         var identifier = childObject.identifier; | ||||
|         var id = [identifier.namespace, identifier.key].join(":"); | ||||
|  | ||||
|         if (!this.rows[id]) { | ||||
|             this.rows[id] = { | ||||
|                 classes: "", | ||||
|                 name: childObject.name, | ||||
|                 value: undefined | ||||
|             }; | ||||
|             this.controllers[id] = new AutoflowTabularRowController( | ||||
|                 childObject, | ||||
|                 this.rows[id], | ||||
|                 this.openmct, | ||||
|                 this.trackLastUpdated.bind(this) | ||||
|             ); | ||||
|             this.controllers[id].activate(); | ||||
|             this.data.items.push(this.rows[id]); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Respond to an `remove` event from composition by removing any | ||||
|      * related row. | ||||
|      * @private | ||||
|      */ | ||||
|     AutoflowTabularController.prototype.removeRow = function (identifier) { | ||||
|         var id = [identifier.namespace, identifier.key].join(":"); | ||||
|  | ||||
|         if (this.rows[id]) { | ||||
|             this.data.items = this.data.items.filter(function (item) { | ||||
|                 return item !== this.rows[id]; | ||||
|             }.bind(this)); | ||||
|             this.controllers[id].destroy(); | ||||
|             delete this.controllers[id]; | ||||
|             delete this.rows[id]; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Activate this controller; begin listening for changes. | ||||
|      */ | ||||
|     AutoflowTabularController.prototype.activate = function () { | ||||
|         this.composition.on('add', this.addRow); | ||||
|         this.composition.on('remove', this.removeRow); | ||||
|         this.composition.load(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Destroy this controller; detach any associated resources. | ||||
|      */ | ||||
|     AutoflowTabularController.prototype.destroy = function () { | ||||
|         Object.keys(this.controllers).forEach(function (id) { | ||||
|             this.controllers[id].destroy(); | ||||
|         }.bind(this)); | ||||
|         this.controllers = {}; | ||||
|         this.composition.off('add', this.addRow); | ||||
|         this.composition.off('remove', this.removeRow); | ||||
|     }; | ||||
|  | ||||
|     return AutoflowTabularController; | ||||
| }); | ||||
							
								
								
									
										60
									
								
								src/plugins/autoflow/AutoflowTabularPlugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/plugins/autoflow/AutoflowTabularPlugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './AutoflowTabularView' | ||||
| ], function ( | ||||
|     AutoflowTabularView | ||||
| ) { | ||||
|     /** | ||||
|      * This plugin provides an Autoflow Tabular View for domain objects | ||||
|      * in Open MCT. | ||||
|      * | ||||
|      * @param {Object} options | ||||
|      * @param {String} [options.type] the domain object type for which | ||||
|      *        this view should be available; if omitted, this view will | ||||
|      *        be available for all objects | ||||
|      */ | ||||
|     return function (options) { | ||||
|         return function (openmct) { | ||||
|             var views = (openmct.mainViews || openmct.objectViews); | ||||
|  | ||||
|             views.addProvider({ | ||||
|                 name: "Autoflow Tabular", | ||||
|                 key: "autoflow", | ||||
|                 cssClass: "icon-packet", | ||||
|                 description: "A tabular view of packet contents.", | ||||
|                 canView: function (d) { | ||||
|                     return !options || (options.type === d.type); | ||||
|                 }, | ||||
|                 view: function (domainObject) { | ||||
|                     return new AutoflowTabularView( | ||||
|                         domainObject, | ||||
|                         openmct, | ||||
|                         document | ||||
|                     ); | ||||
|                 } | ||||
|             }); | ||||
|         }; | ||||
|     }; | ||||
| }); | ||||
|  | ||||
							
								
								
									
										319
									
								
								src/plugins/autoflow/AutoflowTabularPluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								src/plugins/autoflow/AutoflowTabularPluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,319 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './AutoflowTabularPlugin', | ||||
|     './AutoflowTabularConstants', | ||||
|     '../../MCT', | ||||
|     'zepto' | ||||
| ], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $) { | ||||
|     describe("AutoflowTabularPlugin", function () { | ||||
|         var testType; | ||||
|         var testObject; | ||||
|         var mockmct; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             testType = "some-type"; | ||||
|             testObject = { type: testType }; | ||||
|             mockmct = new MCT(); | ||||
|             spyOn(mockmct.composition, 'get'); | ||||
|             spyOn(mockmct.objectViews, 'addProvider'); | ||||
|             spyOn(mockmct.telemetry, 'getMetadata'); | ||||
|             spyOn(mockmct.telemetry, 'getValueFormatter'); | ||||
|             spyOn(mockmct.telemetry, 'limitEvaluator'); | ||||
|             spyOn(mockmct.telemetry, 'request'); | ||||
|             spyOn(mockmct.telemetry, 'subscribe'); | ||||
|  | ||||
|             var plugin = new AutoflowTabularPlugin({ type: testType }); | ||||
|             plugin(mockmct); | ||||
|         }); | ||||
|  | ||||
|         it("installs a view provider", function () { | ||||
|             expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         describe("installs a view provider which", function () { | ||||
|             var provider; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 provider = | ||||
|                     mockmct.objectViews.addProvider.mostRecentCall.args[0]; | ||||
|             }); | ||||
|  | ||||
|             it("applies its view to the type from options", function () { | ||||
|                 expect(provider.canView(testObject)).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             it("does not apply to other types", function () { | ||||
|                 expect(provider.canView({ type: 'foo' })).toBe(false); | ||||
|             }); | ||||
|  | ||||
|             describe("provides a view which", function () { | ||||
|                 var testKeys; | ||||
|                 var testChildren; | ||||
|                 var testContainer; | ||||
|                 var testHistories; | ||||
|                 var mockComposition; | ||||
|                 var mockMetadata; | ||||
|                 var mockEvaluator; | ||||
|                 var mockUnsubscribes; | ||||
|                 var callbacks; | ||||
|                 var view; | ||||
|  | ||||
|                 function waitsForChange() { | ||||
|                     var callback = jasmine.createSpy('callback'); | ||||
|                     window.requestAnimationFrame(callback); | ||||
|                     waitsFor(function () { | ||||
|                         return callback.calls.length > 0; | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 function emitEvent(mockEmitter, type, event) { | ||||
|                     mockEmitter.on.calls.forEach(function (call) { | ||||
|                         if (call.args[0] === type) { | ||||
|                             call.args[1](event); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     callbacks = {}; | ||||
|  | ||||
|                     testObject = { type: 'some-type' }; | ||||
|                     testKeys = ['abc', 'def', 'xyz']; | ||||
|                     testChildren = testKeys.map(function (key) { | ||||
|                         return { | ||||
|                             identifier: { namespace: "test", key: key }, | ||||
|                             name: "Object " + key | ||||
|                         }; | ||||
|                     }); | ||||
|                     testContainer = $('<div>')[0]; | ||||
|                     testHistories = testKeys.reduce(function (histories, key, index) { | ||||
|                         histories[key] = { key: key, range: index + 10, domain: key + index }; | ||||
|                         return histories; | ||||
|                     }, {}); | ||||
|  | ||||
|                     mockComposition = | ||||
|                         jasmine.createSpyObj('composition', ['load', 'on', 'off']); | ||||
|                     mockMetadata = | ||||
|                         jasmine.createSpyObj('metadata', ['valuesForHints']); | ||||
|  | ||||
|                     mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); | ||||
|                     mockUnsubscribes = testKeys.reduce(function (map, key) { | ||||
|                         map[key] = jasmine.createSpy('unsubscribe-' + key); | ||||
|                         return map; | ||||
|                     }, {}); | ||||
|  | ||||
|                     mockmct.composition.get.andReturn(mockComposition); | ||||
|                     mockComposition.load.andCallFake(function () { | ||||
|                         testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); | ||||
|                         return Promise.resolve(testChildren); | ||||
|                     }); | ||||
|  | ||||
|                     mockmct.telemetry.getMetadata.andReturn(mockMetadata); | ||||
|                     mockmct.telemetry.getValueFormatter.andCallFake(function (metadatum) { | ||||
|                         var mockFormatter = jasmine.createSpyObj('formatter', ['format']); | ||||
|                         mockFormatter.format.andCallFake(function (datum) { | ||||
|                             return datum[metadatum.hint]; | ||||
|                         }); | ||||
|                         return mockFormatter; | ||||
|                     }); | ||||
|                     mockmct.telemetry.limitEvaluator.andReturn(mockEvaluator); | ||||
|                     mockmct.telemetry.subscribe.andCallFake(function (obj, callback) { | ||||
|                         var key = obj.identifier.key; | ||||
|                         callbacks[key] = callback; | ||||
|                         return mockUnsubscribes[key]; | ||||
|                     }); | ||||
|                     mockmct.telemetry.request.andCallFake(function (obj, request) { | ||||
|                         var key = obj.identifier.key; | ||||
|                         return Promise.resolve([testHistories[key]]); | ||||
|                     }); | ||||
|                     mockMetadata.valuesForHints.andCallFake(function (hints) { | ||||
|                         return [{ hint: hints[0] }]; | ||||
|                     }); | ||||
|  | ||||
|                     view = provider.view(testObject); | ||||
|                     view.show(testContainer); | ||||
|  | ||||
|                     waitsForChange(); | ||||
|                 }); | ||||
|  | ||||
|                 it("populates its container", function () { | ||||
|                     expect(testContainer.children.length > 0).toBe(true); | ||||
|                 }); | ||||
|  | ||||
|                 describe("when rows have been populated", function () { | ||||
|                     function rowsMatch() { | ||||
|                         var rows = $(testContainer).find(".l-autoflow-row").length; | ||||
|                         return rows === testChildren.length; | ||||
|                     } | ||||
|  | ||||
|                     it("shows one row per child object", function () { | ||||
|                         waitsFor(rowsMatch); | ||||
|                     }); | ||||
|  | ||||
|                     it("adds rows on composition change", function () { | ||||
|                         var child = { | ||||
|                             identifier: { namespace: "test", key: "123" }, | ||||
|                             name: "Object 123" | ||||
|                         }; | ||||
|                         testChildren.push(child); | ||||
|                         emitEvent(mockComposition, 'add', child); | ||||
|                         waitsFor(rowsMatch); | ||||
|                     }); | ||||
|  | ||||
|                     it("removes rows on composition change", function () { | ||||
|                         var child = testChildren.pop(); | ||||
|                         emitEvent(mockComposition, 'remove', child.identifier); | ||||
|                         waitsFor(rowsMatch); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("removes subscriptions when destroyed", function () { | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); | ||||
|                     }); | ||||
|                     view.destroy(); | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(mockUnsubscribes[key]).toHaveBeenCalled(); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("provides a button to change column width", function () { | ||||
|                     var initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; | ||||
|                     var nextWidth = | ||||
|                         initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; | ||||
|  | ||||
|                     expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                         .toEqual(initialWidth + 'px'); | ||||
|  | ||||
|                     $(testContainer).find('.change-column-width').click(); | ||||
|  | ||||
|                     waitsFor(function () { | ||||
|                         var width = $(testContainer).find('.l-autoflow-col').css('width'); | ||||
|                         return width !== initialWidth + 'px'; | ||||
|                     }); | ||||
|  | ||||
|                     runs(function () { | ||||
|                         expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                             .toEqual(nextWidth + 'px'); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("subscribes to all child objects", function () { | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(callbacks[key]).toEqual(jasmine.any(Function)); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("displays historical telemetry", function () { | ||||
|                     waitsFor(function () { | ||||
|                         return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== ""; | ||||
|                     }); | ||||
|  | ||||
|                     runs(function () { | ||||
|                         testKeys.forEach(function (key, index) { | ||||
|                             var datum = testHistories[key]; | ||||
|                             var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.text()).toEqual(String(datum.range)); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("displays incoming telemetry", function () { | ||||
|                     var testData = testKeys.map(function (key, index) { | ||||
|                         return { key: key, range: index * 100, domain: key + index }; | ||||
|                     }); | ||||
|  | ||||
|                     testData.forEach(function (datum) { | ||||
|                         callbacks[datum.key](datum); | ||||
|                     }); | ||||
|  | ||||
|                     waitsForChange(); | ||||
|  | ||||
|                     runs(function () { | ||||
|                         testData.forEach(function (datum, index) { | ||||
|                             var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.text()).toEqual(String(datum.range)); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates classes for limit violations", function () { | ||||
|                     var testClass = "some-limit-violation"; | ||||
|                     mockEvaluator.evaluate.andReturn({ cssClass: testClass }); | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         callbacks[key]({ range: 'foo', domain: 'bar' }); | ||||
|                     }); | ||||
|  | ||||
|                     waitsForChange(); | ||||
|  | ||||
|                     runs(function () { | ||||
|                         testKeys.forEach(function (datum, index) { | ||||
|                             var $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.hasClass(testClass)).toBe(true); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("automatically flows to new columns", function () { | ||||
|                     var rowHeight = AutoflowTabularConstants.ROW_HEIGHT; | ||||
|                     var sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; | ||||
|                     var count = testKeys.length; | ||||
|                     var $container = $(testContainer); | ||||
|  | ||||
|                     function columnsHaveAutoflowed() { | ||||
|                         var itemsHeight = $container.find('.l-autoflow-items').height(); | ||||
|                         var availableHeight = itemsHeight - sliderHeight; | ||||
|                         var availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1); | ||||
|                         var columns = Math.ceil(count / availableRows); | ||||
|                         return $container.find('.l-autoflow-col').length === columns; | ||||
|                     } | ||||
|  | ||||
|                     $container.find('.abs').css({ | ||||
|                         position: 'absolute', | ||||
|                         left: '0px', | ||||
|                         right: '0px', | ||||
|                         top: '0px', | ||||
|                         bottom: '0px' | ||||
|                     }); | ||||
|                     $container.css({ position: 'absolute' }); | ||||
|  | ||||
|                     runs($container.appendTo.bind($container, document.body)); | ||||
|                     for (var height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { | ||||
|                         runs($container.css.bind($container, 'height', height + 'px')); | ||||
|                         waitsFor(columnsHaveAutoflowed); | ||||
|                     } | ||||
|                     runs($container.remove.bind($container)); | ||||
|                 }); | ||||
|  | ||||
|                 it("loads composition exactly once", function () { | ||||
|                     var testObj = testChildren.pop(); | ||||
|                     emitEvent(mockComposition, 'remove', testObj.identifier); | ||||
|                     testChildren.push(testObj); | ||||
|                     emitEvent(mockComposition, 'add', testObj); | ||||
|                     expect(mockComposition.load.calls.length).toEqual(1); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										94
									
								
								src/plugins/autoflow/AutoflowTabularRowController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/plugins/autoflow/AutoflowTabularRowController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|     /** | ||||
|      * Controller for individual rows of an Autoflow Tabular View. | ||||
|      * Subscribes to telemetry and updates row data. | ||||
|      * | ||||
|      * @param {DomainObject} domainObject the object being viewed | ||||
|      * @param {*} data the view data | ||||
|      * @param openmct a reference to the openmct application | ||||
|      * @param {Function} callback a callback to invoke with "last updated" timestamps | ||||
|      */ | ||||
|     function AutoflowTabularRowController(domainObject, data, openmct, callback) { | ||||
|         this.domainObject = domainObject; | ||||
|         this.data = data; | ||||
|         this.openmct = openmct; | ||||
|         this.callback = callback; | ||||
|  | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.ranges = this.metadata.valuesForHints(['range']); | ||||
|         this.domains = this.metadata.valuesForHints(['domain']); | ||||
|         this.rangeFormatter = | ||||
|             this.openmct.telemetry.getValueFormatter(this.ranges[0]); | ||||
|         this.domainFormatter = | ||||
|             this.openmct.telemetry.getValueFormatter(this.domains[0]); | ||||
|         this.evaluator = | ||||
|             this.openmct.telemetry.limitEvaluator(this.domainObject); | ||||
|  | ||||
|         this.initialized = false; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update row to reflect incoming telemetry data. | ||||
|      * @private | ||||
|      */ | ||||
|     AutoflowTabularRowController.prototype.updateRowData = function (datum) { | ||||
|         var violations = this.evaluator.evaluate(datum, this.ranges[0]); | ||||
|  | ||||
|         this.initialized = true; | ||||
|         this.data.classes = violations ? violations.cssClass : ""; | ||||
|         this.data.value = this.rangeFormatter.format(datum); | ||||
|         this.callback(this.domainFormatter.format(datum)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Activate this controller; begin listening for changes. | ||||
|      */ | ||||
|     AutoflowTabularRowController.prototype.activate = function () { | ||||
|         this.unsubscribe = this.openmct.telemetry.subscribe( | ||||
|             this.domainObject, | ||||
|             this.updateRowData.bind(this) | ||||
|         ); | ||||
|  | ||||
|         this.openmct.telemetry.request( | ||||
|             this.domainObject, | ||||
|             { size: 1 } | ||||
|         ).then(function (history) { | ||||
|             if (!this.initialized && history.length > 0) { | ||||
|                 this.updateRowData(history[history.length - 1]); | ||||
|             } | ||||
|         }.bind(this)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Destroy this controller; detach any associated resources. | ||||
|      */ | ||||
|     AutoflowTabularRowController.prototype.destroy = function () { | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return AutoflowTabularRowController; | ||||
| }); | ||||
							
								
								
									
										125
									
								
								src/plugins/autoflow/AutoflowTabularView.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/plugins/autoflow/AutoflowTabularView.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     './AutoflowTabularController', | ||||
|     './AutoflowTabularConstants', | ||||
|     '../../ui/VueView', | ||||
|     'text!./autoflow-tabular.html' | ||||
| ], function ( | ||||
|     AutoflowTabularController, | ||||
|     AutoflowTabularConstants, | ||||
|     VueView, | ||||
|     autoflowTemplate | ||||
| ) { | ||||
|     var ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT; | ||||
|     var SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT; | ||||
|     var INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; | ||||
|     var MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH; | ||||
|     var COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP; | ||||
|  | ||||
|     /** | ||||
|      * Implements the Autoflow Tabular view of a domain object. | ||||
|      */ | ||||
|     function AutoflowTabularView(domainObject, openmct) { | ||||
|         var data = { | ||||
|             items: [], | ||||
|             columns: [], | ||||
|             width: INITIAL_COLUMN_WIDTH, | ||||
|             filter: "", | ||||
|             updated: "No updates", | ||||
|             rowCount: 1 | ||||
|         }; | ||||
|         var controller = | ||||
|             new AutoflowTabularController(domainObject, data, openmct); | ||||
|         var interval; | ||||
|  | ||||
|         VueView.call(this, { | ||||
|             data: data, | ||||
|             methods: { | ||||
|                 increaseColumnWidth: function () { | ||||
|                     data.width += COLUMN_WIDTH_STEP; | ||||
|                     data.width = data.width > MAX_COLUMN_WIDTH ? | ||||
|                             INITIAL_COLUMN_WIDTH : data.width; | ||||
|                 }, | ||||
|                 reflow: function () { | ||||
|                     var column = []; | ||||
|                     var index = 0; | ||||
|                     var filteredItems = | ||||
|                         data.items.filter(function (item) { | ||||
|                             return item.name.toLowerCase() | ||||
|                                 .indexOf(data.filter.toLowerCase()) !== -1; | ||||
|                         }); | ||||
|  | ||||
|                     data.columns = []; | ||||
|  | ||||
|                     while (index < filteredItems.length) { | ||||
|                         if (column.length >= data.rowCount) { | ||||
|                             data.columns.push(column); | ||||
|                             column = []; | ||||
|                         } | ||||
|  | ||||
|                         column.push(filteredItems[index]); | ||||
|                         index += 1; | ||||
|                     } | ||||
|  | ||||
|                     if (column.length > 0) { | ||||
|                         data.columns.push(column); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             watch: { | ||||
|                 filter: 'reflow', | ||||
|                 items: 'reflow', | ||||
|                 rowCount: 'reflow' | ||||
|             }, | ||||
|             template: autoflowTemplate, | ||||
|             destroyed: function () { | ||||
|                 controller.destroy(); | ||||
|  | ||||
|                 if (interval) { | ||||
|                     clearInterval(interval); | ||||
|                     interval = undefined; | ||||
|                 } | ||||
|             }, | ||||
|             mounted: function () { | ||||
|                 controller.activate(); | ||||
|  | ||||
|                 var updateRowHeight = function () { | ||||
|                     var tabularArea = this.$refs.autoflowItems; | ||||
|                     var height = tabularArea ? tabularArea.clientHeight : 0; | ||||
|                     var available = height - SLIDER_HEIGHT; | ||||
|                     var rows = Math.max(1, Math.floor(available / ROW_HEIGHT)); | ||||
|                     data.rowCount = rows; | ||||
|                 }.bind(this); | ||||
|  | ||||
|                 interval = setInterval(updateRowHeight, 50); | ||||
|                 this.$nextTick(updateRowHeight); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     AutoflowTabularView.prototype = Object.create(VueView.prototype); | ||||
|  | ||||
|     return AutoflowTabularView; | ||||
| }); | ||||
|  | ||||
							
								
								
									
										42
									
								
								src/plugins/autoflow/autoflow-tabular.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/plugins/autoflow/autoflow-tabular.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  "License"); you may not use this file except in compliance with the License. | ||||
|  You may obtain a copy of the License at | ||||
|  http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  | ||||
|  Unless required by applicable law or agreed to in writing, software | ||||
|  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  License for the specific language governing permissions and limitations | ||||
|  under the License. | ||||
|  | ||||
|  Open MCT includes source code licensed under additional open source | ||||
|  licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="items-holder abs contents autoflow obj-value-format"> | ||||
|     <div class="abs l-flex-row holder t-autoflow-header l-autoflow-header"> | ||||
|         <span class="t-filter l-filter"> | ||||
|             <input type="search" class="t-filter-input" v-model="filter"/> | ||||
|             <a v-if="filter !== ''" v-on:click="filter = ''" class="clear-icon icon-x-in-circle"></a> | ||||
|         </span> | ||||
|  | ||||
|         <div class="flex-elem grows t-last-update" title="Last Update">{{updated}}</div> | ||||
|         <a title="Change column width" | ||||
|            v-on:click="increaseColumnWidth()" | ||||
|            class="s-button flex-elem icon-arrows-right-left change-column-width"></a> | ||||
|     </div> | ||||
|     <div class="abs t-autoflow-items l-autoflow-items" ref="autoflowItems"> | ||||
|         <ul v-for="column in columns" class="l-autoflow-col" :style="{ width: width + 'px' }"> | ||||
|             <li v-for="row in column" class="l-autoflow-row" > | ||||
|                 <span :title="row.value" :data-value="row.value" :class="'l-autoflow-item r l-obj-val-format ' + row.classes">{{row.value}}</span> | ||||
|                 <span :title="row.name" class="l-autoflow-item l">{{row.name}}</span> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -24,12 +24,13 @@ define([ | ||||
|     'lodash', | ||||
|     './utcTimeSystem/plugin', | ||||
|     '../../example/generator/plugin', | ||||
|     '../../platform/features/autoflow/plugin', | ||||
|     './autoflow/AutoflowTabularPlugin', | ||||
|     './timeConductor/plugin', | ||||
|     '../../example/imagery/plugin', | ||||
|     '../../platform/import-export/bundle', | ||||
|     './summaryWidget/plugin', | ||||
|     './URLIndicatorPlugin/URLIndicatorPlugin' | ||||
|     './URLIndicatorPlugin/URLIndicatorPlugin', | ||||
|     './telemetryMean/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
|     UTCTimeSystem, | ||||
| @@ -39,7 +40,8 @@ define([ | ||||
|     ExampleImagery, | ||||
|     ImportExport, | ||||
|     SummaryWidget, | ||||
|     URLIndicatorPlugin | ||||
|     URLIndicatorPlugin, | ||||
|     TelemetryMean | ||||
| ) { | ||||
|     var bundleMap = { | ||||
|         CouchDB: 'platform/persistence/couch', | ||||
| @@ -124,8 +126,8 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     plugins.ExampleImagery = ExampleImagery; | ||||
|  | ||||
|     plugins.SummaryWidget = SummaryWidget; | ||||
|     plugins.TelemetryMean = TelemetryMean; | ||||
|     plugins.URLIndicatorPlugin = URLIndicatorPlugin; | ||||
|  | ||||
|     return plugins; | ||||
|   | ||||
| @@ -44,11 +44,7 @@ define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (S | ||||
|             return { | ||||
|                 name: 'Widget View', | ||||
|                 view: function (domainObject) { | ||||
|                     var summaryWidget = new SummaryWidget(domainObject, openmct); | ||||
|                     return { | ||||
|                         show: summaryWidget.show, | ||||
|                         destroy: summaryWidget.destroy | ||||
|                     }; | ||||
|                     return new SummaryWidget(domainObject, openmct); | ||||
|                 }, | ||||
|                 canView: function (domainObject) { | ||||
|                     return (domainObject.type === 'summary-widget'); | ||||
| @@ -62,7 +58,8 @@ define(['./src/SummaryWidget', './SummaryWidgetsCompositionPolicy'], function (S | ||||
|             openmct.types.addType('summary-widget', widgetType); | ||||
|             openmct.objectViews.addProvider(initViewProvider(openmct)); | ||||
|             openmct.legacyExtension('policies', {category: 'composition', | ||||
|                 implementation: SummaryWidgetsCompositionPolicy, depends: ['openmct']}); | ||||
|                 implementation: SummaryWidgetsCompositionPolicy, depends: ['openmct'] | ||||
|             }); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -206,6 +206,17 @@ define([], function () { | ||||
|                 getDescription: function () { | ||||
|                     return ' is undefined'; | ||||
|                 } | ||||
|             }, | ||||
|             isDefined: { | ||||
|                 operation: function (input) { | ||||
|                     return typeof input[0] !== 'undefined'; | ||||
|                 }, | ||||
|                 text: 'is defined', | ||||
|                 appliesTo: ['string', 'number'], | ||||
|                 inputCount: 0, | ||||
|                 getDescription: function () { | ||||
|                     return ' is defined'; | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| @@ -304,9 +315,12 @@ define([], function () { | ||||
|         op = this.operations[operation] && this.operations[operation].operation; | ||||
|         input = telemetryValue && telemetryValue.concat(values); | ||||
|         validator = op && this.inputValidators[this.operations[operation].appliesTo[0]]; | ||||
|  | ||||
|         if (op && input && validator) { | ||||
|             return validator(input) && op(input); | ||||
|             if (this.operations[operation].appliesTo.length === 2) { | ||||
|                 return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input); | ||||
|             } else { | ||||
|                 return validator(input) && op(input); | ||||
|             } | ||||
|         } else { | ||||
|             throw new Error('Malformed condition'); | ||||
|         } | ||||
|   | ||||
| @@ -66,6 +66,8 @@ define ([ | ||||
|     ConditionManager.prototype.on = function (event, callback, context) { | ||||
|         if (this.supportedCallbacks.includes(event)) { | ||||
|             this.eventEmitter.on(event, callback, context || this); | ||||
|         } else { | ||||
|             throw event + " is not a supported callback. Supported callbacks are " + this.supportedCallbacks; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
| @@ -127,7 +129,7 @@ define ([ | ||||
|             self = this; | ||||
|  | ||||
|         self.telemetryTypesById[object.identifier.key] = {}; | ||||
|         return telemetryAPI.request(object, {}).then(function (telemetry) { | ||||
|         return telemetryAPI.request(object, {size: 1, strategy: 'latest'}).then(function (telemetry) { | ||||
|             Object.entries(telemetry[telemetry.length - 1]).forEach(function (telem) { | ||||
|                 key = telem[0]; | ||||
|                 type = typeof telem[1]; | ||||
|   | ||||
| @@ -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(); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
| @@ -370,6 +376,7 @@ define([ | ||||
|         this.domainObject.configuration.ruleConfigById = ruleConfigById; | ||||
|         this.updateDomainObject(); | ||||
|         this.refreshConditions(); | ||||
|         this.generateDescription(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
| @@ -412,7 +419,6 @@ define([ | ||||
|             self.conditions[0].hideButtons(); | ||||
|         } | ||||
|  | ||||
|         self.generateDescription(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
| @@ -430,6 +436,7 @@ define([ | ||||
|         this.domainObject.configuration.ruleConfigById[this.config.id] = this.config; | ||||
|         this.updateDomainObject(); | ||||
|         this.refreshConditions(); | ||||
|         this.generateDescription(); | ||||
|         this.eventEmitter.emit('conditionChange'); | ||||
|     }; | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,8 @@ define([ | ||||
|     './ConditionManager', | ||||
|     './TestDataManager', | ||||
|     './WidgetDnD', | ||||
|     './eventHelpers', | ||||
|     '../../../api/objects/object-utils', | ||||
|     'lodash', | ||||
|     'zepto' | ||||
| ], function ( | ||||
| @@ -12,6 +14,8 @@ define([ | ||||
|     ConditionManager, | ||||
|     TestDataManager, | ||||
|     WidgetDnD, | ||||
|     eventHelpers, | ||||
|     objectUtils, | ||||
|     _, | ||||
|     $ | ||||
| ) { | ||||
| @@ -32,6 +36,8 @@ define([ | ||||
|      * @param {MCT} openmct An MCT instance | ||||
|      */ | ||||
|     function SummaryWidget(domainObject, openmct) { | ||||
|         eventHelpers.extend(this); | ||||
|  | ||||
|         this.domainObject = domainObject; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
| @@ -73,7 +79,7 @@ define([ | ||||
|         this.addHyperlink(domainObject.url, domainObject.openNewTab); | ||||
|         this.watchForChanges(openmct, domainObject); | ||||
|  | ||||
|         var id = this.domainObject.identifier.key, | ||||
|         var id = objectUtils.makeKeyString(this.domainObject.identifier), | ||||
|             self = this, | ||||
|             oldDomainObject, | ||||
|             statusCapability; | ||||
| @@ -86,7 +92,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 +102,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 +166,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 +186,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(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
| @@ -395,9 +406,7 @@ define([ | ||||
|      * Mutate this domain object's configuration with the current local configuration | ||||
|      */ | ||||
|     SummaryWidget.prototype.updateDomainObject = function () { | ||||
|         if (this.editing) { | ||||
|             this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration); | ||||
|         } | ||||
|         this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration); | ||||
|     }; | ||||
|  | ||||
|     return SummaryWidget; | ||||
|   | ||||
| @@ -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(); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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; | ||||
| }); | ||||
|   | ||||
							
								
								
									
										74
									
								
								src/plugins/summaryWidget/src/eventHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/plugins/summaryWidget/src/eventHelpers.js
									
									
									
									
									
										Normal 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; | ||||
| }); | ||||
| @@ -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; | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -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 () { | ||||
|   | ||||
| @@ -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; | ||||
| }); | ||||
|   | ||||
| @@ -325,6 +325,10 @@ define(['../src/ConditionEvaluator'], function (ConditionEvaluator) { | ||||
|             testOperation = testEvaluator.operations.isUndefined.operation; | ||||
|             expect(testOperation([1])).toEqual(false); | ||||
|             expect(testOperation([])).toEqual(true); | ||||
|             //isDefined | ||||
|             testOperation = testEvaluator.operations.isDefined.operation; | ||||
|             expect(testOperation([1])).toEqual(true); | ||||
|             expect(testOperation([])).toEqual(false); | ||||
|         }); | ||||
|  | ||||
|         it('can produce a description for all supported operations', function () { | ||||
|   | ||||
| @@ -14,7 +14,8 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) { | ||||
|         beforeEach(function () { | ||||
|             mockDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'testKey' | ||||
|                     key: 'testKey', | ||||
|                     namespace: 'testNamespace' | ||||
|                 }, | ||||
|                 name: 'testName', | ||||
|                 composition: [], | ||||
| @@ -49,7 +50,7 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) { | ||||
|             mockObjectService.getObjects = jasmine.createSpy('objectService'); | ||||
|             mockObjectService.getObjects.andReturn(new Promise(function (resolve, reject) { | ||||
|                 resolve({ | ||||
|                     testKey: mockOldDomainObject | ||||
|                     'testNamespace:testKey': mockOldDomainObject | ||||
|                 }); | ||||
|             })); | ||||
|             mockOpenMCT = jasmine.createSpyObj('openmct', [ | ||||
| @@ -73,6 +74,10 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) { | ||||
|             summaryWidget.show(mockContainer); | ||||
|         }); | ||||
|  | ||||
|         it('queries with legacyId', function () { | ||||
|             expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); | ||||
|         }); | ||||
|  | ||||
|         it('adds its DOM element to the view', function () { | ||||
|             expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); | ||||
|         }); | ||||
|   | ||||
							
								
								
									
										76
									
								
								src/plugins/telemetryMean/plugin.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										76
									
								
								src/plugins/telemetryMean/plugin.js
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['./src/MeanTelemetryProvider'], function (MeanTelemetryProvider) { | ||||
|     var DEFAULT_SAMPLES = 10; | ||||
|  | ||||
|     function plugin() { | ||||
|         return function install(openmct) { | ||||
|             openmct.types.addType('telemetry-mean', { | ||||
|                 name: 'Telemetry Filter', | ||||
|                 description: 'Provides telemetry values that represent the mean of the last N values of a telemetry stream', | ||||
|                 creatable: true, | ||||
|                 cssClass: 'icon-telemetry', | ||||
|                 initialize: function (domainObject) { | ||||
|                     domainObject.samples = DEFAULT_SAMPLES; | ||||
|                     domainObject.telemetry = {}; | ||||
|                     domainObject.telemetry.values = | ||||
|                         openmct.time.getAllTimeSystems().map(function (timeSystem, index) { | ||||
|                             return { | ||||
|                                 key: timeSystem.key, | ||||
|                                 name: timeSystem.name, | ||||
|                                 hints: { | ||||
|                                     domain: index + 1 | ||||
|                                 } | ||||
|                             }; | ||||
|                         }); | ||||
|                     domainObject.telemetry.values.push({ | ||||
|                         key: "value", | ||||
|                         name: "Value", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }); | ||||
|                 }, | ||||
|                 form: [ | ||||
|                     { | ||||
|                         "key": "telemetryPoint", | ||||
|                         "name": "Telemetry Point", | ||||
|                         "control": "textfield", | ||||
|                         "required": true, | ||||
|                         "cssClass": "l-input-lg" | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "samples", | ||||
|                         "name": "Samples to Average", | ||||
|                         "control": "textfield", | ||||
|                         "required": true, | ||||
|                         "cssClass": "l-input-sm" | ||||
|                     } | ||||
|                 ] | ||||
|             }); | ||||
|             openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct)); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return plugin; | ||||
| }); | ||||
							
								
								
									
										116
									
								
								src/plugins/telemetryMean/src/MeanTelemetryProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/plugins/telemetryMean/src/MeanTelemetryProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| /*jshint latedef: nofunc */ | ||||
| /*global console */ | ||||
| define([ | ||||
|     '../../../api/objects/object-utils', | ||||
|     './TelemetryAverager' | ||||
| ], function (objectUtils, TelemetryAverager) { | ||||
|  | ||||
|     function MeanTelemetryProvider(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.telemetryAPI = openmct.telemetry; | ||||
|         this.timeAPI = openmct.time; | ||||
|         this.objectAPI = openmct.objects; | ||||
|         this.perObjectProviders = {}; | ||||
|     } | ||||
|  | ||||
|     MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { | ||||
|         return domainObject.type === 'telemetry-mean'; | ||||
|     }; | ||||
|  | ||||
|     MeanTelemetryProvider.prototype.supportsRequest = | ||||
|         MeanTelemetryProvider.prototype.supportsSubscribe = | ||||
|             MeanTelemetryProvider.prototype.canProvideTelemetry; | ||||
|  | ||||
|     MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) { | ||||
|         var wrappedUnsubscribe; | ||||
|         var unsubscribeCalled = false; | ||||
|         var objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); | ||||
|         var samples = domainObject.samples; | ||||
|  | ||||
|         this.objectAPI.get(objectId) | ||||
|             .then(function (linkedDomainObject) { | ||||
|                 if (!unsubscribeCalled) { | ||||
|                     wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback); | ||||
|                 } | ||||
|             }.bind(this)) | ||||
|             .catch(logError); | ||||
|  | ||||
|         return function unsubscribe() { | ||||
|             unsubscribeCalled = true; | ||||
|             if (wrappedUnsubscribe !== undefined) { | ||||
|                 wrappedUnsubscribe(); | ||||
|             } | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) { | ||||
|         var telemetryAverager = new TelemetryAverager(this.telemetryAPI, this.timeAPI, domainObject, samples, callback); | ||||
|         var createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); | ||||
|  | ||||
|         return this.telemetryAPI.subscribe(domainObject, createAverageDatum); | ||||
|     }; | ||||
|  | ||||
|     MeanTelemetryProvider.prototype.request = function (domainObject, request) { | ||||
|         var objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); | ||||
|         var samples = domainObject.samples; | ||||
|  | ||||
|         return this.objectAPI.get(objectId).then(function (linkedDomainObject) { | ||||
|             return this.requestAverageTelemetry(linkedDomainObject, request, samples); | ||||
|         }.bind(this)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     MeanTelemetryProvider.prototype.requestAverageTelemetry = function (domainObject, request, samples) { | ||||
|         var averageData = []; | ||||
|         var addToAverageData = averageData.push.bind(averageData); | ||||
|         var telemetryAverager = new TelemetryAverager(this.telemetryAPI, this.timeAPI, domainObject, samples, addToAverageData); | ||||
|         var createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); | ||||
|  | ||||
|         return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) { | ||||
|             telemetryData.forEach(createAverageDatum); | ||||
|  | ||||
|             return averageData; | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) { | ||||
|         var objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); | ||||
|         return this.objectAPI.get(objectId); | ||||
|     }; | ||||
|  | ||||
|     function logError(error) { | ||||
|         if (error.stack) { | ||||
|             console.error(error.stack); | ||||
|         } else { | ||||
|             console.error(error); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return MeanTelemetryProvider; | ||||
| }); | ||||
							
								
								
									
										460
									
								
								src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,460 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT Web includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| /*jshint latedef: nofunc */ | ||||
| define([ | ||||
|     "./MeanTelemetryProvider", | ||||
|     "./MockTelemetryApi" | ||||
| ], function ( | ||||
|     MeanTelemetryProvider, | ||||
|     MockTelemetryApi | ||||
| ) { | ||||
|     var RANGE_KEY = 'value'; | ||||
|  | ||||
|     describe("The Mean Telemetry Provider", function () { | ||||
|         var mockApi; | ||||
|         var meanTelemetryProvider; | ||||
|         var outstandingPromises = 0; | ||||
|         var mockDomainObject; | ||||
|         var associatedObject; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             createMockApi(); | ||||
|             setTimeSystemTo('utc'); | ||||
|             createMockObjects(); | ||||
|             meanTelemetryProvider = new MeanTelemetryProvider(mockApi); | ||||
|         }); | ||||
|  | ||||
|         it("supports telemetry-mean objects only", function () { | ||||
|             var mockTelemetryMeanObject = mockObjectWithType('telemetry-mean'); | ||||
|             var mockOtherObject = mockObjectWithType('other'); | ||||
|  | ||||
|             expect(meanTelemetryProvider.canProvideTelemetry(mockTelemetryMeanObject)).toBe(true); | ||||
|             expect(meanTelemetryProvider.canProvideTelemetry(mockOtherObject)).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         describe("the subscribe function", function () { | ||||
|             var subscriptionCallback; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 subscriptionCallback = jasmine.createSpy('subscriptionCallback'); | ||||
|             }); | ||||
|  | ||||
|             it("subscribes to telemetry for the associated object", function () { | ||||
|                 meanTelemetryProvider.subscribe(mockDomainObject); | ||||
|  | ||||
|                 expectObjectWasSubscribedTo(associatedObject); | ||||
|             }); | ||||
|  | ||||
|             it("returns a function that unsubscribes from the associated object", function () { | ||||
|                 var unsubscribe = meanTelemetryProvider.subscribe(mockDomainObject); | ||||
|  | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(unsubscribe); | ||||
|  | ||||
|                 expectUnsubscribeFrom(associatedObject); | ||||
|             }); | ||||
|  | ||||
|             it("returns an average only when the sample size is reached", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313} | ||||
|                 ]; | ||||
|  | ||||
|                 setSampleSize(5); | ||||
|                 meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                 feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                 expectNoAverageForTelemetry(inputTelemetry); | ||||
|             }); | ||||
|  | ||||
|             it("correctly averages a sample of five values", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313}, | ||||
|                     {'utc': 5, 'defaultRange': 1.1231} | ||||
|                 ]; | ||||
|                 var expectedAverages = [{ | ||||
|                     'utc': 5, 'value': 222.44888 | ||||
|                 }]; | ||||
|  | ||||
|                 setSampleSize(5); | ||||
|                 meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                 feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                 expectAveragesForTelemetry(expectedAverages, inputTelemetry); | ||||
|             }); | ||||
|  | ||||
|             it("correctly averages a sample of ten values", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313}, | ||||
|                     {'utc': 5, 'defaultRange': 1.1231}, | ||||
|                     {'utc': 6, 'defaultRange': 2323.12}, | ||||
|                     {'utc': 7, 'defaultRange': 532.12}, | ||||
|                     {'utc': 8, 'defaultRange': 453.543}, | ||||
|                     {'utc': 9, 'defaultRange': 89.2111}, | ||||
|                     {'utc': 10, 'defaultRange': 0.543} | ||||
|                 ]; | ||||
|                 var expectedAverages = [{ | ||||
|                     'utc': 10, 'value': 451.07815 | ||||
|                 }]; | ||||
|  | ||||
|                 setSampleSize(10); | ||||
|                 meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                 feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                 expectAveragesForTelemetry(expectedAverages, inputTelemetry); | ||||
|             }); | ||||
|  | ||||
|             it("only averages values within its sample window", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313}, | ||||
|                     {'utc': 5, 'defaultRange': 1.1231}, | ||||
|                     {'utc': 6, 'defaultRange': 2323.12}, | ||||
|                     {'utc': 7, 'defaultRange': 532.12}, | ||||
|                     {'utc': 8, 'defaultRange': 453.543}, | ||||
|                     {'utc': 9, 'defaultRange': 89.2111}, | ||||
|                     {'utc': 10, 'defaultRange': 0.543} | ||||
|                 ]; | ||||
|                 var expectedAverages = [ | ||||
|                     {'utc': 5, 'value': 222.44888}, | ||||
|                     {'utc': 6, 'value': 662.4482599999999}, | ||||
|                     {'utc': 7, 'value': 704.6078}, | ||||
|                     {'utc': 8, 'value': 773.02748}, | ||||
|                     {'utc': 9, 'value': 679.8234399999999}, | ||||
|                     {'utc': 10, 'value': 679.70742} | ||||
|                 ]; | ||||
|  | ||||
|                 setSampleSize(5); | ||||
|                 meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                 feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                 expectAveragesForTelemetry(expectedAverages, inputTelemetry); | ||||
|             }); | ||||
|             describe("given telemetry input with range values", function () { | ||||
|                 var inputTelemetry; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     inputTelemetry = [{ | ||||
|                         'utc': 1, | ||||
|                         'rangeKey': 5678, | ||||
|                         'otherKey': 9999 | ||||
|                     }]; | ||||
|                     setSampleSize(1); | ||||
|                 }); | ||||
|                 it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { | ||||
|                     var averageTelemetryForRangeKey = [{ | ||||
|                         'utc': 1, | ||||
|                         'value': 5678 | ||||
|                     }]; | ||||
|  | ||||
|                     meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                     mockApi.telemetry.setDefaultRangeTo('rangeKey'); | ||||
|                     feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                     expectAveragesForTelemetry(averageTelemetryForRangeKey, inputTelemetry); | ||||
|                 }); | ||||
|  | ||||
|                 it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { | ||||
|                     var averageTelemetryForOtherKey = [{ | ||||
|                         'utc': 1, | ||||
|                         'value': 9999 | ||||
|                     }]; | ||||
|  | ||||
|                     meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                     mockApi.telemetry.setDefaultRangeTo('otherKey'); | ||||
|                     feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                     expectAveragesForTelemetry(averageTelemetryForOtherKey, inputTelemetry); | ||||
|  | ||||
|                 }); | ||||
|             }); | ||||
|             describe("given telemetry input with range values", function () { | ||||
|                 var inputTelemetry; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     inputTelemetry = [{ | ||||
|                         'utc': 1, | ||||
|                         'rangeKey': 5678, | ||||
|                         'otherKey': 9999 | ||||
|                     }]; | ||||
|                     setSampleSize(1); | ||||
|                 }); | ||||
|                 it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { | ||||
|                     var averageTelemetryForRangeKey = [{ | ||||
|                         'utc': 1, | ||||
|                         'value': 5678 | ||||
|                     }]; | ||||
|  | ||||
|                     meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                     mockApi.telemetry.setDefaultRangeTo('rangeKey'); | ||||
|                     feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                     expectAveragesForTelemetry(averageTelemetryForRangeKey, inputTelemetry); | ||||
|                 }); | ||||
|  | ||||
|                 it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { | ||||
|                     var averageTelemetryForOtherKey = [{ | ||||
|                         'utc': 1, | ||||
|                         'value': 9999 | ||||
|                     }]; | ||||
|  | ||||
|                     meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); | ||||
|                     mockApi.telemetry.setDefaultRangeTo('otherKey'); | ||||
|                     feedInputTelemetry(inputTelemetry); | ||||
|  | ||||
|                     expectAveragesForTelemetry(averageTelemetryForOtherKey, inputTelemetry); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             function feedInputTelemetry(inputTelemetry) { | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(function () { | ||||
|                     inputTelemetry.forEach(mockApi.telemetry.mockReceiveTelemetry); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function expectNoAverageForTelemetry(inputTelemetry) { | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(function () { | ||||
|                     expect(subscriptionCallback).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function expectAveragesForTelemetry(expectedAverages) { | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(function () { | ||||
|                     expectedAverages.forEach(function (averageDatum) { | ||||
|                         expect(subscriptionCallback).toHaveBeenCalledWith(averageDatum); | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function expectObjectWasSubscribedTo(object) { | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(function () { | ||||
|                     expect(mockApi.telemetry.subscribe).toHaveBeenCalledWith(object, jasmine.any(Function)); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function expectUnsubscribeFrom() { | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(function () { | ||||
|                     expect(mockApi.telemetry.unsubscribe).toHaveBeenCalled(); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|         }); | ||||
|  | ||||
|         describe("the request function", function () { | ||||
|  | ||||
|             it("requests telemetry for the associated object", function () { | ||||
|                 meanTelemetryProvider.request(mockDomainObject); | ||||
|  | ||||
|                 expectTelemetryToBeRequestedFor(associatedObject); | ||||
|             }); | ||||
|  | ||||
|             it("returns an average only when the sample size is reached", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313} | ||||
|                 ]; | ||||
|                 var promiseForAverage; | ||||
|  | ||||
|                 setSampleSize(5); | ||||
|                 whenTelemetryRequestedReturn(inputTelemetry); | ||||
|                 promiseForAverage = meanTelemetryProvider.request(mockDomainObject); | ||||
|  | ||||
|                 expectEmptyResponse(promiseForAverage); | ||||
|             }); | ||||
|  | ||||
|             it("correctly averages a sample of five values", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313}, | ||||
|                     {'utc': 5, 'defaultRange': 1.1231} | ||||
|                 ]; | ||||
|                 var promiseForAverage; | ||||
|  | ||||
|                 setSampleSize(5); | ||||
|                 whenTelemetryRequestedReturn(inputTelemetry); | ||||
|                 promiseForAverage = meanTelemetryProvider.request(mockDomainObject); | ||||
|  | ||||
|                 expectAverageToBe(222.44888, promiseForAverage); | ||||
|             }); | ||||
|  | ||||
|             it("correctly averages a sample of ten values", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313}, | ||||
|                     {'utc': 5, 'defaultRange': 1.1231}, | ||||
|                     {'utc': 6, 'defaultRange': 2323.12}, | ||||
|                     {'utc': 7, 'defaultRange': 532.12}, | ||||
|                     {'utc': 8, 'defaultRange': 453.543}, | ||||
|                     {'utc': 9, 'defaultRange': 89.2111}, | ||||
|                     {'utc': 10, 'defaultRange': 0.543} | ||||
|                 ]; | ||||
|                 var promiseForAverage; | ||||
|  | ||||
|                 setSampleSize(10); | ||||
|                 whenTelemetryRequestedReturn(inputTelemetry); | ||||
|                 promiseForAverage = meanTelemetryProvider.request(mockDomainObject); | ||||
|  | ||||
|                 expectAverageToBe(451.07815, promiseForAverage); | ||||
|             }); | ||||
|  | ||||
|             it("only averages values within its sample window", function () { | ||||
|                 var inputTelemetry = [ | ||||
|                     {'utc': 1, 'defaultRange': 123.1231}, | ||||
|                     {'utc': 2, 'defaultRange': 321.3223}, | ||||
|                     {'utc': 3, 'defaultRange': 111.4446}, | ||||
|                     {'utc': 4, 'defaultRange': 555.2313}, | ||||
|                     {'utc': 5, 'defaultRange': 1.1231}, | ||||
|                     {'utc': 6, 'defaultRange': 2323.12}, | ||||
|                     {'utc': 7, 'defaultRange': 532.12}, | ||||
|                     {'utc': 8, 'defaultRange': 453.543}, | ||||
|                     {'utc': 9, 'defaultRange': 89.2111}, | ||||
|                     {'utc': 10, 'defaultRange': 0.543} | ||||
|                 ]; | ||||
|                 var promiseForAverage; | ||||
|  | ||||
|                 setSampleSize(5); | ||||
|                 whenTelemetryRequestedReturn(inputTelemetry); | ||||
|                 promiseForAverage = meanTelemetryProvider.request(mockDomainObject); | ||||
|  | ||||
|                 expectAverageToBe(679.70742, promiseForAverage); | ||||
|             }); | ||||
|  | ||||
|             function expectAverageToBe(expectedValue, promiseForAverage) { | ||||
|                 var averageData; | ||||
|  | ||||
|                 promiseForAverage.then(function (data) { | ||||
|                     averageData = data; | ||||
|                 }); | ||||
|  | ||||
|                 waitsFor(function () { | ||||
|                     return averageData !== undefined; | ||||
|                 }, 'data to return from request', 1); | ||||
|                 runs(function () { | ||||
|                     var averageDatum = averageData[averageData.length - 1]; | ||||
|                     expect(averageDatum[RANGE_KEY]).toBe(expectedValue); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function expectEmptyResponse(promiseForAverage) { | ||||
|                 var averageData; | ||||
|  | ||||
|                 promiseForAverage.then(function (data) { | ||||
|                     averageData = data; | ||||
|                 }); | ||||
|  | ||||
|                 waitsFor(function () { | ||||
|                     return averageData !== undefined; | ||||
|                 }, 'data to return from request', 1); | ||||
|                 runs(function () { | ||||
|                     expect(averageData.length).toBe(0); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             function whenTelemetryRequestedReturn(telemetry) { | ||||
|                 mockApi.telemetry.request.andReturn(resolvePromiseWith(telemetry)); | ||||
|             } | ||||
|  | ||||
|             function expectTelemetryToBeRequestedFor(object) { | ||||
|                 waitsFor(allPromisesToBeResolved); | ||||
|                 runs(function () { | ||||
|                     expect(mockApi.telemetry.request).toHaveBeenCalledWith(object, undefined); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         function createMockObjects() { | ||||
|             mockDomainObject = { | ||||
|                 telemetryPoint: 'someTelemetryPoint' | ||||
|             }; | ||||
|             associatedObject = {}; | ||||
|             mockApi.objects.get.andReturn(resolvePromiseWith(associatedObject)); | ||||
|         } | ||||
|  | ||||
|         function setSampleSize(sampleSize) { | ||||
|             mockDomainObject.samples = sampleSize; | ||||
|         } | ||||
|  | ||||
|         function createMockApi() { | ||||
|             mockApi = { | ||||
|                 telemetry: new MockTelemetryApi(), | ||||
|                 objects: createMockObjectApi(), | ||||
|                 time: createMockTimeApi() | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         function createMockObjectApi() { | ||||
|             return jasmine.createSpyObj('ObjectAPI', [ | ||||
|                 'get' | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
|         function mockObjectWithType(type) { | ||||
|             return { | ||||
|                 type: type | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         function resolvePromiseWith(value) { | ||||
|             outstandingPromises++; | ||||
|             return Promise.resolve(value).then(function () { | ||||
|                 outstandingPromises--; | ||||
|                 return value; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function allPromisesToBeResolved() { | ||||
|             return outstandingPromises === 0; | ||||
|         } | ||||
|  | ||||
|         function createMockTimeApi() { | ||||
|             return jasmine.createSpyObj("timeApi", ['timeSystem']); | ||||
|         } | ||||
|  | ||||
|         function setTimeSystemTo(timeSystemKey) { | ||||
|             mockApi.time.timeSystem.andReturn({ | ||||
|                 key: timeSystemKey | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
| }); | ||||
							
								
								
									
										106
									
								
								src/plugins/telemetryMean/src/MockTelemetryApi.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/plugins/telemetryMean/src/MockTelemetryApi.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| /*global jasmine, spyOn */ | ||||
| define([], function () { | ||||
|  | ||||
|     function MockTelemetryApi() { | ||||
|         this.createSpy('subscribe'); | ||||
|         this.createSpy('getMetadata'); | ||||
|  | ||||
|         this.metadata = this.createMockMetadata(); | ||||
|         this.setDefaultRangeTo('defaultRange'); | ||||
|         this.unsubscribe = jasmine.createSpy('unsubscribe'); | ||||
|         this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this); | ||||
|     } | ||||
|  | ||||
|     MockTelemetryApi.prototype.subscribe = function () { | ||||
|         return this.unsubscribe; | ||||
|     }; | ||||
|  | ||||
|     MockTelemetryApi.prototype.getMetadata = function (object) { | ||||
|         return this.metadata; | ||||
|     }; | ||||
|  | ||||
|     MockTelemetryApi.prototype.request = jasmine.createSpy('request'); | ||||
|  | ||||
|     MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) { | ||||
|         var mockValueFormatter = jasmine.createSpyObj("valueFormatter", [ | ||||
|             "parse" | ||||
|         ]); | ||||
|  | ||||
|         mockValueFormatter.parse.andCallFake(function (value) { | ||||
|             return value[valueMetadata.key]; | ||||
|         }); | ||||
|  | ||||
|         return mockValueFormatter; | ||||
|     }; | ||||
|  | ||||
|     MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) { | ||||
|         var subscriptionCallback = this.subscribe.mostRecentCall.args[1]; | ||||
|         subscriptionCallback(newTelemetryDatum); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) { | ||||
|         this.requestTelemetry = telemetryData; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) { | ||||
|         var mockMetadataValue = { | ||||
|             key: rangeKey | ||||
|         }; | ||||
|         this.metadata.valuesForHints.andReturn([mockMetadataValue]); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     MockTelemetryApi.prototype.createMockMetadata = function () { | ||||
|         var mockMetadata = jasmine.createSpyObj("metadata", [ | ||||
|             'value', | ||||
|             'valuesForHints' | ||||
|         ]); | ||||
|  | ||||
|         mockMetadata.value.andCallFake(function (key) { | ||||
|             return { | ||||
|                 key: key | ||||
|             }; | ||||
|         }); | ||||
|         return mockMetadata; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     MockTelemetryApi.prototype.createSpy = function (functionName) { | ||||
|         this[functionName] = this[functionName].bind(this); | ||||
|         spyOn(this, functionName); | ||||
|         this[functionName].andCallThrough(); | ||||
|     }; | ||||
|  | ||||
|     return MockTelemetryApi; | ||||
| }); | ||||
							
								
								
									
										120
									
								
								src/plugins/telemetryMean/src/TelemetryAverager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/plugins/telemetryMean/src/TelemetryAverager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2017, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([], function () { | ||||
|  | ||||
|     function TelemetryAverager(telemetryAPI, timeAPI, domainObject, samples, averageDatumCallback) { | ||||
|         this.telemetryAPI = telemetryAPI; | ||||
|         this.timeAPI = timeAPI; | ||||
|  | ||||
|         this.domainObject = domainObject; | ||||
|         this.samples = samples; | ||||
|         this.averagingWindow = []; | ||||
|  | ||||
|         this.rangeKey = undefined; | ||||
|         this.rangeFormatter = undefined; | ||||
|         this.setRangeKeyAndFormatter(); | ||||
|  | ||||
|         // Defined dynamically based on current time system | ||||
|         this.domainKey = undefined; | ||||
|         this.domainFormatter = undefined; | ||||
|  | ||||
|         this.averageDatumCallback = averageDatumCallback; | ||||
|     } | ||||
|  | ||||
|     TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) { | ||||
|         this.setDomainKeyAndFormatter(); | ||||
|  | ||||
|         var timeValue = this.domainFormatter.parse(telemetryDatum); | ||||
|         var rangeValue = this.rangeFormatter.parse(telemetryDatum); | ||||
|  | ||||
|         this.averagingWindow.push(rangeValue); | ||||
|  | ||||
|         if (this.averagingWindow.length < this.samples) { | ||||
|             // We do not have enough data to produce an average | ||||
|             return; | ||||
|         } else if (this.averagingWindow.length > this.samples) { | ||||
|             //Do not let averaging window grow beyond defined sample size | ||||
|             this.averagingWindow.shift(); | ||||
|         } | ||||
|  | ||||
|         var averageValue = this.calculateMean(); | ||||
|  | ||||
|         var meanDatum = {}; | ||||
|         meanDatum[this.domainKey] = timeValue; | ||||
|         meanDatum.value = averageValue; | ||||
|  | ||||
|         this.averageDatumCallback(meanDatum); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAverager.prototype.calculateMean = function () { | ||||
|         var sum = 0; | ||||
|         var i = 0; | ||||
|  | ||||
|         for (; i < this.averagingWindow.length; i++) { | ||||
|             sum += this.averagingWindow[i]; | ||||
|         } | ||||
|  | ||||
|         return sum / this.averagingWindow.length; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * The mean telemetry filter produces domain values in whatever time | ||||
|      * system is currently selected from the conductor. Because this can | ||||
|      * change dynamically, the averager needs to be updated regularly with | ||||
|      * the current domain. | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAverager.prototype.setDomainKeyAndFormatter = function () { | ||||
|         var domainKey = this.timeAPI.timeSystem().key; | ||||
|         if (domainKey !== this.domainKey) { | ||||
|             this.domainKey = domainKey; | ||||
|             this.domainFormatter = this.getFormatter(domainKey); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAverager.prototype.setRangeKeyAndFormatter = function () { | ||||
|         var metadatas = this.telemetryAPI.getMetadata(this.domainObject); | ||||
|         var rangeValues = metadatas.valuesForHints(['range']); | ||||
|  | ||||
|         this.rangeKey = rangeValues[0].key; | ||||
|         this.rangeFormatter = this.getFormatter(this.rangeKey); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAverager.prototype.getFormatter = function (key) { | ||||
|         var objectMetadata = this.telemetryAPI.getMetadata(this.domainObject); | ||||
|         var valueMetadata = objectMetadata.value(key); | ||||
|  | ||||
|         return this.telemetryAPI.getValueFormatter(valueMetadata); | ||||
|     }; | ||||
|  | ||||
|     return TelemetryAverager; | ||||
| }); | ||||
| @@ -33,37 +33,96 @@ define(['EventEmitter'], function (EventEmitter) { | ||||
|  | ||||
|     Selection.prototype = Object.create(EventEmitter.prototype); | ||||
|  | ||||
|     Selection.prototype.add = function (context) { | ||||
|         this.clear(); // Only allow single select as initial simplification | ||||
|         this.selected.push(context); | ||||
|         this.emit('change'); | ||||
|     }; | ||||
|  | ||||
|     Selection.prototype.remove = function (path) { | ||||
|         this.selected = this.selected.filter(function (otherPath) { | ||||
|             return !path.matches(otherPath); | ||||
|         }); | ||||
|         this.emit('change'); | ||||
|     }; | ||||
|  | ||||
|     Selection.prototype.contains = function (path) { | ||||
|         return this.selected.some(function (otherPath) { | ||||
|             return path.matches(otherPath); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     Selection.prototype.clear = function () { | ||||
|         this.selected = []; | ||||
|         this.emit('change'); | ||||
|     }; | ||||
|  | ||||
|     Selection.prototype.primary = function () { | ||||
|         return this.selected[this.selected.length - 1]; | ||||
|     }; | ||||
|  | ||||
|     Selection.prototype.all = function () { | ||||
|     /** | ||||
|      * Gets the selected object. | ||||
|      * @public | ||||
|      */ | ||||
|     Selection.prototype.get = function () { | ||||
|         return this.selected; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Selects the selectable object and emits the 'change' event. | ||||
|      * | ||||
|      * @param {object} selectable an object with element and context properties | ||||
|      * @private | ||||
|      */ | ||||
|     Selection.prototype.select = function (selectable) { | ||||
|         if (!Array.isArray(selectable)) { | ||||
|             selectable = [selectable]; | ||||
|         } | ||||
|  | ||||
|         if (this.selected[0] && this.selected[0].element) { | ||||
|             this.selected[0].element.classList.remove('s-selected'); | ||||
|         } | ||||
|  | ||||
|         if (this.selected[1]) { | ||||
|             this.selected[1].element.classList.remove('s-selected-parent'); | ||||
|         } | ||||
|  | ||||
|         if (selectable[0] && selectable[0].element) { | ||||
|             selectable[0].element.classList.add('s-selected'); | ||||
|         } | ||||
|  | ||||
|         if (selectable[1]) { | ||||
|             selectable[1].element.classList.add('s-selected-parent'); | ||||
|         } | ||||
|  | ||||
|         this.selected = selectable; | ||||
|         this.emit('change', this.selected); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     Selection.prototype.capture = function (selectable) { | ||||
|         if (!this.capturing) { | ||||
|             this.capturing = []; | ||||
|         } | ||||
|  | ||||
|         this.capturing.push(selectable); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     Selection.prototype.selectCapture = function (selectable) { | ||||
|         if (!this.capturing) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.select(this.capturing.reverse()); | ||||
|         delete this.capturing; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Attaches the click handlers to the element. | ||||
|      * | ||||
|      * @param element an html element | ||||
|      * @param context object with oldItem, item and toolbar properties | ||||
|      * @param select a flag to select the element if true | ||||
|      * @returns a function that removes the click handlers from the element | ||||
|      * @public | ||||
|      */ | ||||
|     Selection.prototype.selectable = function (element, context, select) { | ||||
|         var selectable = { | ||||
|             context: context, | ||||
|             element: element | ||||
|         }; | ||||
|         var capture = this.capture.bind(this, selectable); | ||||
|         var selectCapture = this.selectCapture.bind(this, selectable); | ||||
|         element.addEventListener('click', capture, true); | ||||
|         element.addEventListener('click', selectCapture); | ||||
|  | ||||
|         if (select) { | ||||
|             element.click(); | ||||
|         } | ||||
|  | ||||
|         return function () { | ||||
|             element.removeEventListener('click', capture); | ||||
|             element.removeEventListener('click', selectCapture); | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     return Selection; | ||||
| }); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user