├── .github └── FUNDING.yml ├── debug.js ├── LICENSE ├── jasmine ├── lib │ └── jasmine-1.3.1 │ │ ├── MIT.LICENSE │ │ ├── jasmine.css │ │ └── jasmine-html.js └── SpecRunner.html ├── flowchart ├── svg_class.js ├── dragging_service.js ├── mouse_capture_service.js ├── svg_class.spec.js ├── flowchart_template.html ├── flowchart_directive.js ├── flowchart_directive.spec.js ├── flowchart_viewmodel.js └── flowchart_viewmodel.spec.js ├── server.js ├── app.css ├── index.html ├── README.md └── app.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ashleydavis 4 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | // 2 | // Debug utilities. 3 | // 4 | 5 | (function () { 6 | 7 | if (typeof debug !== "undefined") { 8 | throw new Error("debug object already defined!"); 9 | } 10 | 11 | debug = {}; 12 | 13 | // 14 | // Assert that an object is valid. 15 | // 16 | debug.assertObjectValid = function (obj) { 17 | 18 | if (!obj) { 19 | throw new Exception("Invalid object!"); 20 | } 21 | 22 | if ($.isPlainObject(obj)) { 23 | throw new Error("Input is not an object! It is a " + typeof(obj)); 24 | } 25 | }; 26 | 27 | })(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ashley Davis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /jasmine/lib/jasmine-1.3.1/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /flowchart/svg_class.js: -------------------------------------------------------------------------------- 1 | // 2 | // http://www.justinmccandless.com/blog/Patching+jQuery's+Lack+of+SVG+Support 3 | // 4 | // Functions to add and remove SVG classes because jQuery doesn't support this. 5 | // 6 | 7 | // jQuery's removeClass doesn't work for SVG, but this does! 8 | // takes the object obj to remove from, and removes class remove 9 | // returns true if successful, false if remove does not exist in obj 10 | var removeClassSVG = function(obj, remove) { 11 | var classes = obj.attr('class'); 12 | if (!classes) { 13 | return false; 14 | } 15 | 16 | var index = classes.search(remove); 17 | 18 | // if the class already doesn't exist, return false now 19 | if (index == -1) { 20 | return false; 21 | } 22 | else { 23 | // string manipulation to remove the class 24 | classes = classes.substring(0, index) + classes.substring((index + remove.length), classes.length); 25 | 26 | // set the new string as the object's class 27 | obj.attr('class', classes); 28 | 29 | return true; 30 | } 31 | }; 32 | 33 | // jQuery's hasClass doesn't work for SVG, but this does! 34 | // takes an object obj and checks for class has 35 | // returns true if the class exits in obj, false otherwise 36 | var hasClassSVG = function(obj, has) { 37 | var classes = obj.attr('class'); 38 | if (!classes) { 39 | return false; 40 | } 41 | 42 | var index = classes.search(has); 43 | 44 | if (index == -1) { 45 | return false; 46 | } 47 | else { 48 | return true; 49 | } 50 | }; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // 2 | // Simple nodejs server for running the sample. 3 | // 4 | // http://stackoverflow.com/questions/6084360/node-js-as-a-simple-web-server 5 | // 6 | 7 | var http = require("http"), 8 | url = require("url"), 9 | path = require("path"), 10 | fs = require("fs") 11 | port = process.argv[2] || 8888; 12 | 13 | http.createServer(function(request, response) { 14 | 15 | var uri = url.parse(request.url).pathname 16 | , filename = path.join(process.cwd(), uri); 17 | 18 | fs.exists(filename, function(exists) { 19 | if(!exists) { 20 | response.writeHead(404, {"Content-Type": "text/plain"}); 21 | response.write("404 Not Found\n"); 22 | response.end(); 23 | return; 24 | } 25 | 26 | if (fs.statSync(filename).isDirectory()) { 27 | filename += '/index.html'; 28 | } 29 | 30 | fs.readFile(filename, "binary", function(err, file) { 31 | if(err) { 32 | response.writeHead(500, {"Content-Type": "text/plain"}); 33 | response.write(err + "\n"); 34 | response.end(); 35 | return; 36 | } 37 | 38 | var contentType = "text/plain"; 39 | var ext = path.extname(filename); 40 | 41 | switch (ext) { 42 | case ".html": 43 | contentType = "text/html"; 44 | break; 45 | case ".css": 46 | contentType = "text/css"; 47 | break; 48 | case ".js": 49 | contentType = "text/javascript"; 50 | break; 51 | } 52 | 53 | console.log("Incoming ext: " + ext + ", content: " + contentType); 54 | 55 | response.writeHead(200, {"Content-Type": contentType}); 56 | response.write(file, "binary"); 57 | response.end(); 58 | }); 59 | }); 60 | }).listen(parseInt(port, 10)); 61 | 62 | console.log("Static file server running at\n => http://localhost:" + port + "/\nCTRL + C to shutdown"); -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | /* 2 | Generic reset. 3 | */ 4 | 5 | * { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | .test { 11 | border: 5px red solid; 12 | padding: 10; 13 | margin: 10; 14 | font-family: "Times New Roman"; 15 | font-style: italic; 16 | } 17 | 18 | /* 19 | Styles for nodes and connectors. 20 | */ 21 | 22 | .node-rect { 23 | stroke: black; 24 | stroke-width: 2; 25 | } 26 | 27 | .mouseover-node-rect { 28 | stroke: black; 29 | stroke-width: 4; 30 | } 31 | 32 | .selected-node-rect { 33 | stroke: red; 34 | stroke-width: 3; 35 | } 36 | 37 | .connector-circle { 38 | fill: white; 39 | stroke: black; 40 | stroke-width: 2; 41 | } 42 | 43 | .mouseover-connector-circle { 44 | fill: white; 45 | stroke: black; 46 | stroke-width: 3; 47 | } 48 | 49 | /* 50 | Style for connections. 51 | */ 52 | 53 | .connection { 54 | } 55 | 56 | .connection-line { 57 | stroke: gray; 58 | stroke-width: 4; 59 | fill: transparent; 60 | } 61 | 62 | .mouseover-connection-line { 63 | stroke: gray; 64 | stroke-width: 6; 65 | fill: transparent; 66 | } 67 | 68 | .selected-connection-line { 69 | stroke: red; 70 | stroke-width: 4; 71 | fill: transparent; 72 | } 73 | 74 | .connection-endpoint { 75 | fill: gray; 76 | } 77 | 78 | .selected-connection-endpoint { 79 | fill: red; 80 | } 81 | 82 | .mouseover-connection-endpoint { 83 | fill: gray; 84 | } 85 | 86 | .connection-name{ 87 | fill: black; 88 | } 89 | 90 | .selected-connection-name{ 91 | fill: red; 92 | } 93 | 94 | .mouseover-connection-name{ 95 | fill: gray; 96 | } 97 | /* 98 | Style for the connection being dragged out. 99 | */ 100 | 101 | .dragging-connection { 102 | pointer-events: none; 103 | } 104 | 105 | .dragging-connection-line { 106 | stroke: gray; 107 | stroke-width: 3; 108 | fill: transparent; 109 | } 110 | 111 | .dragging-connection-endpoint { 112 | fill: gray; 113 | } 114 | 115 | /* 116 | The element (in this case the SVG element) that contains the draggable elements. 117 | */ 118 | 119 | .draggable-container { 120 | border: solid 1px blue; 121 | } 122 | 123 | /* 124 | Drag selection rectangle. 125 | */ 126 | 127 | .drag-selection-rect { 128 | stroke: blue; 129 | stroke-width: 2; 130 | fill: transparent; 131 | } -------------------------------------------------------------------------------- /jasmine/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jasmine Spec Runner 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /flowchart/dragging_service.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('dragging', ['mouseCapture', ] ) 3 | 4 | // 5 | // Service used to help with dragging and clicking on elements. 6 | // 7 | .factory('dragging', ['$rootScope', 'mouseCapture',function ($rootScope, mouseCapture) { 8 | 9 | // 10 | // Threshold for dragging. 11 | // When the mouse moves by at least this amount dragging starts. 12 | // 13 | var threshold = 5; 14 | 15 | return { 16 | 17 | 18 | // 19 | // Called by users of the service to register a mousedown event and start dragging. 20 | // Acquires the 'mouse capture' until the mouseup event. 21 | // 22 | startDrag: function (evt, config) { 23 | 24 | var dragging = false; 25 | var x = evt.pageX; 26 | var y = evt.pageY; 27 | 28 | // 29 | // Handler for mousemove events while the mouse is 'captured'. 30 | // 31 | var mouseMove = function (evt) { 32 | 33 | if (!dragging) { 34 | if (Math.abs(evt.pageX - x) > threshold || 35 | Math.abs(evt.pageY - y) > threshold) 36 | { 37 | dragging = true; 38 | 39 | if (config.dragStarted) { 40 | config.dragStarted(x, y, evt); 41 | } 42 | 43 | if (config.dragging) { 44 | // First 'dragging' call to take into account that we have 45 | // already moved the mouse by a 'threshold' amount. 46 | config.dragging(evt.pageX, evt.pageY, evt); 47 | } 48 | } 49 | } 50 | else { 51 | if (config.dragging) { 52 | config.dragging(evt.pageX, evt.pageY, evt); 53 | } 54 | 55 | x = evt.pageX; 56 | y = evt.pageY; 57 | } 58 | }; 59 | 60 | // 61 | // Handler for when mouse capture is released. 62 | // 63 | var released = function() { 64 | 65 | if (dragging) { 66 | if (config.dragEnded) { 67 | config.dragEnded(); 68 | } 69 | } 70 | else { 71 | if (config.clicked) { 72 | config.clicked(); 73 | } 74 | } 75 | }; 76 | 77 | // 78 | // Handler for mouseup event while the mouse is 'captured'. 79 | // Mouseup releases the mouse capture. 80 | // 81 | var mouseUp = function (evt) { 82 | 83 | mouseCapture.release(); 84 | 85 | evt.stopPropagation(); 86 | evt.preventDefault(); 87 | }; 88 | 89 | // 90 | // Acquire the mouse capture and start handling mouse events. 91 | // 92 | mouseCapture.acquire(evt, { 93 | mouseMove: mouseMove, 94 | mouseUp: mouseUp, 95 | released: released, 96 | }); 97 | 98 | evt.stopPropagation(); 99 | evt.preventDefault(); 100 | }, 101 | 102 | }; 103 | 104 | }]) 105 | 106 | ; 107 | 108 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | AngularJS-FlowChart 4 | 5 | 9 | 10 | 11 | 12 | 19 | 20 |
21 |
22 | 28 |
29 |
30 | 36 | 43 | 50 | 57 | 58 | 61 | 65 | 66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /flowchart/mouse_capture_service.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('mouseCapture', []) 3 | 4 | // 5 | // Service used to acquire 'mouse capture' then receive dragging events while the mouse is captured. 6 | // 7 | .factory('mouseCapture', [ '$rootScope', function ($rootScope) { 8 | 9 | // 10 | // Element that the mouse capture applies to, defaults to 'document' 11 | // unless the 'mouse-capture' directive is used. 12 | // 13 | var $element = $(document); 14 | 15 | // 16 | // Set when mouse capture is acquired to an object that contains 17 | // handlers for 'mousemove' and 'mouseup' events. 18 | // 19 | var mouseCaptureConfig = null; 20 | 21 | // 22 | // Handler for mousemove events while the mouse is 'captured'. 23 | // 24 | var mouseMove = function (evt) { 25 | 26 | if (mouseCaptureConfig && mouseCaptureConfig.mouseMove) { 27 | 28 | mouseCaptureConfig.mouseMove(evt); 29 | 30 | $rootScope.$digest(); 31 | } 32 | }; 33 | 34 | // 35 | // Handler for mouseup event while the mouse is 'captured'. 36 | // 37 | var mouseUp = function (evt) { 38 | 39 | if (mouseCaptureConfig && mouseCaptureConfig.mouseUp) { 40 | 41 | mouseCaptureConfig.mouseUp(evt); 42 | 43 | $rootScope.$digest(); 44 | } 45 | }; 46 | 47 | return { 48 | 49 | // 50 | // Register an element to use as the mouse capture element instead of 51 | // the default which is the document. 52 | // 53 | registerElement: function(element) { 54 | 55 | $element = element; 56 | }, 57 | 58 | // 59 | // Acquire the 'mouse capture'. 60 | // After acquiring the mouse capture mousemove and mouseup events will be 61 | // forwarded to callbacks in 'config'. 62 | // 63 | acquire: function (evt, config) { 64 | 65 | // 66 | // Release any prior mouse capture. 67 | // 68 | this.release(); 69 | 70 | mouseCaptureConfig = config; 71 | 72 | // 73 | // In response to the mousedown event register handlers for mousemove and mouseup 74 | // during 'mouse capture'. 75 | // 76 | $element.mousemove(mouseMove); 77 | $element.mouseup(mouseUp); 78 | }, 79 | 80 | // 81 | // Release the 'mouse capture'. 82 | // 83 | release: function () { 84 | 85 | if (mouseCaptureConfig) { 86 | 87 | if (mouseCaptureConfig.released) { 88 | // 89 | // Let the client know that their 'mouse capture' has been released. 90 | // 91 | mouseCaptureConfig.released(); 92 | } 93 | 94 | mouseCaptureConfig = null; 95 | } 96 | 97 | $element.unbind("mousemove", mouseMove); 98 | $element.unbind("mouseup", mouseUp); 99 | }, 100 | }; 101 | }] 102 | ) 103 | 104 | // 105 | // Directive that marks the mouse capture element. 106 | // 107 | .directive('mouseCapture', function () { 108 | return { 109 | restrict: 'A', 110 | 111 | controller: ['$scope', '$element', '$attrs', 'mouseCapture', 112 | function($scope, $element, $attrs, mouseCapture) { 113 | 114 | // 115 | // Register the directives element as the mouse capture element. 116 | // 117 | mouseCapture.registerElement($element); 118 | 119 | }], 120 | 121 | }; 122 | }) 123 | ; 124 | 125 | -------------------------------------------------------------------------------- /flowchart/svg_class.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('svg_class', function () { 3 | 4 | it('removeClassSVG returns false when there is no classes attr', function () { 5 | 6 | var mockElement = { 7 | attr: function () { 8 | return null; 9 | }, 10 | }; 11 | var testClass = 'foo'; 12 | 13 | expect(removeClassSVG(mockElement, testClass)).toBe(false); 14 | 15 | }); 16 | 17 | it('removeClassSVG returns false when the element doesnt already have the class', function () { 18 | 19 | var mockElement = { 20 | attr: function () { 21 | return 'smeg'; 22 | }, 23 | }; 24 | var testClass = 'foo'; 25 | 26 | expect(removeClassSVG(mockElement, testClass)).toBe(false); 27 | 28 | }); 29 | 30 | it('removeClassSVG returns true and removes the class when the element does have the class', function () { 31 | 32 | var testClass = 'foo'; 33 | 34 | var mockElement = { 35 | attr: function () { 36 | return testClass; 37 | }, 38 | }; 39 | 40 | spyOn(mockElement, 'attr').andCallThrough(); 41 | 42 | expect(removeClassSVG(mockElement, testClass)).toBe(true); 43 | expect(mockElement.attr).toHaveBeenCalledWith('class', ''); 44 | 45 | }); 46 | 47 | it('hasClassSVG returns false when attr returns null', function () { 48 | 49 | var mockElement = { 50 | attr: function () { 51 | return null; 52 | }, 53 | }; 54 | 55 | var testClass = 'foo'; 56 | 57 | expect(hasClassSVG(mockElement, testClass)).toBe(false); 58 | 59 | }); 60 | 61 | it('hasClassSVG returns false when element has no class', function () { 62 | 63 | var mockElement = { 64 | attr: function () { 65 | return ''; 66 | }, 67 | }; 68 | 69 | var testClass = 'foo'; 70 | 71 | expect(hasClassSVG(mockElement, testClass)).toBe(false); 72 | 73 | }); 74 | 75 | it('hasClassSVG returns false when element has wrong class', function () { 76 | 77 | var mockElement = { 78 | attr: function () { 79 | return 'smeg'; 80 | }, 81 | }; 82 | 83 | var testClass = 'foo'; 84 | 85 | expect(hasClassSVG(mockElement, testClass)).toBe(false); 86 | 87 | }); 88 | 89 | it('hasClassSVG returns true when element has correct class', function () { 90 | 91 | var testClass = 'foo'; 92 | 93 | var mockElement = { 94 | attr: function () { 95 | return testClass; 96 | }, 97 | }; 98 | 99 | expect(hasClassSVG(mockElement, testClass)).toBe(true); 100 | 101 | }); 102 | 103 | it('hasClassSVG returns true when element 1 correct class of many ', function () { 104 | 105 | var testClass = 'foo'; 106 | 107 | var mockElement = { 108 | attr: function () { 109 | return "whar " + testClass + " smeg"; 110 | }, 111 | }; 112 | 113 | expect(hasClassSVG(mockElement, testClass)).toBe(true); 114 | }); 115 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AngularJS-FlowChart 2 | =================== 3 | 4 | A WebUI control for visualizing and editing flow charts. 5 | 6 | This isn't designed to be completely general purpose, but it will be a good basis if you need an SVG flowchart and you are willing to work with AngularJS. 7 | 8 | [Click here to support my work](https://www.codecapers.com.au/about#support-my-work) 9 | 10 | Code Project Article 11 | -------------------- 12 | 13 | http://www.codeproject.com/Articles/709340/Implementing-a-Flowchart-with-SVG-and-AngularJS 14 | 15 | 16 | How to use it 17 | ------------- 18 | 19 | Include the following Javascript in your HTML file: 20 | 21 | ```html 22 | 23 | 24 | 25 | 26 | 27 | ``` 28 | 29 | Make a dependency on the the flowchart's AngularJS module from your application (or other module): 30 | 31 | ```javascript 32 | angular.module('app', ['flowChart', ]) 33 | ``` 34 | 35 | In your application (or other) controller setup a data-model for the initial flowchart (or AJAX the data-model in from a JSON resource): 36 | 37 | ```javascript 38 | var chartDataModel = { 39 | 40 | nodes: [ 41 | { 42 | name: "Example Node 1", 43 | id: 0, 44 | x: 0, 45 | y: 0, 46 | inputConnectors: [ 47 | { 48 | name: "A", 49 | }, 50 | { 51 | name: "B", 52 | }, 53 | { 54 | name: "C", 55 | }, 56 | ], 57 | outputConnectors: [ 58 | { 59 | name: "A", 60 | }, 61 | { 62 | name: "B", 63 | }, 64 | { 65 | name: "C", 66 | }, 67 | ], 68 | }, 69 | 70 | { 71 | name: "Example Node 2", 72 | id: 1, 73 | x: 400, 74 | y: 200, 75 | inputConnectors: [ 76 | { 77 | name: "A", 78 | }, 79 | { 80 | name: "B", 81 | }, 82 | { 83 | name: "C", 84 | }, 85 | ], 86 | outputConnectors: [ 87 | { 88 | name: "A", 89 | }, 90 | { 91 | name: "B", 92 | }, 93 | { 94 | name: "C", 95 | }, 96 | ], 97 | }, 98 | 99 | ], 100 | 101 | connections: [ 102 | { 103 | source: { 104 | nodeID: 0, 105 | connectorIndex: 1, 106 | }, 107 | 108 | dest: { 109 | nodeID: 1, 110 | connectorIndex: 2, 111 | }, 112 | }, 113 | 114 | 115 | ] 116 | }; 117 | ``` 118 | 119 | Also in your controller, wrap the data-model in a view-model and add it to the AngularJS scope: 120 | 121 | ```javascript 122 | $scope.chartViewModel = new flowchart.ChartViewModel(chartDataModel); 123 | ``` 124 | 125 | Your code is in direct control of creation of the view-model, so you can interact with it in almost anyway you want. 126 | 127 | Finally instantiate the flowchart's AngularJS directive in your HTML: 128 | 129 | ```html 130 | 134 | 135 | ``` 136 | 137 | Be sure to bind your view-model as the 'chart' attribute! 138 | 139 | 140 | Have fun and please contribute! 141 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Define the 'app' module. 4 | // 5 | angular.module('app', ['flowChart', ]) 6 | 7 | // 8 | // Simple service to create a prompt. 9 | // 10 | .factory('prompt', function () { 11 | 12 | /* Uncomment the following to test that the prompt service is working as expected. 13 | return function () { 14 | return "Test!"; 15 | } 16 | */ 17 | 18 | // Return the browsers prompt function. 19 | return prompt; 20 | }) 21 | 22 | // 23 | // Application controller. 24 | // 25 | .controller('AppCtrl', ['$scope', 'prompt', function AppCtrl ($scope, prompt) { 26 | 27 | // 28 | // Code for the delete key. 29 | // 30 | var deleteKeyCode = 46; 31 | 32 | // 33 | // Code for control key. 34 | // 35 | var ctrlKeyCode = 17; 36 | 37 | // 38 | // Set to true when the ctrl key is down. 39 | // 40 | var ctrlDown = false; 41 | 42 | // 43 | // Code for A key. 44 | // 45 | var aKeyCode = 65; 46 | 47 | // 48 | // Code for esc key. 49 | // 50 | var escKeyCode = 27; 51 | 52 | // 53 | // Selects the next node id. 54 | // 55 | var nextNodeID = 10; 56 | 57 | // 58 | // Setup the data-model for the chart. 59 | // 60 | var chartDataModel = { 61 | 62 | nodes: [ 63 | { 64 | name: "Example Node 1", 65 | id: 0, 66 | x: 0, 67 | y: 0, 68 | width: 350, 69 | inputConnectors: [ 70 | { 71 | name: "A", 72 | }, 73 | { 74 | name: "B", 75 | }, 76 | { 77 | name: "C", 78 | }, 79 | ], 80 | outputConnectors: [ 81 | { 82 | name: "A", 83 | }, 84 | { 85 | name: "B", 86 | }, 87 | { 88 | name: "C", 89 | }, 90 | ], 91 | }, 92 | 93 | { 94 | name: "Example Node 2", 95 | id: 1, 96 | x: 400, 97 | y: 200, 98 | inputConnectors: [ 99 | { 100 | name: "A", 101 | }, 102 | { 103 | name: "B", 104 | }, 105 | { 106 | name: "C", 107 | }, 108 | ], 109 | outputConnectors: [ 110 | { 111 | name: "A", 112 | }, 113 | { 114 | name: "B", 115 | }, 116 | { 117 | name: "C", 118 | }, 119 | ], 120 | }, 121 | 122 | ], 123 | 124 | connections: [ 125 | { 126 | name:'Connection 1', 127 | source: { 128 | nodeID: 0, 129 | connectorIndex: 1, 130 | }, 131 | 132 | dest: { 133 | nodeID: 1, 134 | connectorIndex: 2, 135 | }, 136 | }, 137 | { 138 | name:'Connection 2', 139 | source: { 140 | nodeID: 0, 141 | connectorIndex: 0, 142 | }, 143 | 144 | dest: { 145 | nodeID: 1, 146 | connectorIndex: 0, 147 | }, 148 | }, 149 | 150 | ] 151 | }; 152 | 153 | // 154 | // Event handler for key-down on the flowchart. 155 | // 156 | $scope.keyDown = function (evt) { 157 | 158 | if (evt.keyCode === ctrlKeyCode) { 159 | 160 | ctrlDown = true; 161 | evt.stopPropagation(); 162 | evt.preventDefault(); 163 | } 164 | }; 165 | 166 | // 167 | // Event handler for key-up on the flowchart. 168 | // 169 | $scope.keyUp = function (evt) { 170 | 171 | if (evt.keyCode === deleteKeyCode) { 172 | // 173 | // Delete key. 174 | // 175 | $scope.chartViewModel.deleteSelected(); 176 | } 177 | 178 | if (evt.keyCode == aKeyCode && ctrlDown) { 179 | // 180 | // Ctrl + A 181 | // 182 | $scope.chartViewModel.selectAll(); 183 | } 184 | 185 | if (evt.keyCode == escKeyCode) { 186 | // Escape. 187 | $scope.chartViewModel.deselectAll(); 188 | } 189 | 190 | if (evt.keyCode === ctrlKeyCode) { 191 | ctrlDown = false; 192 | 193 | evt.stopPropagation(); 194 | evt.preventDefault(); 195 | } 196 | }; 197 | 198 | // 199 | // Add a new node to the chart. 200 | // 201 | $scope.addNewNode = function () { 202 | 203 | var nodeName = prompt("Enter a node name:", "New node"); 204 | if (!nodeName) { 205 | return; 206 | } 207 | 208 | // 209 | // Template for a new node. 210 | // 211 | var newNodeDataModel = { 212 | name: nodeName, 213 | id: nextNodeID++, 214 | x: 0, 215 | y: 0, 216 | inputConnectors: [ 217 | { 218 | name: "X" 219 | }, 220 | { 221 | name: "Y" 222 | }, 223 | { 224 | name: "Z" 225 | } 226 | ], 227 | outputConnectors: [ 228 | { 229 | name: "1" 230 | }, 231 | { 232 | name: "2" 233 | }, 234 | { 235 | name: "3" 236 | } 237 | ], 238 | }; 239 | 240 | $scope.chartViewModel.addNode(newNodeDataModel); 241 | }; 242 | 243 | // 244 | // Add an input connector to selected nodes. 245 | // 246 | $scope.addNewInputConnector = function () { 247 | var connectorName = prompt("Enter a connector name:", "New connector"); 248 | if (!connectorName) { 249 | return; 250 | } 251 | 252 | var selectedNodes = $scope.chartViewModel.getSelectedNodes(); 253 | for (var i = 0; i < selectedNodes.length; ++i) { 254 | var node = selectedNodes[i]; 255 | node.addInputConnector({ 256 | name: connectorName, 257 | }); 258 | } 259 | }; 260 | 261 | // 262 | // Add an output connector to selected nodes. 263 | // 264 | $scope.addNewOutputConnector = function () { 265 | var connectorName = prompt("Enter a connector name:", "New connector"); 266 | if (!connectorName) { 267 | return; 268 | } 269 | 270 | var selectedNodes = $scope.chartViewModel.getSelectedNodes(); 271 | for (var i = 0; i < selectedNodes.length; ++i) { 272 | var node = selectedNodes[i]; 273 | node.addOutputConnector({ 274 | name: connectorName, 275 | }); 276 | } 277 | }; 278 | 279 | // 280 | // Delete selected nodes and connections. 281 | // 282 | $scope.deleteSelected = function () { 283 | 284 | $scope.chartViewModel.deleteSelected(); 285 | }; 286 | 287 | // 288 | // Create the view-model for the chart and attach to the scope. 289 | // 290 | $scope.chartViewModel = new flowchart.ChartViewModel(chartDataModel); 291 | }]) 292 | ; -------------------------------------------------------------------------------- /flowchart/flowchart_template.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 21 | 26 | 27 | 28 | 33 | 43 | 44 | 45 | 46 | 52 | {{node.name()}} 53 | 54 | 55 | 60 | 66 | {{connector.name()}} 67 | 68 | 69 | 75 | 76 | 77 | 82 | 88 | {{connector.name()}} 89 | 90 | 91 | 97 | 98 | 99 | 100 | 101 | 106 | 113 | 114 | 115 | {{connection.name()}} 122 | 123 | 129 | 130 | 131 | 137 | 138 | 139 | 140 | 141 | 144 | 151 | 152 | 153 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /jasmine/lib/jasmine-1.3.1/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | #HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | #HTMLReporter a { text-decoration: none; } 5 | #HTMLReporter a:hover { text-decoration: underline; } 6 | #HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } 7 | #HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } 8 | #HTMLReporter #jasmine_content { position: fixed; right: 100%; } 9 | #HTMLReporter .version { color: #aaaaaa; } 10 | #HTMLReporter .banner { margin-top: 14px; } 11 | #HTMLReporter .duration { color: #aaaaaa; float: right; } 12 | #HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } 13 | #HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } 14 | #HTMLReporter .symbolSummary li.passed { font-size: 14px; } 15 | #HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } 16 | #HTMLReporter .symbolSummary li.failed { line-height: 9px; } 17 | #HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 18 | #HTMLReporter .symbolSummary li.skipped { font-size: 14px; } 19 | #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } 20 | #HTMLReporter .symbolSummary li.pending { line-height: 11px; } 21 | #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } 22 | #HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 23 | #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 24 | #HTMLReporter .runningAlert { background-color: #666666; } 25 | #HTMLReporter .skippedAlert { background-color: #aaaaaa; } 26 | #HTMLReporter .skippedAlert:first-child { background-color: #333333; } 27 | #HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } 28 | #HTMLReporter .passingAlert { background-color: #a6b779; } 29 | #HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } 30 | #HTMLReporter .failingAlert { background-color: #cf867e; } 31 | #HTMLReporter .failingAlert:first-child { background-color: #b03911; } 32 | #HTMLReporter .results { margin-top: 14px; } 33 | #HTMLReporter #details { display: none; } 34 | #HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } 35 | #HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | #HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | #HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | #HTMLReporter.showDetails .summary { display: none; } 39 | #HTMLReporter.showDetails #details { display: block; } 40 | #HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | #HTMLReporter .summary { margin-top: 14px; } 42 | #HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } 43 | #HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } 44 | #HTMLReporter .summary .specSummary.failed a { color: #b03911; } 45 | #HTMLReporter .description + .suite { margin-top: 0; } 46 | #HTMLReporter .suite { margin-top: 14px; } 47 | #HTMLReporter .suite a { color: #333333; } 48 | #HTMLReporter #details .specDetail { margin-bottom: 28px; } 49 | #HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } 50 | #HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } 51 | #HTMLReporter .resultMessage span.result { display: block; } 52 | #HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 53 | 54 | #TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } 55 | #TrivialReporter a:visited, #TrivialReporter a { color: #303; } 56 | #TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } 57 | #TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } 58 | #TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } 59 | #TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } 60 | #TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } 61 | #TrivialReporter .runner.running { background-color: yellow; } 62 | #TrivialReporter .options { text-align: right; font-size: .8em; } 63 | #TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } 64 | #TrivialReporter .suite .suite { margin: 5px; } 65 | #TrivialReporter .suite.passed { background-color: #dfd; } 66 | #TrivialReporter .suite.failed { background-color: #fdd; } 67 | #TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } 68 | #TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } 69 | #TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } 70 | #TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } 71 | #TrivialReporter .spec.skipped { background-color: #bbb; } 72 | #TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } 73 | #TrivialReporter .passed { background-color: #cfc; display: none; } 74 | #TrivialReporter .failed { background-color: #fbb; } 75 | #TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } 76 | #TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } 77 | #TrivialReporter .resultMessage .mismatch { color: black; } 78 | #TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } 79 | #TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } 80 | #TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } 81 | #TrivialReporter #jasmine_content { position: fixed; right: 100%; } 82 | #TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } 83 | -------------------------------------------------------------------------------- /flowchart/flowchart_directive.js: -------------------------------------------------------------------------------- 1 | // 2 | // Flowchart module. 3 | // 4 | angular.module('flowChart', ['dragging'] ) 5 | 6 | // 7 | // Directive that generates the rendered chart from the data model. 8 | // 9 | .directive('flowChart', function() { 10 | return { 11 | restrict: 'E', 12 | templateUrl: "flowchart/flowchart_template.html", 13 | replace: true, 14 | scope: { 15 | chart: "=chart", 16 | }, 17 | 18 | // 19 | // Controller for the flowchart directive. 20 | // Having a separate controller is better for unit testing, otherwise 21 | // it is painful to unit test a directive without instantiating the DOM 22 | // (which is possible, just not ideal). 23 | // 24 | controller: 'FlowChartController', 25 | }; 26 | }) 27 | 28 | // 29 | // Directive that allows the chart to be edited as json in a textarea. 30 | // 31 | .directive('chartJsonEdit', function () { 32 | return { 33 | restrict: 'A', 34 | scope: { 35 | viewModel: "=" 36 | }, 37 | link: function (scope, elem, attr) { 38 | 39 | // 40 | // Serialize the data model as json and update the textarea. 41 | // 42 | var updateJson = function () { 43 | if (scope.viewModel) { 44 | var json = JSON.stringify(scope.viewModel.data, null, 4); 45 | $(elem).val(json); 46 | } 47 | }; 48 | 49 | // 50 | // First up, set the initial value of the textarea. 51 | // 52 | updateJson(); 53 | 54 | // 55 | // Watch for changes in the data model and update the textarea whenever necessary. 56 | // 57 | scope.$watch("viewModel.data", updateJson, true); 58 | 59 | // 60 | // Handle the change event from the textarea and update the data model 61 | // from the modified json. 62 | // 63 | $(elem).bind("input propertychange", function () { 64 | var json = $(elem).val(); 65 | var dataModel = JSON.parse(json); 66 | scope.viewModel = new flowchart.ChartViewModel(dataModel); 67 | 68 | scope.$digest(); 69 | }); 70 | } 71 | } 72 | 73 | }) 74 | 75 | // 76 | // Controller for the flowchart directive. 77 | // Having a separate controller is better for unit testing, otherwise 78 | // it is painful to unit test a directive without instantiating the DOM 79 | // (which is possible, just not ideal). 80 | // 81 | .controller('FlowChartController', ['$scope', 'dragging', '$element', function FlowChartController ($scope, dragging, $element) { 82 | 83 | var controller = this; 84 | 85 | // 86 | // Reference to the document and jQuery, can be overridden for testting. 87 | // 88 | this.document = document; 89 | 90 | // 91 | // Wrap jQuery so it can easily be mocked for testing. 92 | // 93 | this.jQuery = function (element) { 94 | return $(element); 95 | } 96 | 97 | // 98 | // Init data-model variables. 99 | // 100 | $scope.draggingConnection = false; 101 | $scope.connectorSize = 10; 102 | $scope.dragSelecting = false; 103 | /* Can use this to test the drag selection rect. 104 | $scope.dragSelectionRect = { 105 | x: 0, 106 | y: 0, 107 | width: 0, 108 | height: 0, 109 | }; 110 | */ 111 | 112 | // 113 | // Reference to the connection, connector or node that the mouse is currently over. 114 | // 115 | $scope.mouseOverConnector = null; 116 | $scope.mouseOverConnection = null; 117 | $scope.mouseOverNode = null; 118 | 119 | // 120 | // The class for connections and connectors. 121 | // 122 | this.connectionClass = 'connection'; 123 | this.connectorClass = 'connector'; 124 | this.nodeClass = 'node'; 125 | 126 | // 127 | // Search up the HTML element tree for an element the requested class. 128 | // 129 | this.searchUp = function (element, parentClass) { 130 | 131 | // 132 | // Reached the root. 133 | // 134 | if (element == null || element.length == 0) { 135 | return null; 136 | } 137 | 138 | // 139 | // Check if the element has the class that identifies it as a connector. 140 | // 141 | if (hasClassSVG(element, parentClass)) { 142 | // 143 | // Found the connector element. 144 | // 145 | return element; 146 | } 147 | 148 | // 149 | // Recursively search parent elements. 150 | // 151 | return this.searchUp(element.parent(), parentClass); 152 | }; 153 | 154 | // 155 | // Hit test and retreive node and connector that was hit at the specified coordinates. 156 | // 157 | this.hitTest = function (clientX, clientY) { 158 | 159 | // 160 | // Retreive the element the mouse is currently over. 161 | // 162 | return this.document.elementFromPoint(clientX, clientY); 163 | }; 164 | 165 | // 166 | // Hit test and retreive node and connector that was hit at the specified coordinates. 167 | // 168 | this.checkForHit = function (mouseOverElement, whichClass) { 169 | 170 | // 171 | // Find the parent element, if any, that is a connector. 172 | // 173 | var hoverElement = this.searchUp(this.jQuery(mouseOverElement), whichClass); 174 | if (!hoverElement) { 175 | return null; 176 | } 177 | 178 | return hoverElement.scope(); 179 | }; 180 | 181 | // 182 | // Translate the coordinates so they are relative to the svg element. 183 | // 184 | this.translateCoordinates = function(x, y, evt) { 185 | var svg_elem = $element.get(0); 186 | var matrix = svg_elem.getScreenCTM(); 187 | var point = svg_elem.createSVGPoint(); 188 | point.x = x - evt.view.pageXOffset; 189 | point.y = y - evt.view.pageYOffset; 190 | return point.matrixTransform(matrix.inverse()); 191 | }; 192 | 193 | // 194 | // Called on mouse down in the chart. 195 | // 196 | $scope.mouseDown = function (evt) { 197 | 198 | $scope.chart.deselectAll(); 199 | 200 | dragging.startDrag(evt, { 201 | 202 | // 203 | // Commence dragging... setup variables to display the drag selection rect. 204 | // 205 | dragStarted: function (x, y) { 206 | $scope.dragSelecting = true; 207 | var startPoint = controller.translateCoordinates(x, y, evt); 208 | $scope.dragSelectionStartPoint = startPoint; 209 | $scope.dragSelectionRect = { 210 | x: startPoint.x, 211 | y: startPoint.y, 212 | width: 0, 213 | height: 0, 214 | }; 215 | }, 216 | 217 | // 218 | // Update the drag selection rect while dragging continues. 219 | // 220 | dragging: function (x, y) { 221 | var startPoint = $scope.dragSelectionStartPoint; 222 | var curPoint = controller.translateCoordinates(x, y, evt); 223 | 224 | $scope.dragSelectionRect = { 225 | x: curPoint.x > startPoint.x ? startPoint.x : curPoint.x, 226 | y: curPoint.y > startPoint.y ? startPoint.y : curPoint.y, 227 | width: curPoint.x > startPoint.x ? curPoint.x - startPoint.x : startPoint.x - curPoint.x, 228 | height: curPoint.y > startPoint.y ? curPoint.y - startPoint.y : startPoint.y - curPoint.y, 229 | }; 230 | }, 231 | 232 | // 233 | // Dragging has ended... select all that are within the drag selection rect. 234 | // 235 | dragEnded: function () { 236 | $scope.dragSelecting = false; 237 | $scope.chart.applySelectionRect($scope.dragSelectionRect); 238 | delete $scope.dragSelectionStartPoint; 239 | delete $scope.dragSelectionRect; 240 | }, 241 | }); 242 | }; 243 | 244 | // 245 | // Called for each mouse move on the svg element. 246 | // 247 | $scope.mouseMove = function (evt) { 248 | 249 | // 250 | // Clear out all cached mouse over elements. 251 | // 252 | $scope.mouseOverConnection = null; 253 | $scope.mouseOverConnector = null; 254 | $scope.mouseOverNode = null; 255 | 256 | var mouseOverElement = controller.hitTest(evt.clientX, evt.clientY); 257 | if (mouseOverElement == null) { 258 | // Mouse isn't over anything, just clear all. 259 | return; 260 | } 261 | 262 | if (!$scope.draggingConnection) { // Only allow 'connection mouse over' when not dragging out a connection. 263 | 264 | // Figure out if the mouse is over a connection. 265 | var scope = controller.checkForHit(mouseOverElement, controller.connectionClass); 266 | $scope.mouseOverConnection = (scope && scope.connection) ? scope.connection : null; 267 | if ($scope.mouseOverConnection) { 268 | // Don't attempt to mouse over anything else. 269 | return; 270 | } 271 | } 272 | 273 | // Figure out if the mouse is over a connector. 274 | var scope = controller.checkForHit(mouseOverElement, controller.connectorClass); 275 | $scope.mouseOverConnector = (scope && scope.connector) ? scope.connector : null; 276 | if ($scope.mouseOverConnector) { 277 | // Don't attempt to mouse over anything else. 278 | return; 279 | } 280 | 281 | // Figure out if the mouse is over a node. 282 | var scope = controller.checkForHit(mouseOverElement, controller.nodeClass); 283 | $scope.mouseOverNode = (scope && scope.node) ? scope.node : null; 284 | }; 285 | 286 | // 287 | // Handle mousedown on a node. 288 | // 289 | $scope.nodeMouseDown = function (evt, node) { 290 | 291 | var chart = $scope.chart; 292 | var lastMouseCoords; 293 | 294 | dragging.startDrag(evt, { 295 | 296 | // 297 | // Node dragging has commenced. 298 | // 299 | dragStarted: function (x, y) { 300 | 301 | lastMouseCoords = controller.translateCoordinates(x, y, evt); 302 | 303 | // 304 | // If nothing is selected when dragging starts, 305 | // at least select the node we are dragging. 306 | // 307 | if (!node.selected()) { 308 | chart.deselectAll(); 309 | node.select(); 310 | } 311 | }, 312 | 313 | // 314 | // Dragging selected nodes... update their x,y coordinates. 315 | // 316 | dragging: function (x, y) { 317 | 318 | var curCoords = controller.translateCoordinates(x, y, evt); 319 | var deltaX = curCoords.x - lastMouseCoords.x; 320 | var deltaY = curCoords.y - lastMouseCoords.y; 321 | 322 | chart.updateSelectedNodesLocation(deltaX, deltaY); 323 | 324 | lastMouseCoords = curCoords; 325 | }, 326 | 327 | // 328 | // The node wasn't dragged... it was clicked. 329 | // 330 | clicked: function () { 331 | chart.handleNodeClicked(node, evt.ctrlKey); 332 | }, 333 | 334 | }); 335 | }; 336 | 337 | // 338 | // Handle mousedown on a connection. 339 | // 340 | $scope.connectionMouseDown = function (evt, connection) { 341 | var chart = $scope.chart; 342 | chart.handleConnectionMouseDown(connection, evt.ctrlKey); 343 | 344 | // Don't let the chart handle the mouse down. 345 | evt.stopPropagation(); 346 | evt.preventDefault(); 347 | }; 348 | 349 | // 350 | // Handle mousedown on an input connector. 351 | // 352 | $scope.connectorMouseDown = function (evt, node, connector, connectorIndex, isInputConnector) { 353 | 354 | // 355 | // Initiate dragging out of a connection. 356 | // 357 | dragging.startDrag(evt, { 358 | 359 | // 360 | // Called when the mouse has moved greater than the threshold distance 361 | // and dragging has commenced. 362 | // 363 | dragStarted: function (x, y) { 364 | 365 | var curCoords = controller.translateCoordinates(x, y, evt); 366 | 367 | $scope.draggingConnection = true; 368 | $scope.dragPoint1 = flowchart.computeConnectorPos(node, connectorIndex, isInputConnector); 369 | $scope.dragPoint2 = { 370 | x: curCoords.x, 371 | y: curCoords.y 372 | }; 373 | $scope.dragTangent1 = flowchart.computeConnectionSourceTangent($scope.dragPoint1, $scope.dragPoint2); 374 | $scope.dragTangent2 = flowchart.computeConnectionDestTangent($scope.dragPoint1, $scope.dragPoint2); 375 | }, 376 | 377 | // 378 | // Called on mousemove while dragging out a connection. 379 | // 380 | dragging: function (x, y, evt) { 381 | var startCoords = controller.translateCoordinates(x, y, evt); 382 | $scope.dragPoint1 = flowchart.computeConnectorPos(node, connectorIndex, isInputConnector); 383 | $scope.dragPoint2 = { 384 | x: startCoords.x, 385 | y: startCoords.y 386 | }; 387 | $scope.dragTangent1 = flowchart.computeConnectionSourceTangent($scope.dragPoint1, $scope.dragPoint2); 388 | $scope.dragTangent2 = flowchart.computeConnectionDestTangent($scope.dragPoint1, $scope.dragPoint2); 389 | }, 390 | 391 | // 392 | // Clean up when dragging has finished. 393 | // 394 | dragEnded: function () { 395 | 396 | if ($scope.mouseOverConnector && 397 | $scope.mouseOverConnector !== connector) { 398 | 399 | // 400 | // Dragging has ended... 401 | // The mouse is over a valid connector... 402 | // Create a new connection. 403 | // 404 | $scope.chart.createNewConnection(connector, $scope.mouseOverConnector); 405 | } 406 | 407 | $scope.draggingConnection = false; 408 | delete $scope.dragPoint1; 409 | delete $scope.dragTangent1; 410 | delete $scope.dragPoint2; 411 | delete $scope.dragTangent2; 412 | }, 413 | 414 | }); 415 | }; 416 | }]) 417 | ; 418 | -------------------------------------------------------------------------------- /flowchart/flowchart_directive.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('flowchart-directive', function () { 3 | 4 | var testObject; 5 | var mockScope; 6 | var mockDragging; 7 | var mockSvgElement; 8 | 9 | // 10 | // Bring in the flowChart module before each test. 11 | // 12 | beforeEach(module('flowChart')); 13 | 14 | // 15 | // Helper function to create the controller for each test. 16 | // 17 | var createController = function ($rootScope, $controller) { 18 | 19 | mockScope = $rootScope.$new(); 20 | mockDragging = createMockDragging(); 21 | mockSvgElement = { 22 | get: function () { 23 | return createMockSvgElement(); 24 | } 25 | }; 26 | 27 | testObject = $controller('FlowChartController', { 28 | $scope: mockScope, 29 | dragging: mockDragging, 30 | $element: mockSvgElement, 31 | }); 32 | }; 33 | 34 | // 35 | // Setup the controller before each test. 36 | // 37 | beforeEach(inject(function ($rootScope, $controller) { 38 | 39 | createController($rootScope, $controller); 40 | })); 41 | 42 | // 43 | // Create a mock DOM element. 44 | // 45 | var createMockElement = function(attr, parent, scope) { 46 | return { 47 | attr: function() { 48 | return attr; 49 | }, 50 | 51 | parent: function () { 52 | return parent; 53 | }, 54 | 55 | scope: function () { 56 | return scope || {}; 57 | }, 58 | 59 | }; 60 | } 61 | 62 | // 63 | // Create a mock node data model. 64 | // 65 | var createMockNode = function (inputConnectors, outputConnectors) { 66 | return { 67 | x: function () { return 0 }, 68 | y: function () { return 0 }, 69 | inputConnectors: inputConnectors || [], 70 | outputConnectors: outputConnectors || [], 71 | select: jasmine.createSpy(), 72 | selected: function () { return false; }, 73 | }; 74 | }; 75 | 76 | // 77 | // Create a mock chart. 78 | // 79 | var createMockChart = function (mockNodes, mockConnections) { 80 | return { 81 | nodes: mockNodes, 82 | connections: mockConnections, 83 | 84 | handleNodeClicked: jasmine.createSpy(), 85 | handleConnectionMouseDown: jasmine.createSpy(), 86 | updateSelectedNodesLocation: jasmine.createSpy(), 87 | deselectAll: jasmine.createSpy(), 88 | createNewConnection: jasmine.createSpy(), 89 | applySelectionRect: jasmine.createSpy(), 90 | }; 91 | }; 92 | 93 | // 94 | // Create a mock dragging service. 95 | // 96 | var createMockDragging = function () { 97 | 98 | var mockDragging = { 99 | startDrag: function (evt, config) { 100 | mockDragging.evt = evt; 101 | mockDragging.config = config; 102 | }, 103 | }; 104 | 105 | return mockDragging; 106 | }; 107 | 108 | // 109 | // Create a mock version of the SVG element. 110 | // 111 | var createMockSvgElement = function () { 112 | return { 113 | getScreenCTM: function () { 114 | return { 115 | inverse: function () { 116 | return this; 117 | }, 118 | }; 119 | }, 120 | 121 | createSVGPoint: function () { 122 | return { 123 | x: 0, 124 | y: 0 , 125 | matrixTransform: function () { 126 | return this; 127 | }, 128 | }; 129 | } 130 | 131 | 132 | 133 | }; 134 | }; 135 | 136 | it('searchUp returns null when at root 1', function () { 137 | 138 | expect(testObject.searchUp(null, "some-class")).toBe(null); 139 | }); 140 | 141 | 142 | it('searchUp returns null when at root 2', function () { 143 | 144 | expect(testObject.searchUp([], "some-class")).toBe(null); 145 | }); 146 | 147 | it('searchUp returns element when it has requested class', function () { 148 | 149 | var whichClass = "some-class"; 150 | var mockElement = createMockElement(whichClass); 151 | 152 | expect(testObject.searchUp(mockElement, whichClass)).toBe(mockElement); 153 | }); 154 | 155 | it('searchUp returns parent when it has requested class', function () { 156 | 157 | var whichClass = "some-class"; 158 | var mockParent = createMockElement(whichClass); 159 | var mockElement = createMockElement('', mockParent); 160 | 161 | expect(testObject.searchUp(mockElement, whichClass)).toBe(mockParent); 162 | }); 163 | 164 | it('hitTest returns result of elementFromPoint', function () { 165 | 166 | var mockElement = {}; 167 | 168 | // Mock out the document. 169 | testObject.document = { 170 | elementFromPoint: function () { 171 | return mockElement; 172 | }, 173 | }; 174 | 175 | expect(testObject.hitTest(12, 30)).toBe(mockElement); 176 | }); 177 | 178 | it('checkForHit returns null when the hit element has no parent with requested class', function () { 179 | 180 | var mockElement = createMockElement(null, null); 181 | 182 | testObject.jQuery = function (input) { 183 | return input; 184 | }; 185 | 186 | expect(testObject.checkForHit(mockElement, "some-class")).toBe(null); 187 | }); 188 | 189 | it('checkForHit returns the result of searchUp when found', function () { 190 | 191 | var mockConnectorScope = {}; 192 | 193 | var whichClass = "some-class"; 194 | var mockElement = createMockElement(whichClass, null, mockConnectorScope); 195 | 196 | testObject.jQuery = function (input) { 197 | return input; 198 | }; 199 | 200 | expect(testObject.checkForHit(mockElement, whichClass)).toBe(mockConnectorScope); 201 | }); 202 | 203 | it('checkForHit returns null when searchUp fails', function () { 204 | 205 | var mockElement = createMockElement(null, null, null); 206 | 207 | testObject.jQuery = function (input) { 208 | return input; 209 | }; 210 | 211 | expect(testObject.checkForHit(mockElement, "some-class")).toBe(null); 212 | }); 213 | 214 | it('test node dragging is started on node mouse down', function () { 215 | 216 | mockDragging.startDrag = jasmine.createSpy(); 217 | 218 | var mockEvt = {}; 219 | var mockNode = createMockNode(); 220 | 221 | mockScope.nodeMouseDown(mockEvt, mockNode); 222 | 223 | expect(mockDragging.startDrag).toHaveBeenCalled(); 224 | 225 | }); 226 | 227 | it('test node click handling is forwarded to view model', function () { 228 | 229 | mockScope.chart = createMockChart([mockNode]); 230 | 231 | var mockEvt = { 232 | ctrlKey: false, 233 | }; 234 | var mockNode = createMockNode(); 235 | 236 | mockScope.nodeMouseDown(mockEvt, mockNode); 237 | 238 | mockDragging.config.clicked(); 239 | 240 | expect(mockScope.chart.handleNodeClicked).toHaveBeenCalledWith(mockNode, false); 241 | }); 242 | 243 | it('test control + node click handling is forwarded to view model', function () { 244 | 245 | var mockNode = createMockNode(); 246 | 247 | mockScope.chart = createMockChart([mockNode]); 248 | 249 | var mockEvt = { 250 | ctrlKey: true, 251 | }; 252 | 253 | mockScope.nodeMouseDown(mockEvt, mockNode); 254 | 255 | mockDragging.config.clicked(); 256 | 257 | expect(mockScope.chart.handleNodeClicked).toHaveBeenCalledWith(mockNode, true); 258 | }); 259 | 260 | it('test node dragging updates selected nodes location', function () { 261 | 262 | var mockEvt = { 263 | view: { 264 | pageXOffset: 0, 265 | pageYOffset: 0, 266 | }, 267 | }; 268 | 269 | mockScope.chart = createMockChart([createMockNode()]); 270 | 271 | mockScope.nodeMouseDown(mockEvt, mockScope.chart.nodes[0]); 272 | 273 | var xIncrement = 5; 274 | var yIncrement = 15; 275 | 276 | mockDragging.config.dragStarted(0, 0); 277 | mockDragging.config.dragging(xIncrement, yIncrement); 278 | 279 | expect(mockScope.chart.updateSelectedNodesLocation).toHaveBeenCalledWith(xIncrement, yIncrement); 280 | }); 281 | 282 | it('test node dragging doesnt modify selection when node is already selected', function () { 283 | 284 | var mockNode1 = createMockNode(); 285 | var mockNode2 = createMockNode(); 286 | 287 | mockScope.chart = createMockChart([mockNode1, mockNode2]); 288 | 289 | mockNode2.selected = function () { return true; } 290 | 291 | var mockEvt = { 292 | view: { 293 | scrollX: 0, 294 | scrollY: 0, 295 | }, 296 | }; 297 | 298 | mockScope.nodeMouseDown(mockEvt, mockNode2); 299 | 300 | mockDragging.config.dragStarted(0, 0); 301 | 302 | expect(mockScope.chart.deselectAll).not.toHaveBeenCalled(); 303 | }); 304 | 305 | it('test node dragging selects node, when the node is not already selected', function () { 306 | 307 | var mockNode1 = createMockNode(); 308 | var mockNode2 = createMockNode(); 309 | 310 | mockScope.chart = createMockChart([mockNode1, mockNode2]); 311 | 312 | var mockEvt = { 313 | view: { 314 | scrollX: 0, 315 | scrollY: 0, 316 | }, 317 | }; 318 | 319 | mockScope.nodeMouseDown(mockEvt, mockNode2); 320 | 321 | mockDragging.config.dragStarted(0, 0); 322 | 323 | expect(mockScope.chart.deselectAll).toHaveBeenCalled(); 324 | expect(mockNode2.select).toHaveBeenCalled(); 325 | }); 326 | 327 | it('test connection click handling is forwarded to view model', function () { 328 | 329 | var mockNode = createMockNode(); 330 | 331 | var mockEvt = { 332 | stopPropagation: jasmine.createSpy(), 333 | preventDefault: jasmine.createSpy(), 334 | ctrlKey: false, 335 | }; 336 | var mockConnection = {}; 337 | 338 | mockScope.chart = createMockChart([mockNode]); 339 | 340 | mockScope.connectionMouseDown(mockEvt, mockConnection); 341 | 342 | expect(mockScope.chart.handleConnectionMouseDown).toHaveBeenCalledWith(mockConnection, false); 343 | expect(mockEvt.stopPropagation).toHaveBeenCalled(); 344 | expect(mockEvt.preventDefault).toHaveBeenCalled(); 345 | }); 346 | 347 | it('test control + connection click handling is forwarded to view model', function () { 348 | 349 | var mockNode = createMockNode(); 350 | 351 | var mockEvt = { 352 | stopPropagation: jasmine.createSpy(), 353 | preventDefault: jasmine.createSpy(), 354 | ctrlKey: true, 355 | }; 356 | var mockConnection = {}; 357 | 358 | mockScope.chart = createMockChart([mockNode]); 359 | 360 | mockScope.connectionMouseDown(mockEvt, mockConnection); 361 | 362 | expect(mockScope.chart.handleConnectionMouseDown).toHaveBeenCalledWith(mockConnection, true); 363 | }); 364 | 365 | it('test selection is cleared when background is clicked', function () { 366 | 367 | var mockEvt = {}; 368 | 369 | mockScope.chart = createMockChart([createMockNode()]); 370 | 371 | mockScope.chart.nodes[0].selected = true; 372 | 373 | mockScope.mouseDown(mockEvt); 374 | 375 | expect(mockScope.chart.deselectAll).toHaveBeenCalled(); 376 | }); 377 | 378 | it('test background mouse down commences selection dragging', function () { 379 | 380 | var mockNode = createMockNode(); 381 | var mockEvt = { 382 | view: { 383 | scrollX: 0, 384 | scrollY: 0, 385 | }, 386 | }; 387 | 388 | mockScope.chart = createMockChart([mockNode]); 389 | 390 | mockScope.mouseDown(mockEvt); 391 | 392 | mockDragging.config.dragStarted(0, 0); 393 | 394 | expect(mockScope.dragSelecting).toBe(true); 395 | }); 396 | 397 | it('test can end selection dragging', function () { 398 | 399 | var mockNode = createMockNode(); 400 | var mockEvt = { 401 | view: { 402 | scrollX: 0, 403 | scrollY: 0, 404 | }, 405 | }; 406 | 407 | mockScope.chart = createMockChart([mockNode]); 408 | 409 | mockScope.mouseDown(mockEvt); 410 | 411 | mockDragging.config.dragStarted(0, 0, mockEvt); 412 | mockDragging.config.dragging(0, 0, mockEvt); 413 | mockDragging.config.dragEnded(); 414 | 415 | expect(mockScope.dragSelecting).toBe(false); 416 | }); 417 | 418 | it('test selection dragging ends by selecting nodes', function () { 419 | 420 | var mockNode = createMockNode(); 421 | var mockEvt = { 422 | view: { 423 | scrollX: 0, 424 | scrollY: 0, 425 | }, 426 | }; 427 | 428 | mockScope.chart = createMockChart([mockNode]); 429 | 430 | mockScope.mouseDown(mockEvt); 431 | 432 | mockDragging.config.dragStarted(0, 0, mockEvt); 433 | mockDragging.config.dragging(0, 0, mockEvt); 434 | 435 | var selectionRect = { 436 | x: 1, 437 | y: 2, 438 | width: 3, 439 | height: 4, 440 | }; 441 | 442 | mockScope.dragSelectionRect = selectionRect; 443 | 444 | mockDragging.config.dragEnded(); 445 | 446 | expect(mockScope.chart.applySelectionRect).toHaveBeenCalledWith(selectionRect); 447 | }); 448 | 449 | it('test mouse down commences connection dragging', function () { 450 | 451 | var mockNode = createMockNode(); 452 | var mockEvt = { 453 | view: { 454 | scrollX: 0, 455 | scrollY: 0, 456 | }, 457 | }; 458 | 459 | mockScope.chart = createMockChart([mockNode]); 460 | 461 | mockScope.connectorMouseDown(mockEvt, mockScope.chart.nodes[0], mockScope.chart.nodes[0].inputConnectors[0], 0, false); 462 | 463 | mockDragging.config.dragStarted(0, 0); 464 | 465 | expect(mockScope.draggingConnection).toBe(true); 466 | }); 467 | 468 | it('test can end connection dragging', function () { 469 | 470 | var mockNode = createMockNode(); 471 | var mockEvt = { 472 | view: { 473 | scrollX: 0, 474 | scrollY: 0, 475 | }, 476 | }; 477 | 478 | mockScope.chart = createMockChart([mockNode]); 479 | 480 | mockScope.connectorMouseDown(mockEvt, mockScope.chart.nodes[0], mockScope.chart.nodes[0].inputConnectors[0], 0, false); 481 | 482 | mockDragging.config.dragStarted(0, 0, mockEvt); 483 | mockDragging.config.dragging(0, 0, mockEvt); 484 | mockDragging.config.dragEnded(); 485 | 486 | expect(mockScope.draggingConnection).toBe(false); 487 | }); 488 | 489 | it('test can make a connection by dragging', function () { 490 | 491 | var mockNode = createMockNode(); 492 | var mockDraggingConnector = {}; 493 | var mockDragOverConnector = {}; 494 | var mockEvt = { 495 | view: { 496 | scrollX: 0, 497 | scrollY: 0, 498 | }, 499 | }; 500 | 501 | mockScope.chart = createMockChart([mockNode]); 502 | 503 | mockScope.connectorMouseDown(mockEvt, mockScope.chart.nodes[0], mockDraggingConnector, 0, false); 504 | 505 | mockDragging.config.dragStarted(0, 0, mockEvt); 506 | mockDragging.config.dragging(0, 0, mockEvt); 507 | 508 | // Fake out the mouse over connector. 509 | mockScope.mouseOverConnector = mockDragOverConnector; 510 | 511 | mockDragging.config.dragEnded(); 512 | 513 | expect(mockScope.chart.createNewConnection).toHaveBeenCalledWith(mockDraggingConnector, mockDragOverConnector); 514 | }); 515 | 516 | it('test connection creation by dragging is cancelled when dragged over invalid connector', function () { 517 | 518 | var mockNode = createMockNode(); 519 | var mockDraggingConnector = {}; 520 | var mockEvt = { 521 | view: { 522 | scrollX: 0, 523 | scrollY: 0, 524 | }, 525 | }; 526 | 527 | mockScope.chart = createMockChart([mockNode]); 528 | 529 | mockScope.connectorMouseDown(mockEvt, mockScope.chart.nodes[0], mockDraggingConnector, 0, false); 530 | 531 | mockDragging.config.dragStarted(0, 0, mockEvt); 532 | mockDragging.config.dragging(0, 0, mockEvt); 533 | 534 | // Fake out the invalid connector. 535 | mockScope.mouseOverConnector = null; 536 | 537 | mockDragging.config.dragEnded(); 538 | 539 | expect(mockScope.chart.createNewConnection).not.toHaveBeenCalled(); 540 | }); 541 | 542 | it('mouse move over connection caches the connection', function () { 543 | 544 | var mockElement = {}; 545 | var mockConnection = {}; 546 | var mockConnectionScope = { 547 | connection: mockConnection 548 | }; 549 | var mockEvent = {}; 550 | 551 | // 552 | // Fake out the function that check if a connection has been hit. 553 | // 554 | testObject.checkForHit = function (element, whichClass) { 555 | if (whichClass === testObject.connectionClass) { 556 | return mockConnectionScope; 557 | } 558 | 559 | return null; 560 | }; 561 | 562 | testObject.hitTest = function () { 563 | return mockElement; 564 | }; 565 | 566 | mockScope.mouseMove(mockEvent); 567 | 568 | expect(mockScope.mouseOverConnection).toBe(mockConnection); 569 | }); 570 | 571 | it('test mouse over connection clears mouse over connector and node', function () { 572 | 573 | var mockElement = {}; 574 | var mockConnection = {}; 575 | var mockConnectionScope = { 576 | connection: mockConnection 577 | }; 578 | var mockEvent = {}; 579 | 580 | // 581 | // Fake out the function that check if a connection has been hit. 582 | // 583 | testObject.checkForHit = function (element, whichClass) { 584 | if (whichClass === testObject.connectionClass) { 585 | return mockConnectionScope; 586 | } 587 | 588 | return null; 589 | }; 590 | 591 | testObject.hitTest = function () { 592 | return mockElement; 593 | }; 594 | 595 | 596 | mockScope.mouseOverConnector = {}; 597 | mockScope.mouseOverNode = {}; 598 | 599 | mockScope.mouseMove(mockEvent); 600 | 601 | expect(mockScope.mouseOverConnector).toBe(null); 602 | expect(mockScope.mouseOverNode).toBe(null); 603 | }); 604 | 605 | it('test mouseMove handles mouse over connector', function () { 606 | 607 | var mockElement = {}; 608 | var mockConnector = {}; 609 | var mockConnectorScope = { 610 | connector: mockConnector 611 | }; 612 | var mockEvent = {}; 613 | 614 | // 615 | // Fake out the function that check if a connector has been hit. 616 | // 617 | testObject.checkForHit = function (element, whichClass) { 618 | if (whichClass === testObject.connectorClass) { 619 | return mockConnectorScope; 620 | } 621 | 622 | return null; 623 | }; 624 | 625 | testObject.hitTest = function () { 626 | return mockElement; 627 | }; 628 | 629 | mockScope.mouseMove(mockEvent); 630 | 631 | expect(mockScope.mouseOverConnector).toBe(mockConnector); 632 | }); 633 | 634 | it('test mouseMove handles mouse over node', function () { 635 | 636 | var mockElement = {}; 637 | var mockNode = {}; 638 | var mockNodeScope = { 639 | node: mockNode 640 | }; 641 | var mockEvent = {}; 642 | 643 | // 644 | // Fake out the function that check if a connector has been hit. 645 | // 646 | testObject.checkForHit = function (element, whichClass) { 647 | if (whichClass === testObject.nodeClass) { 648 | return mockNodeScope; 649 | } 650 | 651 | return null; 652 | }; 653 | 654 | testObject.hitTest = function () { 655 | return mockElement; 656 | }; 657 | 658 | mockScope.mouseMove(mockEvent); 659 | 660 | expect(mockScope.mouseOverNode).toBe(mockNode); 661 | }); 662 | }); 663 | -------------------------------------------------------------------------------- /flowchart/flowchart_viewmodel.js: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Global accessor. 4 | // 5 | var flowchart = { 6 | 7 | }; 8 | 9 | // Module. 10 | (function () { 11 | 12 | // 13 | // Width of a node. 14 | // 15 | flowchart.defaultNodeWidth = 250; 16 | 17 | // 18 | // Amount of space reserved for displaying the node's name. 19 | // 20 | flowchart.nodeNameHeight = 40; 21 | 22 | // 23 | // Height of a connector in a node. 24 | // 25 | flowchart.connectorHeight = 35; 26 | 27 | // 28 | // Compute the Y coordinate of a connector, given its index. 29 | // 30 | flowchart.computeConnectorY = function (connectorIndex) { 31 | return flowchart.nodeNameHeight + (connectorIndex * flowchart.connectorHeight); 32 | } 33 | 34 | // 35 | // Compute the position of a connector in the graph. 36 | // 37 | flowchart.computeConnectorPos = function (node, connectorIndex, inputConnector) { 38 | return { 39 | x: node.x() + (inputConnector ? 0 : node.width ? node.width() : flowchart.defaultNodeWidth), 40 | y: node.y() + flowchart.computeConnectorY(connectorIndex), 41 | }; 42 | }; 43 | 44 | // 45 | // View model for a connector. 46 | // 47 | flowchart.ConnectorViewModel = function (connectorDataModel, x, y, parentNode) { 48 | 49 | this.data = connectorDataModel; 50 | this._parentNode = parentNode; 51 | this._x = x; 52 | this._y = y; 53 | 54 | // 55 | // The name of the connector. 56 | // 57 | this.name = function () { 58 | return this.data.name; 59 | } 60 | 61 | // 62 | // X coordinate of the connector. 63 | // 64 | this.x = function () { 65 | return this._x; 66 | }; 67 | 68 | // 69 | // Y coordinate of the connector. 70 | // 71 | this.y = function () { 72 | return this._y; 73 | }; 74 | 75 | // 76 | // The parent node that the connector is attached to. 77 | // 78 | this.parentNode = function () { 79 | return this._parentNode; 80 | }; 81 | }; 82 | 83 | // 84 | // Create view model for a list of data models. 85 | // 86 | var createConnectorsViewModel = function (connectorDataModels, x, parentNode) { 87 | var viewModels = []; 88 | 89 | if (connectorDataModels) { 90 | for (var i = 0; i < connectorDataModels.length; ++i) { 91 | var connectorViewModel = 92 | new flowchart.ConnectorViewModel(connectorDataModels[i], x, flowchart.computeConnectorY(i), parentNode); 93 | viewModels.push(connectorViewModel); 94 | } 95 | } 96 | 97 | return viewModels; 98 | }; 99 | 100 | // 101 | // View model for a node. 102 | // 103 | flowchart.NodeViewModel = function (nodeDataModel) { 104 | 105 | this.data = nodeDataModel; 106 | 107 | // set the default width value of the node 108 | if (!this.data.width || this.data.width < 0) { 109 | this.data.width = flowchart.defaultNodeWidth; 110 | } 111 | this.inputConnectors = createConnectorsViewModel(this.data.inputConnectors, 0, this); 112 | this.outputConnectors = createConnectorsViewModel(this.data.outputConnectors, this.data.width, this); 113 | 114 | // Set to true when the node is selected. 115 | this._selected = false; 116 | 117 | // 118 | // Name of the node. 119 | // 120 | this.name = function () { 121 | return this.data.name || ""; 122 | }; 123 | 124 | // 125 | // X coordinate of the node. 126 | // 127 | this.x = function () { 128 | return this.data.x; 129 | }; 130 | 131 | // 132 | // Y coordinate of the node. 133 | // 134 | this.y = function () { 135 | return this.data.y; 136 | }; 137 | 138 | // 139 | // Width of the node. 140 | // 141 | this.width = function () { 142 | return this.data.width; 143 | } 144 | 145 | // 146 | // Height of the node. 147 | // 148 | this.height = function () { 149 | var numConnectors = 150 | Math.max( 151 | this.inputConnectors.length, 152 | this.outputConnectors.length); 153 | return flowchart.computeConnectorY(numConnectors); 154 | } 155 | 156 | // 157 | // Select the node. 158 | // 159 | this.select = function () { 160 | this._selected = true; 161 | }; 162 | 163 | // 164 | // Deselect the node. 165 | // 166 | this.deselect = function () { 167 | this._selected = false; 168 | }; 169 | 170 | // 171 | // Toggle the selection state of the node. 172 | // 173 | this.toggleSelected = function () { 174 | this._selected = !this._selected; 175 | }; 176 | 177 | // 178 | // Returns true if the node is selected. 179 | // 180 | this.selected = function () { 181 | return this._selected; 182 | }; 183 | 184 | // 185 | // Internal function to add a connector. 186 | this._addConnector = function (connectorDataModel, x, connectorsDataModel, connectorsViewModel) { 187 | var connectorViewModel = 188 | new flowchart.ConnectorViewModel(connectorDataModel, x, 189 | flowchart.computeConnectorY(connectorsViewModel.length), this); 190 | 191 | connectorsDataModel.push(connectorDataModel); 192 | 193 | // Add to node's view model. 194 | connectorsViewModel.push(connectorViewModel); 195 | } 196 | 197 | // 198 | // Add an input connector to the node. 199 | // 200 | this.addInputConnector = function (connectorDataModel) { 201 | 202 | if (!this.data.inputConnectors) { 203 | this.data.inputConnectors = []; 204 | } 205 | this._addConnector(connectorDataModel, 0, this.data.inputConnectors, this.inputConnectors); 206 | }; 207 | 208 | // 209 | // Add an ouput connector to the node. 210 | // 211 | this.addOutputConnector = function (connectorDataModel) { 212 | 213 | if (!this.data.outputConnectors) { 214 | this.data.outputConnectors = []; 215 | } 216 | this._addConnector(connectorDataModel, this.data.width, this.data.outputConnectors, this.outputConnectors); 217 | }; 218 | }; 219 | 220 | // 221 | // Wrap the nodes data-model in a view-model. 222 | // 223 | var createNodesViewModel = function (nodesDataModel) { 224 | var nodesViewModel = []; 225 | 226 | if (nodesDataModel) { 227 | for (var i = 0; i < nodesDataModel.length; ++i) { 228 | nodesViewModel.push(new flowchart.NodeViewModel(nodesDataModel[i])); 229 | } 230 | } 231 | 232 | return nodesViewModel; 233 | }; 234 | 235 | // 236 | // View model for a connection. 237 | // 238 | flowchart.ConnectionViewModel = function (connectionDataModel, sourceConnector, destConnector) { 239 | 240 | this.data = connectionDataModel; 241 | this.source = sourceConnector; 242 | this.dest = destConnector; 243 | 244 | // Set to true when the connection is selected. 245 | this._selected = false; 246 | 247 | this.name = function() { 248 | return this.data.name || ""; 249 | } 250 | 251 | this.sourceCoordX = function () { 252 | return this.source.parentNode().x() + this.source.x(); 253 | }; 254 | 255 | this.sourceCoordY = function () { 256 | return this.source.parentNode().y() + this.source.y(); 257 | }; 258 | 259 | this.sourceCoord = function () { 260 | return { 261 | x: this.sourceCoordX(), 262 | y: this.sourceCoordY() 263 | }; 264 | } 265 | 266 | this.sourceTangentX = function () { 267 | return flowchart.computeConnectionSourceTangentX(this.sourceCoord(), this.destCoord()); 268 | }; 269 | 270 | this.sourceTangentY = function () { 271 | return flowchart.computeConnectionSourceTangentY(this.sourceCoord(), this.destCoord()); 272 | }; 273 | 274 | this.destCoordX = function () { 275 | return this.dest.parentNode().x() + this.dest.x(); 276 | }; 277 | 278 | this.destCoordY = function () { 279 | return this.dest.parentNode().y() + this.dest.y(); 280 | }; 281 | 282 | this.destCoord = function () { 283 | return { 284 | x: this.destCoordX(), 285 | y: this.destCoordY() 286 | }; 287 | } 288 | 289 | this.destTangentX = function () { 290 | return flowchart.computeConnectionDestTangentX(this.sourceCoord(), this.destCoord()); 291 | }; 292 | 293 | this.destTangentY = function () { 294 | return flowchart.computeConnectionDestTangentY(this.sourceCoord(), this.destCoord()); 295 | }; 296 | 297 | this.middleX = function(scale) { 298 | if(typeof(scale)=="undefined") 299 | scale = 0.5; 300 | return this.sourceCoordX()*(1-scale)+this.destCoordX()*scale; 301 | }; 302 | 303 | this.middleY = function(scale) { 304 | if(typeof(scale)=="undefined") 305 | scale = 0.5; 306 | return this.sourceCoordY()*(1-scale)+this.destCoordY()*scale; 307 | }; 308 | 309 | 310 | // 311 | // Select the connection. 312 | // 313 | this.select = function () { 314 | this._selected = true; 315 | }; 316 | 317 | // 318 | // Deselect the connection. 319 | // 320 | this.deselect = function () { 321 | this._selected = false; 322 | }; 323 | 324 | // 325 | // Toggle the selection state of the connection. 326 | // 327 | this.toggleSelected = function () { 328 | this._selected = !this._selected; 329 | }; 330 | 331 | // 332 | // Returns true if the connection is selected. 333 | // 334 | this.selected = function () { 335 | return this._selected; 336 | }; 337 | }; 338 | 339 | // 340 | // Helper function. 341 | // 342 | var computeConnectionTangentOffset = function (pt1, pt2) { 343 | 344 | return (pt2.x - pt1.x) / 2; 345 | } 346 | 347 | // 348 | // Compute the tangent for the bezier curve. 349 | // 350 | flowchart.computeConnectionSourceTangentX = function (pt1, pt2) { 351 | 352 | return pt1.x + computeConnectionTangentOffset(pt1, pt2); 353 | }; 354 | 355 | // 356 | // Compute the tangent for the bezier curve. 357 | // 358 | flowchart.computeConnectionSourceTangentY = function (pt1, pt2) { 359 | 360 | return pt1.y; 361 | }; 362 | 363 | // 364 | // Compute the tangent for the bezier curve. 365 | // 366 | flowchart.computeConnectionSourceTangent = function(pt1, pt2) { 367 | return { 368 | x: flowchart.computeConnectionSourceTangentX(pt1, pt2), 369 | y: flowchart.computeConnectionSourceTangentY(pt1, pt2), 370 | }; 371 | }; 372 | 373 | // 374 | // Compute the tangent for the bezier curve. 375 | // 376 | flowchart.computeConnectionDestTangentX = function (pt1, pt2) { 377 | 378 | return pt2.x - computeConnectionTangentOffset(pt1, pt2); 379 | }; 380 | 381 | // 382 | // Compute the tangent for the bezier curve. 383 | // 384 | flowchart.computeConnectionDestTangentY = function (pt1, pt2) { 385 | 386 | return pt2.y; 387 | }; 388 | 389 | // 390 | // Compute the tangent for the bezier curve. 391 | // 392 | flowchart.computeConnectionDestTangent = function(pt1, pt2) { 393 | return { 394 | x: flowchart.computeConnectionDestTangentX(pt1, pt2), 395 | y: flowchart.computeConnectionDestTangentY(pt1, pt2), 396 | }; 397 | }; 398 | 399 | // 400 | // View model for the chart. 401 | // 402 | flowchart.ChartViewModel = function (chartDataModel) { 403 | 404 | // 405 | // Find a specific node within the chart. 406 | // 407 | this.findNode = function (nodeID) { 408 | 409 | for (var i = 0; i < this.nodes.length; ++i) { 410 | var node = this.nodes[i]; 411 | if (node.data.id == nodeID) { 412 | return node; 413 | } 414 | } 415 | 416 | throw new Error("Failed to find node " + nodeID); 417 | }; 418 | 419 | // 420 | // Find a specific input connector within the chart. 421 | // 422 | this.findInputConnector = function (nodeID, connectorIndex) { 423 | 424 | var node = this.findNode(nodeID); 425 | 426 | if (!node.inputConnectors || node.inputConnectors.length <= connectorIndex) { 427 | throw new Error("Node " + nodeID + " has invalid input connectors."); 428 | } 429 | 430 | return node.inputConnectors[connectorIndex]; 431 | }; 432 | 433 | // 434 | // Find a specific output connector within the chart. 435 | // 436 | this.findOutputConnector = function (nodeID, connectorIndex) { 437 | 438 | var node = this.findNode(nodeID); 439 | 440 | if (!node.outputConnectors || node.outputConnectors.length <= connectorIndex) { 441 | throw new Error("Node " + nodeID + " has invalid output connectors."); 442 | } 443 | 444 | return node.outputConnectors[connectorIndex]; 445 | }; 446 | 447 | // 448 | // Create a view model for connection from the data model. 449 | // 450 | this._createConnectionViewModel = function(connectionDataModel) { 451 | 452 | var sourceConnector = this.findOutputConnector(connectionDataModel.source.nodeID, connectionDataModel.source.connectorIndex); 453 | var destConnector = this.findInputConnector(connectionDataModel.dest.nodeID, connectionDataModel.dest.connectorIndex); 454 | return new flowchart.ConnectionViewModel(connectionDataModel, sourceConnector, destConnector); 455 | } 456 | 457 | // 458 | // Wrap the connections data-model in a view-model. 459 | // 460 | this._createConnectionsViewModel = function (connectionsDataModel) { 461 | 462 | var connectionsViewModel = []; 463 | 464 | if (connectionsDataModel) { 465 | for (var i = 0; i < connectionsDataModel.length; ++i) { 466 | connectionsViewModel.push(this._createConnectionViewModel(connectionsDataModel[i])); 467 | } 468 | } 469 | 470 | return connectionsViewModel; 471 | }; 472 | 473 | // Reference to the underlying data. 474 | this.data = chartDataModel; 475 | 476 | // Create a view-model for nodes. 477 | this.nodes = createNodesViewModel(this.data.nodes); 478 | 479 | // Create a view-model for connections. 480 | this.connections = this._createConnectionsViewModel(this.data.connections); 481 | 482 | // 483 | // Create a view model for a new connection. 484 | // 485 | this.createNewConnection = function (startConnector, endConnector) { 486 | 487 | var connectionsDataModel = this.data.connections; 488 | if (!connectionsDataModel) { 489 | connectionsDataModel = this.data.connections = []; 490 | } 491 | 492 | var connectionsViewModel = this.connections; 493 | if (!connectionsViewModel) { 494 | connectionsViewModel = this.connections = []; 495 | } 496 | 497 | var startNode = startConnector.parentNode(); 498 | var startConnectorIndex = startNode.outputConnectors.indexOf(startConnector); 499 | var startConnectorType = 'output'; 500 | if (startConnectorIndex == -1) { 501 | startConnectorIndex = startNode.inputConnectors.indexOf(startConnector); 502 | startConnectorType = 'input'; 503 | if (startConnectorIndex == -1) { 504 | throw new Error("Failed to find source connector within either inputConnectors or outputConnectors of source node."); 505 | } 506 | } 507 | 508 | var endNode = endConnector.parentNode(); 509 | var endConnectorIndex = endNode.inputConnectors.indexOf(endConnector); 510 | var endConnectorType = 'input'; 511 | if (endConnectorIndex == -1) { 512 | endConnectorIndex = endNode.outputConnectors.indexOf(endConnector); 513 | endConnectorType = 'output'; 514 | if (endConnectorIndex == -1) { 515 | throw new Error("Failed to find dest connector within inputConnectors or outputConnectors of dest node."); 516 | } 517 | } 518 | 519 | if (startConnectorType == endConnectorType) { 520 | throw new Error("Failed to create connection. Only output to input connections are allowed.") 521 | } 522 | 523 | if (startNode == endNode) { 524 | throw new Error("Failed to create connection. Cannot link a node with itself.") 525 | } 526 | 527 | var startNode = { 528 | nodeID: startNode.data.id, 529 | connectorIndex: startConnectorIndex, 530 | } 531 | 532 | var endNode = { 533 | nodeID: endNode.data.id, 534 | connectorIndex: endConnectorIndex, 535 | } 536 | 537 | var connectionDataModel = { 538 | source: startConnectorType == 'output' ? startNode : endNode, 539 | dest: startConnectorType == 'output' ? endNode : startNode, 540 | }; 541 | connectionsDataModel.push(connectionDataModel); 542 | 543 | var outputConnector = startConnectorType == 'output' ? startConnector : endConnector; 544 | var inputConnector = startConnectorType == 'output' ? endConnector : startConnector; 545 | 546 | var connectionViewModel = new flowchart.ConnectionViewModel(connectionDataModel, outputConnector, inputConnector); 547 | connectionsViewModel.push(connectionViewModel); 548 | }; 549 | 550 | // 551 | // Add a node to the view model. 552 | // 553 | this.addNode = function (nodeDataModel) { 554 | if (!this.data.nodes) { 555 | this.data.nodes = []; 556 | } 557 | 558 | // 559 | // Update the data model. 560 | // 561 | this.data.nodes.push(nodeDataModel); 562 | 563 | // 564 | // Update the view model. 565 | // 566 | this.nodes.push(new flowchart.NodeViewModel(nodeDataModel)); 567 | } 568 | 569 | // 570 | // Select all nodes and connections in the chart. 571 | // 572 | this.selectAll = function () { 573 | 574 | var nodes = this.nodes; 575 | for (var i = 0; i < nodes.length; ++i) { 576 | var node = nodes[i]; 577 | node.select(); 578 | } 579 | 580 | var connections = this.connections; 581 | for (var i = 0; i < connections.length; ++i) { 582 | var connection = connections[i]; 583 | connection.select(); 584 | } 585 | } 586 | 587 | // 588 | // Deselect all nodes and connections in the chart. 589 | // 590 | this.deselectAll = function () { 591 | 592 | var nodes = this.nodes; 593 | for (var i = 0; i < nodes.length; ++i) { 594 | var node = nodes[i]; 595 | node.deselect(); 596 | } 597 | 598 | var connections = this.connections; 599 | for (var i = 0; i < connections.length; ++i) { 600 | var connection = connections[i]; 601 | connection.deselect(); 602 | } 603 | }; 604 | 605 | // 606 | // Update the location of the node and its connectors. 607 | // 608 | this.updateSelectedNodesLocation = function (deltaX, deltaY) { 609 | 610 | var selectedNodes = this.getSelectedNodes(); 611 | 612 | for (var i = 0; i < selectedNodes.length; ++i) { 613 | var node = selectedNodes[i]; 614 | node.data.x += deltaX; 615 | node.data.y += deltaY; 616 | } 617 | }; 618 | 619 | // 620 | // Handle mouse click on a particular node. 621 | // 622 | this.handleNodeClicked = function (node, ctrlKey) { 623 | 624 | if (ctrlKey) { 625 | node.toggleSelected(); 626 | } 627 | else { 628 | this.deselectAll(); 629 | node.select(); 630 | } 631 | 632 | // Move node to the end of the list so it is rendered after all the other. 633 | // This is the way Z-order is done in SVG. 634 | 635 | var nodeIndex = this.nodes.indexOf(node); 636 | if (nodeIndex == -1) { 637 | throw new Error("Failed to find node in view model!"); 638 | } 639 | this.nodes.splice(nodeIndex, 1); 640 | this.nodes.push(node); 641 | }; 642 | 643 | // 644 | // Handle mouse down on a connection. 645 | // 646 | this.handleConnectionMouseDown = function (connection, ctrlKey) { 647 | 648 | if (ctrlKey) { 649 | connection.toggleSelected(); 650 | } 651 | else { 652 | this.deselectAll(); 653 | connection.select(); 654 | } 655 | }; 656 | 657 | // 658 | // Delete all nodes and connections that are selected. 659 | // 660 | this.deleteSelected = function () { 661 | 662 | var newNodeViewModels = []; 663 | var newNodeDataModels = []; 664 | 665 | var deletedNodeIds = []; 666 | 667 | // 668 | // Sort nodes into: 669 | // nodes to keep and 670 | // nodes to delete. 671 | // 672 | 673 | for (var nodeIndex = 0; nodeIndex < this.nodes.length; ++nodeIndex) { 674 | 675 | var node = this.nodes[nodeIndex]; 676 | if (!node.selected()) { 677 | // Only retain non-selected nodes. 678 | newNodeViewModels.push(node); 679 | newNodeDataModels.push(node.data); 680 | } 681 | else { 682 | // Keep track of nodes that were deleted, so their connections can also 683 | // be deleted. 684 | deletedNodeIds.push(node.data.id); 685 | } 686 | } 687 | 688 | var newConnectionViewModels = []; 689 | var newConnectionDataModels = []; 690 | 691 | // 692 | // Remove connections that are selected. 693 | // Also remove connections for nodes that have been deleted. 694 | // 695 | for (var connectionIndex = 0; connectionIndex < this.connections.length; ++connectionIndex) { 696 | 697 | var connection = this.connections[connectionIndex]; 698 | if (!connection.selected() && 699 | deletedNodeIds.indexOf(connection.data.source.nodeID) === -1 && 700 | deletedNodeIds.indexOf(connection.data.dest.nodeID) === -1) 701 | { 702 | // 703 | // The nodes this connection is attached to, where not deleted, 704 | // so keep the connection. 705 | // 706 | newConnectionViewModels.push(connection); 707 | newConnectionDataModels.push(connection.data); 708 | } 709 | } 710 | 711 | // 712 | // Update nodes and connections. 713 | // 714 | this.nodes = newNodeViewModels; 715 | this.data.nodes = newNodeDataModels; 716 | this.connections = newConnectionViewModels; 717 | this.data.connections = newConnectionDataModels; 718 | }; 719 | 720 | // 721 | // Select nodes and connections that fall within the selection rect. 722 | // 723 | this.applySelectionRect = function (selectionRect) { 724 | 725 | this.deselectAll(); 726 | 727 | for (var i = 0; i < this.nodes.length; ++i) { 728 | var node = this.nodes[i]; 729 | if (node.x() >= selectionRect.x && 730 | node.y() >= selectionRect.y && 731 | node.x() + node.width() <= selectionRect.x + selectionRect.width && 732 | node.y() + node.height() <= selectionRect.y + selectionRect.height) 733 | { 734 | // Select nodes that are within the selection rect. 735 | node.select(); 736 | } 737 | } 738 | 739 | for (var i = 0; i < this.connections.length; ++i) { 740 | var connection = this.connections[i]; 741 | if (connection.source.parentNode().selected() && 742 | connection.dest.parentNode().selected()) 743 | { 744 | // Select the connection if both its parent nodes are selected. 745 | connection.select(); 746 | } 747 | } 748 | 749 | }; 750 | 751 | // 752 | // Get the array of nodes that are currently selected. 753 | // 754 | this.getSelectedNodes = function () { 755 | var selectedNodes = []; 756 | 757 | for (var i = 0; i < this.nodes.length; ++i) { 758 | var node = this.nodes[i]; 759 | if (node.selected()) { 760 | selectedNodes.push(node); 761 | } 762 | } 763 | 764 | return selectedNodes; 765 | }; 766 | 767 | // 768 | // Get the array of connections that are currently selected. 769 | // 770 | this.getSelectedConnections = function () { 771 | var selectedConnections = []; 772 | 773 | for (var i = 0; i < this.connections.length; ++i) { 774 | var connection = this.connections[i]; 775 | if (connection.selected()) { 776 | selectedConnections.push(connection); 777 | } 778 | } 779 | 780 | return selectedConnections; 781 | }; 782 | 783 | 784 | }; 785 | 786 | })(); 787 | -------------------------------------------------------------------------------- /jasmine/lib/jasmine-1.3.1/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.HtmlReporterHelpers = {}; 2 | 3 | jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { 4 | var el = document.createElement(type); 5 | 6 | for (var i = 2; i < arguments.length; i++) { 7 | var child = arguments[i]; 8 | 9 | if (typeof child === 'string') { 10 | el.appendChild(document.createTextNode(child)); 11 | } else { 12 | if (child) { 13 | el.appendChild(child); 14 | } 15 | } 16 | } 17 | 18 | for (var attr in attrs) { 19 | if (attr == "className") { 20 | el[attr] = attrs[attr]; 21 | } else { 22 | el.setAttribute(attr, attrs[attr]); 23 | } 24 | } 25 | 26 | return el; 27 | }; 28 | 29 | jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { 30 | var results = child.results(); 31 | var status = results.passed() ? 'passed' : 'failed'; 32 | if (results.skipped) { 33 | status = 'skipped'; 34 | } 35 | 36 | return status; 37 | }; 38 | 39 | jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { 40 | var parentDiv = this.dom.summary; 41 | var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; 42 | var parent = child[parentSuite]; 43 | 44 | if (parent) { 45 | if (typeof this.views.suites[parent.id] == 'undefined') { 46 | this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); 47 | } 48 | parentDiv = this.views.suites[parent.id].element; 49 | } 50 | 51 | parentDiv.appendChild(childElement); 52 | }; 53 | 54 | 55 | jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { 56 | for(var fn in jasmine.HtmlReporterHelpers) { 57 | ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; 58 | } 59 | }; 60 | 61 | jasmine.HtmlReporter = function(_doc) { 62 | var self = this; 63 | var doc = _doc || window.document; 64 | 65 | var reporterView; 66 | 67 | var dom = {}; 68 | 69 | // Jasmine Reporter Public Interface 70 | self.logRunningSpecs = false; 71 | 72 | self.reportRunnerStarting = function(runner) { 73 | var specs = runner.specs() || []; 74 | 75 | if (specs.length == 0) { 76 | return; 77 | } 78 | 79 | createReporterDom(runner.env.versionString()); 80 | doc.body.appendChild(dom.reporter); 81 | setExceptionHandling(); 82 | 83 | reporterView = new jasmine.HtmlReporter.ReporterView(dom); 84 | reporterView.addSpecs(specs, self.specFilter); 85 | }; 86 | 87 | self.reportRunnerResults = function(runner) { 88 | reporterView && reporterView.complete(); 89 | }; 90 | 91 | self.reportSuiteResults = function(suite) { 92 | reporterView.suiteComplete(suite); 93 | }; 94 | 95 | self.reportSpecStarting = function(spec) { 96 | if (self.logRunningSpecs) { 97 | self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 98 | } 99 | }; 100 | 101 | self.reportSpecResults = function(spec) { 102 | reporterView.specComplete(spec); 103 | }; 104 | 105 | self.log = function() { 106 | var console = jasmine.getGlobal().console; 107 | if (console && console.log) { 108 | if (console.log.apply) { 109 | console.log.apply(console, arguments); 110 | } else { 111 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 112 | } 113 | } 114 | }; 115 | 116 | self.specFilter = function(spec) { 117 | if (!focusedSpecName()) { 118 | return true; 119 | } 120 | 121 | return spec.getFullName().indexOf(focusedSpecName()) === 0; 122 | }; 123 | 124 | return self; 125 | 126 | function focusedSpecName() { 127 | var specName; 128 | 129 | (function memoizeFocusedSpec() { 130 | if (specName) { 131 | return; 132 | } 133 | 134 | var paramMap = []; 135 | var params = jasmine.HtmlReporter.parameters(doc); 136 | 137 | for (var i = 0; i < params.length; i++) { 138 | var p = params[i].split('='); 139 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 140 | } 141 | 142 | specName = paramMap.spec; 143 | })(); 144 | 145 | return specName; 146 | } 147 | 148 | function createReporterDom(version) { 149 | dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, 150 | dom.banner = self.createDom('div', { className: 'banner' }, 151 | self.createDom('span', { className: 'title' }, "Jasmine "), 152 | self.createDom('span', { className: 'version' }, version)), 153 | 154 | dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), 155 | dom.alert = self.createDom('div', {className: 'alert'}, 156 | self.createDom('span', { className: 'exceptions' }, 157 | self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), 158 | self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), 159 | dom.results = self.createDom('div', {className: 'results'}, 160 | dom.summary = self.createDom('div', { className: 'summary' }), 161 | dom.details = self.createDom('div', { id: 'details' })) 162 | ); 163 | } 164 | 165 | function noTryCatch() { 166 | return window.location.search.match(/catch=false/); 167 | } 168 | 169 | function searchWithCatch() { 170 | var params = jasmine.HtmlReporter.parameters(window.document); 171 | var removed = false; 172 | var i = 0; 173 | 174 | while (!removed && i < params.length) { 175 | if (params[i].match(/catch=/)) { 176 | params.splice(i, 1); 177 | removed = true; 178 | } 179 | i++; 180 | } 181 | if (jasmine.CATCH_EXCEPTIONS) { 182 | params.push("catch=false"); 183 | } 184 | 185 | return params.join("&"); 186 | } 187 | 188 | function setExceptionHandling() { 189 | var chxCatch = document.getElementById('no_try_catch'); 190 | 191 | if (noTryCatch()) { 192 | chxCatch.setAttribute('checked', true); 193 | jasmine.CATCH_EXCEPTIONS = false; 194 | } 195 | chxCatch.onclick = function() { 196 | window.location.search = searchWithCatch(); 197 | }; 198 | } 199 | }; 200 | jasmine.HtmlReporter.parameters = function(doc) { 201 | var paramStr = doc.location.search.substring(1); 202 | var params = []; 203 | 204 | if (paramStr.length > 0) { 205 | params = paramStr.split('&'); 206 | } 207 | return params; 208 | } 209 | jasmine.HtmlReporter.sectionLink = function(sectionName) { 210 | var link = '?'; 211 | var params = []; 212 | 213 | if (sectionName) { 214 | params.push('spec=' + encodeURIComponent(sectionName)); 215 | } 216 | if (!jasmine.CATCH_EXCEPTIONS) { 217 | params.push("catch=false"); 218 | } 219 | if (params.length > 0) { 220 | link += params.join("&"); 221 | } 222 | 223 | return link; 224 | }; 225 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); 226 | jasmine.HtmlReporter.ReporterView = function(dom) { 227 | this.startedAt = new Date(); 228 | this.runningSpecCount = 0; 229 | this.completeSpecCount = 0; 230 | this.passedCount = 0; 231 | this.failedCount = 0; 232 | this.skippedCount = 0; 233 | 234 | this.createResultsMenu = function() { 235 | this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, 236 | this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), 237 | ' | ', 238 | this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); 239 | 240 | this.summaryMenuItem.onclick = function() { 241 | dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); 242 | }; 243 | 244 | this.detailsMenuItem.onclick = function() { 245 | showDetails(); 246 | }; 247 | }; 248 | 249 | this.addSpecs = function(specs, specFilter) { 250 | this.totalSpecCount = specs.length; 251 | 252 | this.views = { 253 | specs: {}, 254 | suites: {} 255 | }; 256 | 257 | for (var i = 0; i < specs.length; i++) { 258 | var spec = specs[i]; 259 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); 260 | if (specFilter(spec)) { 261 | this.runningSpecCount++; 262 | } 263 | } 264 | }; 265 | 266 | this.specComplete = function(spec) { 267 | this.completeSpecCount++; 268 | 269 | if (isUndefined(this.views.specs[spec.id])) { 270 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); 271 | } 272 | 273 | var specView = this.views.specs[spec.id]; 274 | 275 | switch (specView.status()) { 276 | case 'passed': 277 | this.passedCount++; 278 | break; 279 | 280 | case 'failed': 281 | this.failedCount++; 282 | break; 283 | 284 | case 'skipped': 285 | this.skippedCount++; 286 | break; 287 | } 288 | 289 | specView.refresh(); 290 | this.refresh(); 291 | }; 292 | 293 | this.suiteComplete = function(suite) { 294 | var suiteView = this.views.suites[suite.id]; 295 | if (isUndefined(suiteView)) { 296 | return; 297 | } 298 | suiteView.refresh(); 299 | }; 300 | 301 | this.refresh = function() { 302 | 303 | if (isUndefined(this.resultsMenu)) { 304 | this.createResultsMenu(); 305 | } 306 | 307 | // currently running UI 308 | if (isUndefined(this.runningAlert)) { 309 | this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); 310 | dom.alert.appendChild(this.runningAlert); 311 | } 312 | this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); 313 | 314 | // skipped specs UI 315 | if (isUndefined(this.skippedAlert)) { 316 | this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); 317 | } 318 | 319 | this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 320 | 321 | if (this.skippedCount === 1 && isDefined(dom.alert)) { 322 | dom.alert.appendChild(this.skippedAlert); 323 | } 324 | 325 | // passing specs UI 326 | if (isUndefined(this.passedAlert)) { 327 | this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); 328 | } 329 | this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); 330 | 331 | // failing specs UI 332 | if (isUndefined(this.failedAlert)) { 333 | this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); 334 | } 335 | this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); 336 | 337 | if (this.failedCount === 1 && isDefined(dom.alert)) { 338 | dom.alert.appendChild(this.failedAlert); 339 | dom.alert.appendChild(this.resultsMenu); 340 | } 341 | 342 | // summary info 343 | this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); 344 | this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; 345 | }; 346 | 347 | this.complete = function() { 348 | dom.alert.removeChild(this.runningAlert); 349 | 350 | this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 351 | 352 | if (this.failedCount === 0) { 353 | dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); 354 | } else { 355 | showDetails(); 356 | } 357 | 358 | dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); 359 | }; 360 | 361 | return this; 362 | 363 | function showDetails() { 364 | if (dom.reporter.className.search(/showDetails/) === -1) { 365 | dom.reporter.className += " showDetails"; 366 | } 367 | } 368 | 369 | function isUndefined(obj) { 370 | return typeof obj === 'undefined'; 371 | } 372 | 373 | function isDefined(obj) { 374 | return !isUndefined(obj); 375 | } 376 | 377 | function specPluralizedFor(count) { 378 | var str = count + " spec"; 379 | if (count > 1) { 380 | str += "s" 381 | } 382 | return str; 383 | } 384 | 385 | }; 386 | 387 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); 388 | 389 | 390 | jasmine.HtmlReporter.SpecView = function(spec, dom, views) { 391 | this.spec = spec; 392 | this.dom = dom; 393 | this.views = views; 394 | 395 | this.symbol = this.createDom('li', { className: 'pending' }); 396 | this.dom.symbolSummary.appendChild(this.symbol); 397 | 398 | this.summary = this.createDom('div', { className: 'specSummary' }, 399 | this.createDom('a', { 400 | className: 'description', 401 | href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), 402 | title: this.spec.getFullName() 403 | }, this.spec.description) 404 | ); 405 | 406 | this.detail = this.createDom('div', { className: 'specDetail' }, 407 | this.createDom('a', { 408 | className: 'description', 409 | href: '?spec=' + encodeURIComponent(this.spec.getFullName()), 410 | title: this.spec.getFullName() 411 | }, this.spec.getFullName()) 412 | ); 413 | }; 414 | 415 | jasmine.HtmlReporter.SpecView.prototype.status = function() { 416 | return this.getSpecStatus(this.spec); 417 | }; 418 | 419 | jasmine.HtmlReporter.SpecView.prototype.refresh = function() { 420 | this.symbol.className = this.status(); 421 | 422 | switch (this.status()) { 423 | case 'skipped': 424 | break; 425 | 426 | case 'passed': 427 | this.appendSummaryToSuiteDiv(); 428 | break; 429 | 430 | case 'failed': 431 | this.appendSummaryToSuiteDiv(); 432 | this.appendFailureDetail(); 433 | break; 434 | } 435 | }; 436 | 437 | jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { 438 | this.summary.className += ' ' + this.status(); 439 | this.appendToSummary(this.spec, this.summary); 440 | }; 441 | 442 | jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { 443 | this.detail.className += ' ' + this.status(); 444 | 445 | var resultItems = this.spec.results().getItems(); 446 | var messagesDiv = this.createDom('div', { className: 'messages' }); 447 | 448 | for (var i = 0; i < resultItems.length; i++) { 449 | var result = resultItems[i]; 450 | 451 | if (result.type == 'log') { 452 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 453 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 454 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 455 | 456 | if (result.trace.stack) { 457 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 458 | } 459 | } 460 | } 461 | 462 | if (messagesDiv.childNodes.length > 0) { 463 | this.detail.appendChild(messagesDiv); 464 | this.dom.details.appendChild(this.detail); 465 | } 466 | }; 467 | 468 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { 469 | this.suite = suite; 470 | this.dom = dom; 471 | this.views = views; 472 | 473 | this.element = this.createDom('div', { className: 'suite' }, 474 | this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) 475 | ); 476 | 477 | this.appendToSummary(this.suite, this.element); 478 | }; 479 | 480 | jasmine.HtmlReporter.SuiteView.prototype.status = function() { 481 | return this.getSpecStatus(this.suite); 482 | }; 483 | 484 | jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { 485 | this.element.className += " " + this.status(); 486 | }; 487 | 488 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); 489 | 490 | /* @deprecated Use jasmine.HtmlReporter instead 491 | */ 492 | jasmine.TrivialReporter = function(doc) { 493 | this.document = doc || document; 494 | this.suiteDivs = {}; 495 | this.logRunningSpecs = false; 496 | }; 497 | 498 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 499 | var el = document.createElement(type); 500 | 501 | for (var i = 2; i < arguments.length; i++) { 502 | var child = arguments[i]; 503 | 504 | if (typeof child === 'string') { 505 | el.appendChild(document.createTextNode(child)); 506 | } else { 507 | if (child) { el.appendChild(child); } 508 | } 509 | } 510 | 511 | for (var attr in attrs) { 512 | if (attr == "className") { 513 | el[attr] = attrs[attr]; 514 | } else { 515 | el.setAttribute(attr, attrs[attr]); 516 | } 517 | } 518 | 519 | return el; 520 | }; 521 | 522 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 523 | var showPassed, showSkipped; 524 | 525 | this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, 526 | this.createDom('div', { className: 'banner' }, 527 | this.createDom('div', { className: 'logo' }, 528 | this.createDom('span', { className: 'title' }, "Jasmine"), 529 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 530 | this.createDom('div', { className: 'options' }, 531 | "Show ", 532 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 533 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 534 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 535 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 536 | ) 537 | ), 538 | 539 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 540 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 541 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 542 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 543 | ); 544 | 545 | this.document.body.appendChild(this.outerDiv); 546 | 547 | var suites = runner.suites(); 548 | for (var i = 0; i < suites.length; i++) { 549 | var suite = suites[i]; 550 | var suiteDiv = this.createDom('div', { className: 'suite' }, 551 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 552 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 553 | this.suiteDivs[suite.id] = suiteDiv; 554 | var parentDiv = this.outerDiv; 555 | if (suite.parentSuite) { 556 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 557 | } 558 | parentDiv.appendChild(suiteDiv); 559 | } 560 | 561 | this.startedAt = new Date(); 562 | 563 | var self = this; 564 | showPassed.onclick = function(evt) { 565 | if (showPassed.checked) { 566 | self.outerDiv.className += ' show-passed'; 567 | } else { 568 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 569 | } 570 | }; 571 | 572 | showSkipped.onclick = function(evt) { 573 | if (showSkipped.checked) { 574 | self.outerDiv.className += ' show-skipped'; 575 | } else { 576 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 577 | } 578 | }; 579 | }; 580 | 581 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 582 | var results = runner.results(); 583 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 584 | this.runnerDiv.setAttribute("class", className); 585 | //do it twice for IE 586 | this.runnerDiv.setAttribute("className", className); 587 | var specs = runner.specs(); 588 | var specCount = 0; 589 | for (var i = 0; i < specs.length; i++) { 590 | if (this.specFilter(specs[i])) { 591 | specCount++; 592 | } 593 | } 594 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 595 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 596 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 597 | 598 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 599 | }; 600 | 601 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 602 | var results = suite.results(); 603 | var status = results.passed() ? 'passed' : 'failed'; 604 | if (results.totalCount === 0) { // todo: change this to check results.skipped 605 | status = 'skipped'; 606 | } 607 | this.suiteDivs[suite.id].className += " " + status; 608 | }; 609 | 610 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 611 | if (this.logRunningSpecs) { 612 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 613 | } 614 | }; 615 | 616 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 617 | var results = spec.results(); 618 | var status = results.passed() ? 'passed' : 'failed'; 619 | if (results.skipped) { 620 | status = 'skipped'; 621 | } 622 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 623 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 624 | this.createDom('a', { 625 | className: 'description', 626 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 627 | title: spec.getFullName() 628 | }, spec.description)); 629 | 630 | 631 | var resultItems = results.getItems(); 632 | var messagesDiv = this.createDom('div', { className: 'messages' }); 633 | for (var i = 0; i < resultItems.length; i++) { 634 | var result = resultItems[i]; 635 | 636 | if (result.type == 'log') { 637 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 638 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 639 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 640 | 641 | if (result.trace.stack) { 642 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 643 | } 644 | } 645 | } 646 | 647 | if (messagesDiv.childNodes.length > 0) { 648 | specDiv.appendChild(messagesDiv); 649 | } 650 | 651 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 652 | }; 653 | 654 | jasmine.TrivialReporter.prototype.log = function() { 655 | var console = jasmine.getGlobal().console; 656 | if (console && console.log) { 657 | if (console.log.apply) { 658 | console.log.apply(console, arguments); 659 | } else { 660 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 661 | } 662 | } 663 | }; 664 | 665 | jasmine.TrivialReporter.prototype.getLocation = function() { 666 | return this.document.location; 667 | }; 668 | 669 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 670 | var paramMap = {}; 671 | var params = this.getLocation().search.substring(1).split('&'); 672 | for (var i = 0; i < params.length; i++) { 673 | var p = params[i].split('='); 674 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 675 | } 676 | 677 | if (!paramMap.spec) { 678 | return true; 679 | } 680 | return spec.getFullName().indexOf(paramMap.spec) === 0; 681 | }; 682 | -------------------------------------------------------------------------------- /flowchart/flowchart_viewmodel.spec.js: -------------------------------------------------------------------------------- 1 | describe('flowchart-viewmodel', function () { 2 | 3 | // 4 | // Create a mock data model from a simple definition. 5 | // 6 | var createMockDataModel = function (nodeIds, connections) { 7 | 8 | var nodeDataModels = null; 9 | 10 | if (nodeIds) { 11 | nodeDataModels = []; 12 | 13 | for (var i = 0; i < nodeIds.length; ++i) { 14 | nodeDataModels.push({ 15 | id: nodeIds[i], 16 | x: 0, 17 | y: 0, 18 | inputConnectors: [ {}, {}, {} ], 19 | outputConnectors: [ {}, {}, {} ], 20 | }); 21 | } 22 | } 23 | 24 | var connectionDataModels = null; 25 | 26 | if (connections) { 27 | connectionDataModels = []; 28 | 29 | for (var i = 0; i < connections.length; ++i) { 30 | connectionDataModels.push({ 31 | source: { 32 | nodeID: connections[i][0][0], 33 | connectorIndex: connections[i][0][1], 34 | }, 35 | dest: { 36 | nodeID: connections[i][1][0], 37 | connectorIndex: connections[i][1][1], 38 | }, 39 | }); 40 | } 41 | } 42 | 43 | var dataModel = {}; 44 | 45 | if (nodeDataModels) { 46 | dataModel.nodes = nodeDataModels; 47 | } 48 | 49 | if (connectionDataModels) { 50 | dataModel.connections = connectionDataModels; 51 | } 52 | 53 | return dataModel; 54 | }; 55 | 56 | it('compute input connector pos', function () { 57 | 58 | var mockNode = { 59 | x: function () { return 10 }, 60 | y: function () { return 15 }, 61 | }; 62 | 63 | flowchart.computeConnectorPos(mockNode, 0, true); 64 | flowchart.computeConnectorPos(mockNode, 1, true); 65 | flowchart.computeConnectorPos(mockNode, 2, true); 66 | }); 67 | 68 | it('compute output connector pos', function () { 69 | 70 | var mockNode = { 71 | x: function () { return 10 }, 72 | y: function () { return 15 }, 73 | }; 74 | 75 | flowchart.computeConnectorPos(mockNode, 0, false); 76 | flowchart.computeConnectorPos(mockNode, 1, false); 77 | flowchart.computeConnectorPos(mockNode, 2, false); 78 | }); 79 | 80 | it('construct ConnectorViewModel', function () { 81 | 82 | var mockDataModel = { 83 | name: "Fooey", 84 | }; 85 | 86 | new flowchart.ConnectorViewModel(mockDataModel, 0, 10, 0); 87 | new flowchart.ConnectorViewModel(mockDataModel, 0, 10, 1); 88 | new flowchart.ConnectorViewModel(mockDataModel, 0, 10, 2); 89 | 90 | }); 91 | 92 | it('ConnectorViewModel has reference to parent node', function () { 93 | 94 | var mockDataModel = { 95 | name: "Fooey", 96 | }; 97 | var mockParentNodeViewModel = {}; 98 | 99 | var testObject = new flowchart.ConnectorViewModel(mockDataModel, 0, 10, mockParentNodeViewModel); 100 | 101 | expect(testObject.parentNode()).toBe(mockParentNodeViewModel); 102 | }); 103 | 104 | it('construct NodeViewModel with no connectors', function () { 105 | 106 | var mockDataModel = { 107 | x: 10, 108 | y: 12, 109 | name: "Woot", 110 | }; 111 | 112 | new flowchart.NodeViewModel(mockDataModel); 113 | }); 114 | 115 | it('construct NodeViewModel with empty connectors', function () { 116 | 117 | var mockDataModel = { 118 | x: 10, 119 | y: 12, 120 | name: "Woot", 121 | inputConnectors: [], 122 | outputConnectors: [], 123 | }; 124 | 125 | new flowchart.NodeViewModel(mockDataModel); 126 | }); 127 | 128 | it('construct NodeViewModel with connectors', function () { 129 | 130 | var mockInputConnector = { 131 | name: "Input", 132 | }; 133 | 134 | var mockOutputConnector = { 135 | name: "Output", 136 | }; 137 | 138 | var mockDataModel = { 139 | x: 10, 140 | y: 12, 141 | name: "Woot", 142 | inputConnectors: [ 143 | mockInputConnector 144 | ], 145 | outputConnectors: [ 146 | mockOutputConnector 147 | ], 148 | }; 149 | 150 | new flowchart.NodeViewModel(mockDataModel); 151 | }); 152 | 153 | it('test name of NodeViewModel', function () { 154 | 155 | var mockDataModel = { 156 | name: "Woot", 157 | }; 158 | 159 | var testObject = new flowchart.NodeViewModel(mockDataModel); 160 | 161 | expect(testObject.name()).toBe(mockDataModel.name); 162 | }); 163 | 164 | it('test name of NodeViewModel defaults to empty string', function () { 165 | 166 | var mockDataModel = {}; 167 | 168 | var testObject = new flowchart.NodeViewModel(mockDataModel); 169 | 170 | expect(testObject.name()).toBe(""); 171 | }); 172 | 173 | it('test node is deselected by default', function () { 174 | 175 | var mockDataModel = {}; 176 | 177 | var testObject = new flowchart.NodeViewModel(mockDataModel); 178 | 179 | expect(testObject.selected()).toBe(false); 180 | }); 181 | 182 | it('test node width is set by default', function () { 183 | 184 | var mockDataModel = {}; 185 | 186 | var testObject = new flowchart.NodeViewModel(mockDataModel); 187 | 188 | expect(testObject.width() === flowchart.defaultNodeWidth).toBe(true); 189 | }); 190 | 191 | it('test node width is used', function () { 192 | 193 | var mockDataModel = {"width": 900 }; 194 | 195 | var testObject = new flowchart.NodeViewModel(mockDataModel); 196 | 197 | expect(testObject.width()).toBe(900); 198 | }); 199 | 200 | it('test computeConnectorPos uses node width', function () { 201 | 202 | var mockDataModel = { 203 | x: function () { 204 | return 10; 205 | }, 206 | y: function () { 207 | return 15; 208 | }, 209 | width: function () { 210 | return 900; 211 | }, 212 | }; 213 | 214 | var testObject = flowchart.computeConnectorPos(mockDataModel, 1, false); 215 | 216 | expect(testObject.x).toBe(910); 217 | }); 218 | 219 | it('test computeConnectorPos uses default node width', function () { 220 | 221 | var mockDataModel = { 222 | x: function () { 223 | return 10 224 | }, 225 | y: function () { 226 | return 15 227 | }, 228 | }; 229 | 230 | var testObject = flowchart.computeConnectorPos(mockDataModel, 1, false); 231 | 232 | expect(testObject.x).toBe(flowchart.defaultNodeWidth + 10); 233 | }); 234 | 235 | it('test node can be selected', function () { 236 | 237 | var mockDataModel = {}; 238 | 239 | var testObject = new flowchart.NodeViewModel(mockDataModel); 240 | 241 | testObject.select(); 242 | 243 | expect(testObject.selected()).toBe(true); 244 | }); 245 | 246 | it('test node can be deselected', function () { 247 | 248 | var mockDataModel = {}; 249 | 250 | var testObject = new flowchart.NodeViewModel(mockDataModel); 251 | 252 | testObject.select(); 253 | 254 | testObject.deselect(); 255 | 256 | expect(testObject.selected()).toBe(false); 257 | }); 258 | 259 | it('test node can be selection can be toggled', function () { 260 | 261 | var mockDataModel = {}; 262 | 263 | var testObject = new flowchart.NodeViewModel(mockDataModel); 264 | 265 | testObject.toggleSelected(); 266 | 267 | expect(testObject.selected()).toBe(true); 268 | 269 | testObject.toggleSelected(); 270 | 271 | expect(testObject.selected()).toBe(false); 272 | }); 273 | 274 | it('test can add input connector to node', function () { 275 | 276 | var mockDataModel = {}; 277 | 278 | var testObject = new flowchart.NodeViewModel(mockDataModel); 279 | 280 | var name1 = "Connector1"; 281 | var name2 = "Connector2"; 282 | var data1 = { 283 | name: name1 284 | }; 285 | var data2 = { 286 | name: name2 287 | } 288 | testObject.addInputConnector(data1); 289 | testObject.addInputConnector(data2); 290 | 291 | expect(testObject.inputConnectors.length).toBe(2); 292 | expect(testObject.inputConnectors[0].data).toBe(data1); 293 | expect(testObject.inputConnectors[1].data).toBe(data2); 294 | 295 | expect(testObject.data.inputConnectors.length).toBe(2); 296 | expect(testObject.data.inputConnectors[0]).toBe(data1); 297 | expect(testObject.data.inputConnectors[1]).toBe(data2); 298 | }); 299 | 300 | it('test can add output connector to node', function () { 301 | 302 | var mockDataModel = {}; 303 | 304 | var testObject = new flowchart.NodeViewModel(mockDataModel); 305 | 306 | var name1 = "Connector1"; 307 | var name2 = "Connector2"; 308 | var data1 = { 309 | name: name1 310 | }; 311 | var data2 = { 312 | name: name2 313 | } 314 | testObject.addOutputConnector(data1); 315 | testObject.addOutputConnector(data2); 316 | 317 | expect(testObject.outputConnectors.length).toBe(2); 318 | expect(testObject.outputConnectors[0].data).toBe(data1); 319 | expect(testObject.outputConnectors[1].data).toBe(data2); 320 | 321 | expect(testObject.data.outputConnectors.length).toBe(2); 322 | expect(testObject.data.outputConnectors[0]).toBe(data1); 323 | expect(testObject.data.outputConnectors[1]).toBe(data2); 324 | }); 325 | 326 | it('construct ChartViewModel with no nodes or connections', function () { 327 | 328 | var mockDataModel = {}; 329 | 330 | new flowchart.ChartViewModel(mockDataModel); 331 | 332 | }); 333 | 334 | it('construct ChartViewModel with empty nodes and connections', function () { 335 | 336 | var mockDataModel = { 337 | nodes: [], 338 | connections: [], 339 | }; 340 | 341 | new flowchart.ChartViewModel(mockDataModel); 342 | 343 | }); 344 | 345 | it('construct ConnectionViewModel', function () { 346 | 347 | var mockDataModel = {}; 348 | var mockSourceConnector = {}; 349 | var mockDestConnector = {}; 350 | 351 | new flowchart.ConnectionViewModel(mockDataModel, mockSourceConnector, mockDestConnector); 352 | }); 353 | 354 | it('retreive source and dest coordinates', function () { 355 | 356 | var mockDataModel = { 357 | }; 358 | 359 | var mockSourceParentNode = { 360 | x: function () { return 5 }, 361 | y: function () { return 10 }, 362 | }; 363 | 364 | var mockSourceConnector = { 365 | parentNode: function () { 366 | return mockSourceParentNode; 367 | }, 368 | 369 | x: function() { 370 | return 5; 371 | }, 372 | 373 | y: function() { 374 | return 15; 375 | }, 376 | }; 377 | 378 | var mockDestParentNode = { 379 | x: function () { return 50 }, 380 | y: function () { return 30 }, 381 | }; 382 | 383 | var mockDestConnector = { 384 | parentNode: function () { 385 | return mockDestParentNode; 386 | }, 387 | 388 | x: function() { 389 | return 25; 390 | }, 391 | 392 | y: function() { 393 | return 35; 394 | }, 395 | }; 396 | 397 | var testObject = new flowchart.ConnectionViewModel(mockDataModel, mockSourceConnector, mockDestConnector); 398 | 399 | testObject.sourceCoord(); 400 | expect(testObject.sourceCoordX()).toBe(10); 401 | expect(testObject.sourceCoordY()).toBe(25); 402 | testObject.sourceTangentX(); 403 | testObject.sourceTangentY(); 404 | 405 | testObject.destCoord(); 406 | expect(testObject.destCoordX()).toBe(75); 407 | expect(testObject.destCoordY()).toBe(65); 408 | testObject.destTangentX(); 409 | testObject.destTangentY(); 410 | }); 411 | 412 | it('test connection is deselected by default', function () { 413 | 414 | var mockDataModel = {}; 415 | 416 | var testObject = new flowchart.ConnectionViewModel(mockDataModel); 417 | 418 | expect(testObject.selected()).toBe(false); 419 | }); 420 | 421 | it('test connection can be selected', function () { 422 | 423 | var mockDataModel = {}; 424 | 425 | var testObject = new flowchart.ConnectionViewModel(mockDataModel); 426 | 427 | testObject.select(); 428 | 429 | expect(testObject.selected()).toBe(true); 430 | }); 431 | 432 | it('test connection can be deselected', function () { 433 | 434 | var mockDataModel = {}; 435 | 436 | var testObject = new flowchart.ConnectionViewModel(mockDataModel); 437 | 438 | testObject.select(); 439 | 440 | testObject.deselect(); 441 | 442 | expect(testObject.selected()).toBe(false); 443 | }); 444 | 445 | it('test connection can be selection can be toggled', function () { 446 | 447 | var mockDataModel = {}; 448 | 449 | var testObject = new flowchart.ConnectionViewModel(mockDataModel); 450 | 451 | testObject.toggleSelected(); 452 | 453 | expect(testObject.selected()).toBe(true); 454 | 455 | testObject.toggleSelected(); 456 | 457 | expect(testObject.selected()).toBe(false); 458 | }); 459 | 460 | it('construct ChartViewModel with a node', function () { 461 | 462 | var mockDataModel = createMockDataModel([1]); 463 | 464 | var testObject = new flowchart.ChartViewModel(mockDataModel); 465 | expect(testObject.nodes.length).toBe(1); 466 | expect(testObject.nodes[0].data).toBe(mockDataModel.nodes[0]); 467 | 468 | }); 469 | 470 | it('data model with existing connection creates a connection view model', function () { 471 | 472 | var mockDataModel = createMockDataModel( 473 | [ 5, 12 ], 474 | [ 475 | [[ 5, 0 ], [ 12, 1 ]], 476 | ] 477 | ); 478 | 479 | var testObject = new flowchart.ChartViewModel(mockDataModel); 480 | 481 | expect(testObject.connections.length).toBe(1); 482 | expect(testObject.connections[0].data).toBe(mockDataModel.connections[0]); 483 | expect(testObject.connections[0].source.data).toBe(mockDataModel.nodes[0].outputConnectors[0]); 484 | expect(testObject.connections[0].dest.data).toBe(mockDataModel.nodes[1].inputConnectors[1]); 485 | }); 486 | 487 | it('test can add new node', function () { 488 | 489 | var mockDataModel = createMockDataModel(); 490 | 491 | var testObject = new flowchart.ChartViewModel(mockDataModel); 492 | 493 | var nodeDataModel = {}; 494 | testObject.addNode(nodeDataModel); 495 | 496 | expect(testObject.nodes.length).toBe(1); 497 | expect(testObject.nodes[0].data).toBe(nodeDataModel); 498 | 499 | expect(testObject.data.nodes.length).toBe(1); 500 | expect(testObject.data.nodes[0]).toBe(nodeDataModel); 501 | }); 502 | 503 | it('test can select all', function () { 504 | 505 | var mockDataModel = createMockDataModel([1, 2], [[[1, 0], [2, 1]]]); 506 | 507 | var testObject = new flowchart.ChartViewModel(mockDataModel); 508 | 509 | var node1 = testObject.nodes[0]; 510 | var node2 = testObject.nodes[1]; 511 | var connection = testObject.connections[0]; 512 | 513 | testObject.selectAll(); 514 | 515 | expect(node1.selected()).toBe(true); 516 | expect(node2.selected()).toBe(true); 517 | expect(connection.selected()).toBe(true); 518 | }); 519 | 520 | it('test can deselect all nodes', function () { 521 | 522 | var mockDataModel = createMockDataModel([1, 2]); 523 | 524 | var testObject = new flowchart.ChartViewModel(mockDataModel); 525 | 526 | var node1 = testObject.nodes[0]; 527 | var node2 = testObject.nodes[1]; 528 | 529 | node1.select(); 530 | node2.select(); 531 | 532 | testObject.deselectAll(); 533 | 534 | expect(node1.selected()).toBe(false); 535 | expect(node2.selected()).toBe(false); 536 | }); 537 | 538 | it('test can deselect all connections', function () { 539 | 540 | var mockDataModel = createMockDataModel( 541 | [ 5, 12 ], 542 | [ 543 | [[ 5, 0 ], [ 12, 1 ]], 544 | [[ 5, 0 ], [ 12, 1 ]], 545 | ] 546 | ); 547 | 548 | var testObject = new flowchart.ChartViewModel(mockDataModel); 549 | 550 | var connection1 = testObject.connections[0]; 551 | var connection2 = testObject.connections[1]; 552 | 553 | connection1.select(); 554 | connection2.select(); 555 | 556 | testObject.deselectAll(); 557 | 558 | expect(connection1.selected()).toBe(false); 559 | expect(connection2.selected()).toBe(false); 560 | }); 561 | 562 | it('test mouse down deselects nodes other than the one clicked', function () { 563 | 564 | var mockDataModel = createMockDataModel([ 1, 2, 3 ]); 565 | 566 | var testObject = new flowchart.ChartViewModel(mockDataModel); 567 | 568 | var node1 = testObject.nodes[0]; 569 | var node2 = testObject.nodes[1]; 570 | var node3 = testObject.nodes[2]; 571 | 572 | // Fake out the nodes as selected. 573 | node1.select(); 574 | node2.select(); 575 | node3.select(); 576 | 577 | testObject.handleNodeClicked(node2); // Doesn't matter which node is actually clicked. 578 | 579 | expect(node1.selected()).toBe(false); 580 | expect(node2.selected()).toBe(true); 581 | expect(node3.selected()).toBe(false); 582 | }); 583 | 584 | it('test mouse down selects the clicked node', function () { 585 | 586 | var mockDataModel = createMockDataModel([ 1, 2, 3 ]); 587 | 588 | var testObject = new flowchart.ChartViewModel(mockDataModel); 589 | 590 | var node1 = testObject.nodes[0]; 591 | var node2 = testObject.nodes[1]; 592 | var node3 = testObject.nodes[2]; 593 | 594 | testObject.handleNodeClicked(node3); // Doesn't matter which node is actually clicked. 595 | 596 | expect(node1.selected()).toBe(false); 597 | expect(node2.selected()).toBe(false); 598 | expect(node3.selected()).toBe(true); 599 | }); 600 | 601 | it('test mouse down brings node to front', function () { 602 | 603 | var mockDataModel = createMockDataModel([ 1, 2 ]); 604 | 605 | var testObject = new flowchart.ChartViewModel(mockDataModel); 606 | 607 | var node1 = testObject.nodes[0]; 608 | var node2 = testObject.nodes[1]; 609 | 610 | testObject.handleNodeClicked(node1); 611 | 612 | expect(testObject.nodes[0]).toBe(node2); // Mock node 2 should be bought to front. 613 | expect(testObject.nodes[1]).toBe(node1); 614 | }); 615 | 616 | it('test control + mouse down toggles node selection', function () { 617 | 618 | var mockDataModel = createMockDataModel([ 1, 2, 3 ]); 619 | 620 | var testObject = new flowchart.ChartViewModel(mockDataModel); 621 | 622 | var node1 = testObject.nodes[0]; 623 | var node2 = testObject.nodes[1]; 624 | var node3 = testObject.nodes[2]; 625 | 626 | node1.select(); // Mark node 1 as already selected. 627 | 628 | testObject.handleNodeClicked(node2, true); 629 | 630 | expect(node1.selected()).toBe(true); // This node remains selected. 631 | expect(node2.selected()).toBe(true); // This node is being toggled. 632 | expect(node3.selected()).toBe(false); // This node remains unselected. 633 | 634 | testObject.handleNodeClicked(node2, true); 635 | 636 | expect(node1.selected()).toBe(true); // This node remains selected. 637 | expect(node2.selected()).toBe(false); // This node is being toggled. 638 | expect(node3.selected()).toBe(false); // This node remains unselected. 639 | 640 | testObject.handleNodeClicked(node2, true); 641 | 642 | expect(node1.selected()).toBe(true); // This node remains selected. 643 | expect(node2.selected()).toBe(true); // This node is being toggled. 644 | expect(node3.selected()).toBe(false); // This node remains unselected. 645 | }); 646 | 647 | it('test mouse down deselects connections other than the one clicked', function () { 648 | 649 | var mockDataModel = createMockDataModel( 650 | [ 1, 2, 3 ], 651 | [ 652 | [[ 1, 0 ], [ 3, 0 ]], 653 | [[ 2, 1 ], [ 3, 2 ]], 654 | [[ 1, 2 ], [ 3, 0 ]] 655 | ] 656 | ); 657 | 658 | var testObject = new flowchart.ChartViewModel(mockDataModel); 659 | 660 | var connection1 = testObject.connections[0]; 661 | var connection2 = testObject.connections[1]; 662 | var connection3 = testObject.connections[2]; 663 | 664 | // Fake out the connections as selected. 665 | connection1.select(); 666 | connection2.select(); 667 | connection3.select(); 668 | 669 | testObject.handleConnectionMouseDown(connection2); 670 | 671 | expect(connection1.selected()).toBe(false); 672 | expect(connection2.selected()).toBe(true); 673 | expect(connection3.selected()).toBe(false); 674 | }); 675 | 676 | it('test node mouse down selects the clicked connection', function () { 677 | 678 | var mockDataModel = createMockDataModel( 679 | [ 1, 2, 3 ], 680 | [ 681 | [[ 1, 0 ], [ 3, 0 ]], 682 | [[ 2, 1 ], [ 3, 2 ]], 683 | [[ 1, 2 ], [ 3, 0 ]] 684 | ] 685 | ); 686 | 687 | var testObject = new flowchart.ChartViewModel(mockDataModel); 688 | 689 | var connection1 = testObject.connections[0]; 690 | var connection2 = testObject.connections[1]; 691 | var connection3 = testObject.connections[2]; 692 | 693 | testObject.handleConnectionMouseDown(connection3); 694 | 695 | expect(connection1.selected()).toBe(false); 696 | expect(connection2.selected()).toBe(false); 697 | expect(connection3.selected()).toBe(true); 698 | }); 699 | 700 | it('test control + mouse down toggles connection selection', function () { 701 | 702 | var mockDataModel = createMockDataModel( 703 | [ 1, 2, 3 ], 704 | [ 705 | [[ 1, 0 ], [ 3, 0 ]], 706 | [[ 2, 1 ], [ 3, 2 ]], 707 | [[ 1, 2 ], [ 3, 0 ]] 708 | ] 709 | ); 710 | 711 | var testObject = new flowchart.ChartViewModel(mockDataModel); 712 | 713 | var connection1 = testObject.connections[0]; 714 | var connection2 = testObject.connections[1]; 715 | var connection3 = testObject.connections[2]; 716 | 717 | connection1.select(); // Mark connection 1 as already selected. 718 | 719 | testObject.handleConnectionMouseDown(connection2, true); 720 | 721 | expect(connection1.selected()).toBe(true); // This connection remains selected. 722 | expect(connection2.selected()).toBe(true); // This connection is being toggle. 723 | expect(connection3.selected()).toBe(false); // This connection remains unselected. 724 | 725 | testObject.handleConnectionMouseDown(connection2, true); 726 | 727 | expect(connection1.selected()).toBe(true); // This connection remains selected. 728 | expect(connection2.selected()).toBe(false); // This connection is being toggle. 729 | expect(connection3.selected()).toBe(false); // This connection remains unselected. 730 | 731 | testObject.handleConnectionMouseDown(connection2, true); 732 | 733 | expect(connection1.selected()).toBe(true); // This connection remains selected. 734 | expect(connection2.selected()).toBe(true); // This connection is being toggle. 735 | expect(connection3.selected()).toBe(false); // This connection remains unselected. 736 | }); 737 | 738 | it('test data-model is wrapped in view-model', function () { 739 | 740 | var mockDataModel = createMockDataModel([ 1, 2 ], [[[1, 0], [2, 0]]]); 741 | var mockNode = mockDataModel.nodes[0]; 742 | var mockInputConnector = mockNode.inputConnectors[0]; 743 | var mockOutputConnector = mockNode.outputConnectors[0]; 744 | 745 | var testObject = new flowchart.ChartViewModel(mockDataModel); 746 | 747 | // Chart 748 | 749 | expect(testObject).toBeDefined(); 750 | expect(testObject).toNotBe(mockDataModel); 751 | expect(testObject.data).toBe(mockDataModel); 752 | expect(testObject.nodes).toBeDefined(); 753 | expect(testObject.nodes.length).toBe(2); 754 | 755 | // Node 756 | 757 | var node = testObject.nodes[0]; 758 | 759 | expect(node).toNotBe(mockNode); 760 | expect(node.data).toBe(mockNode); 761 | 762 | // Connectors 763 | 764 | expect(node.inputConnectors.length).toBe(3); 765 | expect(node.inputConnectors[0].data).toBe(mockInputConnector); 766 | 767 | expect(node.outputConnectors.length).toBe(3); 768 | expect(node.outputConnectors[0].data).toBe(mockOutputConnector); 769 | 770 | // Connection 771 | 772 | expect(testObject.connections.length).toBe(1); 773 | expect(testObject.connections[0].source).toBe(testObject.nodes[0].outputConnectors[0]); 774 | expect(testObject.connections[0].dest).toBe(testObject.nodes[1].inputConnectors[0]); 775 | }); 776 | 777 | it('test can delete 1st selected node', function () { 778 | 779 | var mockDataModel = createMockDataModel([ 1, 2 ]); 780 | 781 | var testObject = new flowchart.ChartViewModel(mockDataModel); 782 | 783 | expect(testObject.nodes.length).toBe(2); 784 | 785 | testObject.nodes[0].select(); 786 | 787 | var mockNode2 = mockDataModel.nodes[1]; 788 | 789 | testObject.deleteSelected(); 790 | 791 | expect(testObject.nodes.length).toBe(1); 792 | expect(mockDataModel.nodes.length).toBe(1); 793 | expect(testObject.nodes[0].data).toBe(mockNode2); 794 | }); 795 | 796 | it('test can delete 2nd selected nodes', function () { 797 | 798 | var mockDataModel = createMockDataModel([ 1, 2 ]); 799 | 800 | var testObject = new flowchart.ChartViewModel(mockDataModel); 801 | 802 | expect(testObject.nodes.length).toBe(2); 803 | 804 | testObject.nodes[1].select(); 805 | 806 | var mockNode1 = mockDataModel.nodes[0]; 807 | 808 | testObject.deleteSelected(); 809 | 810 | expect(testObject.nodes.length).toBe(1); 811 | expect(mockDataModel.nodes.length).toBe(1); 812 | expect(testObject.nodes[0].data).toBe(mockNode1); 813 | }); 814 | 815 | it('test can delete multiple selected nodes', function () { 816 | 817 | var mockDataModel = createMockDataModel([ 1, 2, 3, 4 ]); 818 | 819 | var testObject = new flowchart.ChartViewModel(mockDataModel); 820 | 821 | expect(testObject.nodes.length).toBe(4); 822 | 823 | testObject.nodes[1].select(); 824 | testObject.nodes[2].select(); 825 | 826 | var mockNode1 = mockDataModel.nodes[0]; 827 | var mockNode4 = mockDataModel.nodes[3]; 828 | 829 | testObject.deleteSelected(); 830 | 831 | expect(testObject.nodes.length).toBe(2); 832 | expect(mockDataModel.nodes.length).toBe(2); 833 | expect(testObject.nodes[0].data).toBe(mockNode1); 834 | expect(testObject.nodes[1].data).toBe(mockNode4); 835 | }); 836 | 837 | it('deleting a node also deletes its connections', function () { 838 | 839 | var mockDataModel = createMockDataModel( 840 | [ 1, 2, 3 ], 841 | [ 842 | [[ 1, 0 ], [ 2, 0 ]], 843 | [[ 2, 0 ], [ 3, 0 ]], 844 | ] 845 | ); 846 | 847 | var testObject = new flowchart.ChartViewModel(mockDataModel); 848 | 849 | expect(testObject.connections.length).toBe(2); 850 | 851 | // Select the middle node. 852 | testObject.nodes[1].select(); 853 | 854 | testObject.deleteSelected(); 855 | 856 | expect(testObject.connections.length).toBe(0); 857 | }); 858 | 859 | it('deleting a node doesnt delete other connections', function () { 860 | 861 | var mockDataModel = createMockDataModel( 862 | [ 1, 2, 3 ], 863 | [ 864 | [[ 1, 0 ], [ 3, 0 ],] 865 | ] 866 | ); 867 | 868 | var testObject = new flowchart.ChartViewModel(mockDataModel); 869 | 870 | expect(testObject.connections.length).toBe(1); 871 | 872 | // Select the middle node. 873 | testObject.nodes[1].select(); 874 | 875 | testObject.deleteSelected(); 876 | 877 | expect(testObject.connections.length).toBe(1); 878 | }); 879 | 880 | it('test can delete 1st selected connection', function () { 881 | 882 | var mockDataModel = createMockDataModel( 883 | [ 1, 2 ], 884 | [ 885 | [[ 1, 0 ], [ 2, 0 ]], 886 | [[ 2, 1 ], [ 1, 2 ]] 887 | ] 888 | ); 889 | 890 | var mockRemainingConnectionDataModel = mockDataModel.connections[1]; 891 | 892 | var testObject = new flowchart.ChartViewModel(mockDataModel); 893 | 894 | expect(testObject.connections.length).toBe(2); 895 | 896 | testObject.connections[0].select(); 897 | 898 | testObject.deleteSelected(); 899 | 900 | expect(testObject.connections.length).toBe(1); 901 | expect(mockDataModel.connections.length).toBe(1); 902 | expect(testObject.connections[0].data).toBe(mockRemainingConnectionDataModel); 903 | }); 904 | 905 | it('test can delete 2nd selected connection', function () { 906 | 907 | var mockDataModel = createMockDataModel( 908 | [ 1, 2 ], 909 | [ 910 | [[ 1, 0 ], [ 2, 0 ]], 911 | [[ 2, 1 ], [ 1, 2 ]] 912 | ] 913 | ); 914 | 915 | var mockRemainingConnectionDataModel = mockDataModel.connections[0]; 916 | 917 | var testObject = new flowchart.ChartViewModel(mockDataModel); 918 | 919 | expect(testObject.connections.length).toBe(2); 920 | 921 | testObject.connections[1].select(); 922 | 923 | testObject.deleteSelected(); 924 | 925 | expect(testObject.connections.length).toBe(1); 926 | expect(mockDataModel.connections.length).toBe(1); 927 | expect(testObject.connections[0].data).toBe(mockRemainingConnectionDataModel); 928 | }); 929 | 930 | 931 | it('test can delete multiple selected connections', function () { 932 | 933 | var mockDataModel = createMockDataModel( 934 | [ 1, 2, 3 ], 935 | [ 936 | [[ 1, 0 ], [ 2, 0 ]], 937 | [[ 2, 1 ], [ 1, 2 ]], 938 | [[ 1, 1 ], [ 3, 0 ]], 939 | [[ 3, 2 ], [ 2, 1 ]] 940 | ] 941 | ); 942 | 943 | var mockRemainingConnectionDataModel1 = mockDataModel.connections[0]; 944 | var mockRemainingConnectionDataModel2 = mockDataModel.connections[3]; 945 | 946 | var testObject = new flowchart.ChartViewModel(mockDataModel); 947 | 948 | expect(testObject.connections.length).toBe(4); 949 | 950 | testObject.connections[1].select(); 951 | testObject.connections[2].select(); 952 | 953 | testObject.deleteSelected(); 954 | 955 | expect(testObject.connections.length).toBe(2); 956 | expect(mockDataModel.connections.length).toBe(2); 957 | expect(testObject.connections[0].data).toBe(mockRemainingConnectionDataModel1); 958 | expect(testObject.connections[1].data).toBe(mockRemainingConnectionDataModel2); 959 | }); 960 | 961 | it('can select nodes via selection rect', function () { 962 | 963 | var mockDataModel = createMockDataModel([ 1, 2, 3 ]); 964 | mockDataModel.nodes[0].x = 0; 965 | mockDataModel.nodes[0].y = 0; 966 | mockDataModel.nodes[1].x = 1020; 967 | mockDataModel.nodes[1].y = 1020; 968 | mockDataModel.nodes[2].x = 3000; 969 | mockDataModel.nodes[2].y = 3000; 970 | 971 | var testObject = new flowchart.ChartViewModel(mockDataModel); 972 | 973 | testObject.nodes[0].select(); // Select a nodes, to ensure it is correctly deselected. 974 | 975 | testObject.applySelectionRect({ x: 1000, y: 1000, width: 1000, height: 1000 }); 976 | 977 | expect(testObject.nodes[0].selected()).toBe(false); 978 | expect(testObject.nodes[1].selected()).toBe(true); 979 | expect(testObject.nodes[2].selected()).toBe(false); 980 | }); 981 | 982 | it('can select connections via selection rect', function () { 983 | 984 | var mockDataModel = createMockDataModel( 985 | [ 1, 2, 3, 4 ], 986 | [ 987 | [[ 1, 0 ], [ 2, 0 ]], 988 | [[ 2, 1 ], [ 3, 2 ]], 989 | [[ 3, 2 ], [ 4, 1 ]] 990 | ] 991 | ); 992 | mockDataModel.nodes[0].x = 0; 993 | mockDataModel.nodes[0].y = 0; 994 | mockDataModel.nodes[1].x = 1020; 995 | mockDataModel.nodes[1].y = 1020; 996 | mockDataModel.nodes[2].x = 1500; 997 | mockDataModel.nodes[2].y = 1500; 998 | mockDataModel.nodes[3].x = 3000; 999 | mockDataModel.nodes[3].y = 3000; 1000 | 1001 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1002 | 1003 | testObject.connections[0].select(); // Select a connection, to ensure it is correctly deselected. 1004 | 1005 | testObject.applySelectionRect({ x: 1000, y: 1000, width: 1000, height: 1000 }); 1006 | 1007 | expect(testObject.connections[0].selected()).toBe(false); 1008 | expect(testObject.connections[1].selected()).toBe(true); 1009 | expect(testObject.connections[2].selected()).toBe(false); 1010 | }); 1011 | 1012 | it('test update selected nodes location', function () { 1013 | var mockDataModel = createMockDataModel([1]); 1014 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1015 | 1016 | var node = testObject.nodes[0]; 1017 | node.select(); 1018 | 1019 | var xInc = 5; 1020 | var yInc = 15; 1021 | 1022 | testObject.updateSelectedNodesLocation(xInc, yInc); 1023 | 1024 | expect(node.x()).toBe(xInc); 1025 | expect(node.y()).toBe(yInc); 1026 | }); 1027 | 1028 | it('test update selected nodes location, ignores unselected nodes', function () { 1029 | var mockDataModel = createMockDataModel([1]); 1030 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1031 | 1032 | var node = testObject.nodes[0]; 1033 | 1034 | var xInc = 5; 1035 | var yInc = 15; 1036 | 1037 | testObject.updateSelectedNodesLocation(xInc, yInc); 1038 | 1039 | expect(node.x()).toBe(0); 1040 | expect(node.y()).toBe(0); 1041 | }); 1042 | 1043 | it('test find node throws when there are no nodes', function () { 1044 | var mockDataModel = createMockDataModel(); 1045 | 1046 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1047 | 1048 | expect(function () { testObject.findNode(150); }).toThrow(); 1049 | }); 1050 | 1051 | it('test find node throws when node is not found', function () { 1052 | var mockDataModel = createMockDataModel([5, 25, 15, 30]); 1053 | 1054 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1055 | 1056 | expect(function () { testObject.findNode(150); }).toThrow(); 1057 | }); 1058 | 1059 | it('test find node retreives correct node', function () { 1060 | var mockDataModel = createMockDataModel([5, 25, 15, 30]); 1061 | 1062 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1063 | 1064 | expect(testObject.findNode(15)).toBe(testObject.nodes[2]); 1065 | }); 1066 | 1067 | it('test find input connector throws when there are no nodes', function () { 1068 | var mockDataModel = createMockDataModel(); 1069 | 1070 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1071 | 1072 | expect(function () { testObject.findInputConnector(150, 1); }).toThrow(); 1073 | }); 1074 | 1075 | it('test find input connector throws when the node is not found', function () { 1076 | var mockDataModel = createMockDataModel([ 1, 2, 3]); 1077 | 1078 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1079 | 1080 | expect(function () { testObject.findInputConnector(150, 1); }).toThrow(); 1081 | }); 1082 | 1083 | it('test find input connector throws when there are no connectors', function () { 1084 | var mockDataModel = createMockDataModel([ 1 ]); 1085 | 1086 | mockDataModel.nodes[0].inputConnectors = []; 1087 | 1088 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1089 | 1090 | expect(function () { testObject.findInputConnector(1, 1); }).toThrow(); 1091 | }); 1092 | 1093 | it('test find input connector throws when connector is not found', function () { 1094 | var mockDataModel = createMockDataModel([5]); 1095 | 1096 | mockDataModel.nodes[0].inputConnectors = [ 1097 | {} // Only 1 input connector. 1098 | ]; 1099 | 1100 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1101 | 1102 | expect(function () { testObject.findInputConnector(5, 1); }).toThrow(); 1103 | }); 1104 | 1105 | it('test find input connector retreives correct connector', function () { 1106 | var mockDataModel = createMockDataModel([5, 25, 15, 30]); 1107 | 1108 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1109 | 1110 | expect(testObject.findInputConnector(15, 1)).toBe(testObject.nodes[2].inputConnectors[1]); 1111 | }); 1112 | 1113 | it('test find output connector throws when there are no nodes', function () { 1114 | var mockDataModel = createMockDataModel(); 1115 | 1116 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1117 | 1118 | expect(function () { testObject.findOutputConnector(150, 1); }).toThrow(); 1119 | }); 1120 | 1121 | it('test find output connector throws when the node is not found', function () { 1122 | var mockDataModel = createMockDataModel([ 1, 2, 3]); 1123 | 1124 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1125 | 1126 | expect(function () { testObject.findOutputConnector(150, 1); }).toThrow(); 1127 | }); 1128 | 1129 | it('test find output connector throws when there are no connectors', function () { 1130 | var mockDataModel = createMockDataModel([ 1 ]); 1131 | 1132 | mockDataModel.nodes[0].outputConnectors = []; 1133 | 1134 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1135 | 1136 | expect(function () { testObject.findOutputConnector(1, 1); }).toThrow(); 1137 | }); 1138 | 1139 | it('test find output connector throws when connector is not found', function () { 1140 | var mockDataModel = createMockDataModel([5]); 1141 | 1142 | mockDataModel.nodes[0].outputConnectors = [ 1143 | {} // Only 1 input connector. 1144 | ]; 1145 | 1146 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1147 | 1148 | expect(function () { testObject.findOutputConnector(5, 1); }).toThrow(); 1149 | }); 1150 | 1151 | it('test find output connector retreives correct connector', function () { 1152 | var mockDataModel = createMockDataModel([5, 25, 15, 30]); 1153 | 1154 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1155 | 1156 | expect(testObject.findOutputConnector(15, 1)).toBe(testObject.nodes[2].outputConnectors[1]); 1157 | }); 1158 | 1159 | 1160 | it('test create new connection', function () { 1161 | 1162 | var mockDataModel = createMockDataModel([5, 25]); 1163 | 1164 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1165 | 1166 | var startConnector = testObject.nodes[0].outputConnectors[0]; 1167 | var endConnector = testObject.nodes[1].inputConnectors[1]; 1168 | 1169 | testObject.createNewConnection(startConnector, endConnector); 1170 | 1171 | expect(testObject.connections.length).toBe(1); 1172 | var connection = testObject.connections[0]; 1173 | expect(connection.source).toBe(startConnector); 1174 | expect(connection.dest).toBe(endConnector); 1175 | 1176 | expect(testObject.data.connections.length).toBe(1); 1177 | var connectionData = testObject.data.connections[0]; 1178 | expect(connection.data).toBe(connectionData); 1179 | 1180 | expect(connectionData.source.nodeID).toBe(5); 1181 | expect(connectionData.source.connectorIndex).toBe(0); 1182 | expect(connectionData.dest.nodeID).toBe(25); 1183 | expect(connectionData.dest.connectorIndex).toBe(1); 1184 | }); 1185 | 1186 | it('test create new connection from input to output', function () { 1187 | 1188 | var mockDataModel = createMockDataModel([5, 25]); 1189 | 1190 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1191 | 1192 | var startConnector = testObject.nodes[1].inputConnectors[1]; 1193 | var endConnector = testObject.nodes[0].outputConnectors[0]; 1194 | 1195 | testObject.createNewConnection(startConnector, endConnector); 1196 | 1197 | expect(testObject.connections.length).toBe(1); 1198 | var connection = testObject.connections[0]; 1199 | expect(connection.source).toBe(endConnector); 1200 | expect(connection.dest).toBe(startConnector); 1201 | 1202 | expect(testObject.data.connections.length).toBe(1); 1203 | var connectionData = testObject.data.connections[0]; 1204 | expect(connection.data).toBe(connectionData); 1205 | 1206 | expect(connectionData.source.nodeID).toBe(5); 1207 | expect(connectionData.source.connectorIndex).toBe(0); 1208 | expect(connectionData.dest.nodeID).toBe(25); 1209 | expect(connectionData.dest.connectorIndex).toBe(1); 1210 | }); 1211 | 1212 | it('test get selected nodes results in empty array when there are no nodes', function () { 1213 | 1214 | var mockDataModel = createMockDataModel(); 1215 | 1216 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1217 | 1218 | var selectedNodes = testObject.getSelectedNodes(); 1219 | 1220 | expect(selectedNodes.length).toBe(0); 1221 | }); 1222 | 1223 | it('test get selected nodes results in empty array when none selected', function () { 1224 | 1225 | var mockDataModel = createMockDataModel([1, 2, 3, 4]); 1226 | 1227 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1228 | 1229 | var selectedNodes = testObject.getSelectedNodes(); 1230 | 1231 | expect(selectedNodes.length).toBe(0); 1232 | }); 1233 | 1234 | it('test can get selected nodes', function () { 1235 | 1236 | var mockDataModel = createMockDataModel([1, 2, 3, 4]); 1237 | 1238 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1239 | 1240 | var node1 = testObject.nodes[0]; 1241 | var node2 = testObject.nodes[1]; 1242 | var node3 = testObject.nodes[2]; 1243 | var node4 = testObject.nodes[3]; 1244 | 1245 | node2.select(); 1246 | node3.select(); 1247 | 1248 | var selectedNodes = testObject.getSelectedNodes(); 1249 | 1250 | expect(selectedNodes.length).toBe(2); 1251 | expect(selectedNodes[0]).toBe(node2); 1252 | expect(selectedNodes[1]).toBe(node3); 1253 | }); 1254 | 1255 | it('test can get selected connections', function () { 1256 | 1257 | var mockDataModel = createMockDataModel( 1258 | [ 1, 2, 3 ], 1259 | [ 1260 | [[ 1, 0 ], [ 2, 0 ]], 1261 | [[ 2, 1 ], [ 1, 2 ]], 1262 | [[ 1, 1 ], [ 3, 0 ]], 1263 | [[ 3, 2 ], [ 2, 1 ]] 1264 | ] 1265 | ); 1266 | var testObject = new flowchart.ChartViewModel(mockDataModel); 1267 | 1268 | var connection1 = testObject.connections[0]; 1269 | var connection2 = testObject.connections[1]; 1270 | var connection3 = testObject.connections[2]; 1271 | var connection4 = testObject.connections[3]; 1272 | 1273 | connection2.select(); 1274 | connection3.select(); 1275 | 1276 | var selectedConnections = testObject.getSelectedConnections(); 1277 | 1278 | expect(selectedConnections.length).toBe(2); 1279 | expect(selectedConnections[0]).toBe(connection2); 1280 | expect(selectedConnections[1]).toBe(connection3); 1281 | }); 1282 | }); 1283 | --------------------------------------------------------------------------------