├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── app └── flowchart │ ├── canvas-controller.js │ ├── canvas-controller_test.js │ ├── canvas-directive.js │ ├── canvas-directive_test.js │ ├── canvas-service.js │ ├── canvas.html │ ├── connector-directive.js │ ├── connector-directive_test.js │ ├── edgedragging-service.js │ ├── edgedragging-service_test.js │ ├── edgedrawing-service.js │ ├── edgedrawing-service_test.js │ ├── flowchart-constant.js │ ├── flowchart.css │ ├── flowchart.js │ ├── magnet-directive.js │ ├── magnet-directive_test.js │ ├── model-service.js │ ├── model-service_test.js │ ├── modelvalidation-service.js │ ├── modelvalidation-service_test.js │ ├── mouseover-service.js │ ├── mouseover-service_test.js │ ├── node-directive.js │ ├── node-directive_test.js │ ├── node.html │ ├── nodeTemplatePath-provider.js │ ├── nodedragging-service.js │ ├── nodedragging-service_test.js │ ├── onedatanode.html │ ├── topsort-service.js │ └── topsort-service_test.js ├── bower.json ├── dist ├── app.js ├── flowchart.css ├── index.html ├── ngFlowchart.js └── onedatastyle.css ├── gulpfile.js ├── jscs.json ├── karma.conf.js ├── liveDemo.gif ├── ngFlowchartDependency.png ├── ngFlowchartDependencyGraph.dia ├── package.json └── wallaby.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/* 2 | !.gitkeep 3 | node_modules/ 4 | app/bower_components/ 5 | tmp 6 | .DS_Store 7 | 8 | # JetBrains IDE project files 9 | .idea 10 | *.iml 11 | 12 | dist/ngFlowchart.js 13 | dist/vendor.js 14 | 15 | dist/flowchart.css 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": true, 3 | "globals": { 4 | "angular": false, 5 | "describe": false, 6 | "it": false, 7 | "expect": false, 8 | "beforeEach": false, 9 | "afterEach": false, 10 | "module": false, 11 | "inject": false 12 | } 13 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | before_script: 8 | - npm install -g bower gulp 9 | - npm install 10 | - bower install 11 | script: 12 | - gulp test --verbose 13 | notifications: 14 | slack: 15 | secure: Uj5D1G4avyyrJaZbbkKF6yOD8IXCQSkoAqu8yPAJvP6rJJ0G3P56Nik6ydAkwtMaiWttTGgxa/t+aTWlKCkD3TkV/4XqZSI8ptz1OchhY86bqrFBiaeNAmxzpbhKuT6UKi2xxGg5h9VN5YBkp30C5C5KldX95Dlrn653UNa2Ho1xwJX8S3qjB22GqD8ZJBd+abMPCSWWCcSAcl6cRnzd9DbTOW9a3Jm1QWgfWZEqEjXX9/E/4PkgRTrtWlKtbutsJ6eE1jtFiK4eNk7xLv/EbmhXAT7NuupqJwKmWfC4vByd8BIRQ9S4Ka0UoJDuHcd6ZM0zCa2LTRgo63MipVuACZWE1Eez46TZfIeTURxkleRNTp2wZvI9WpaSOB7kkNcg8EI/gfgxzHs1z9RdlVtnnuhxt4t1lM60M+q1cH4ILNlzvU7EUS90KbfZtinsT5NB6A00Z4bnRW+3RtqEp8UnN9uMSKuIybUkmIDHLWt5xNNfseKbM1bVhbckLNZTSiIhshG9W+TAd9NUivSt0pzldpQRkRJXtycP+FCoCUyaN2edfsf0qya5xlc6fbsiwnaECe8oieQ3RC+CLW1LY/tH/P8jPqq4VwxMqsq7KbLpG6DxIUFr0fxLhKBrrmho1eQzps02p4QlUHDwo8ce0seScSYjtnk/FYG9k4bFLTRg13w= 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ONE LOGIC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngFlowchart [![Bower version](https://badge.fury.io/bo/ngFlowchart.svg)](https://github.com/ONE-LOGIC/ngFlowchart) [![Build Status](https://travis-ci.org/ONE-LOGIC/ngFlowchart.svg?branch=master)](https://travis-ci.org/ONE-LOGIC/ngFlowchart/) [![Dependency Status](https://gemnasium.com/ONE-LOGIC/ngFlowchart.svg)](https://gemnasium.com/ONE-LOGIC/ngFlowchart) 2 | 3 | ngFlowchart is an easy and customizable way to draw flowchart graphs using AngularJS. Its main features are: 4 | * Native AngularJS support 5 | * An easy way to customize the look of nodes, by writing your own [template](#the-node-template) 6 | * Automatically adjusts size to its graph 7 | 8 | 9 | Live Demo 10 | 11 | Visit the live demo 12 | 13 | ## Getting Started 14 | 15 | Install ngFlowchart via bower with `bower install ngFlowchart` 16 | 17 | Run `gulp` in the ngFlowchart directory to start an interactive demo. 18 | 19 | ## Table of Contents 20 | * [Getting Started](#getting-started) 21 | * [Integration](#integration) 22 | * [API](#api) 23 | * [Model](#the-model) 24 | * [fc-canvas attribute](#fc-canvas-attributes) 25 | * [Setting your own node template](#the-node-template) 26 | * [Browser Support](#browser-support) 27 | 28 | ## Integration 29 | 30 | Add stylesheet: 31 | ```html 32 | 33 | ``` 34 | 35 | Include script: 36 | ```html 37 | 38 | ``` 39 | 40 | Use the `fc-canvas` directive to display the graph: 41 | ```html 42 | 43 | ``` 44 | 45 | Add `model` and `selectedObjects` to your scope: 46 | ```javascript 47 | model = { 48 | nodes: [ 49 | { 50 | id: 1, 51 | x: 10, 52 | y: 10, 53 | name: "My first node", 54 | connectors: [ 55 | { 56 | id: 1, 57 | type: bottomConnector 58 | } 59 | ] 60 | }, 61 | { 62 | id: 2, 63 | x: 50, 64 | y: 50, 65 | name: "My seconde node", 66 | connectors: [ 67 | { 68 | id: 2, 69 | type: topConnector 70 | } 71 | ] 72 | } 73 | ], 74 | edges: [ 75 | { 76 | source: 1, 77 | destination: 2, 78 | active: false 79 | } 80 | ] 81 | }; 82 | 83 | flowchartselected = []; 84 | ``` 85 | 86 | Your site should now show your first flowchart with two connected nodes. 87 | 88 | ## Api 89 | 90 | ### The model 91 | 92 | ```javascript 93 | { 94 | nodes: [Node], 95 | edges: [Edge] 96 | } 97 | ``` 98 | 99 | #### Node 100 | ```javascript 101 | { 102 | id: integer, 103 | name: string, 104 | x: integer, // x-coordinate of the node relative to the canvas. 105 | y: integer, // y-coordinate of the node relative to the canvas. 106 | connectors: [Connector] 107 | } 108 | ``` 109 | 110 | #### Connector 111 | ```javascript 112 | { 113 | id: integer, 114 | type: string 115 | } 116 | ``` 117 | 118 | #### Edge 119 | ```javascript 120 | { 121 | source: Connector.id 122 | destination: Connector.id 123 | active: boolean 124 | } 125 | ``` 126 | 127 | ### fc-canvas attributes 128 | * `model` The model. 129 | * `selected-objects` The selected nodes and edges as objects. Example: `[{id: 1, name: "First node", {...}}, {source: 1, destination: 2}]` 130 | * `edge-style` "line" or "curved". 131 | * `automatic-resize` If `true` the canvas will adjust its size while node dragging and allow "endless" dragging. 132 | * `drag-animation` Either `repaint` (default) or `shadow` where `repaint` repaints the whole flowchart including edges according to new position while `shadow` show the new position only by showing a shadow of the node at the new position and repaints the edges only at the end of dragging. 133 | * `callbacks` Object with callbacks. 134 | * `edgeAdded` will be called if an edge is added by ngFlowchart. 135 | * `edgeDoubleClick(event, edge)` will be called when an edge is doubleclicked. 136 | * `edgeMouseOver(event, edge)` will be called if the mouse hovers an edge. 137 | * `isValidEdge(sourceConnector, destinationConnector)` will be called, when the user tries to connect to edges. Returns `true` if this is an valid edge in your application or `false` otherwise. 138 | * `edgeRemoved(edge)` will be called if an edge has been removed 139 | * `nodeRemoved(node)` will be called if a node has been removed 140 | * `nodeCallbacks` an object which will be available in the scope of the node template. This is usefull, to register a doubleclick handler on a node or similiar things. Every method that is handed into the `nodeCallbacks` will be available in the node template via the `callbacks` attribute. 141 | 142 | ### The Node template 143 | Easily change the look and feel of the graph by writing your own node template. This is a simple AngularJS template registered with our `NodeTemplatePath` provider: 144 | 145 | ```javascript 146 | angular.module('yourApp', ['flowchart']) 147 | .config(function(NodeTemplatePathProvider) { 148 | NodeTemplatePathProvider.setTemplatePath("path/to/your/template/node.html"); 149 | }) 150 | ``` 151 | 152 | The $scope in this template includes following variables: 153 | * `node` The node object from the model. 154 | * `modelservice` The modelservice instance of this canvas. 155 | * `underMouse` `true` when the mouse hovers this node, `false` otherwise. 156 | * `selected` `true` if this node is selected, `false` otherwise. 157 | * `mouseOverConnector` The connector object from the model witch is hovered by the mouse or `null`. 158 | * `draggedNode` The node object from the model witch is dragged. 159 | * `nodeCallbacks` The object you assigned to `nodeCallbacks` on the `callbacks` attribute of `fc-canvas`. 160 | 161 | ### Modelservice 162 | Our `Modelfactory` could contain some interesting functions for you to use. 163 | Instantiate it with `Modelfactory(model, selectedObjects)` with model and selectedObjects as references to the same objects you gave the canvas. 164 | 165 | 166 | ## Browser Support 167 | ngFlowchart supports Chrome, Firefox, Opera and IE10+. Safari is not supported. PRs to expand support are welcome. 168 | 169 | Right now it is only possible to have one canvas per site, this may changes in future. 170 | 171 | ###Sponsors 172 | Thanks to BrowserStack for kindly helping us improve cross browser support. -------------------------------------------------------------------------------- /app/flowchart/canvas-controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function canvasController($scope, Mouseoverfactory, Nodedraggingfactory, Modelfactory, Edgedraggingfactory, Edgedrawingservice, FlowchartCanvasService) { 6 | 7 | $scope.dragAnimation = angular.isDefined($scope.dragAnimation) ? $scope.dragAnimation : 'repaint'; 8 | 9 | $scope.userCallbacks = $scope.userCallbacks || {}; 10 | $scope.automaticResize = $scope.automaticResize || false; 11 | angular.forEach($scope.userCallbacks, function(callback, key) { 12 | if (!angular.isFunction(callback) && key !== 'nodeCallbacks') { 13 | throw new Error('All callbacks should be functions.'); 14 | } 15 | }); 16 | 17 | $scope.modelservice = Modelfactory($scope.model, $scope.selectedObjects, $scope.userCallbacks.edgeAdded || angular.noop, $scope.userCallbacks.nodeRemoved || angular.noop, $scope.userCallbacks.edgeRemoved || angular.noop); 18 | 19 | $scope.nodeDragging = {}; 20 | var nodedraggingservice = Nodedraggingfactory($scope.modelservice, $scope.nodeDragging, $scope.$apply.bind($scope), $scope.automaticResize, $scope.dragAnimation); 21 | 22 | $scope.edgeDragging = {}; 23 | var edgedraggingservice = Edgedraggingfactory($scope.modelservice, $scope.model, $scope.edgeDragging, $scope.userCallbacks.isValidEdge || null, $scope.$apply.bind($scope), $scope.dragAnimation, $scope.edgeStyle); 24 | 25 | $scope.mouseOver = {}; 26 | var mouseoverservice = Mouseoverfactory($scope.mouseOver, $scope.$apply.bind($scope)); 27 | 28 | $scope.edgeMouseEnter = mouseoverservice.edgeMouseEnter; 29 | $scope.edgeMouseLeave = mouseoverservice.edgeMouseLeave; 30 | 31 | $scope.canvasClick = $scope.modelservice.deselectAll; 32 | 33 | $scope.drop = function(event) { 34 | nodedraggingservice.drop(event); 35 | FlowchartCanvasService._notifyDrop(event); 36 | }; 37 | 38 | $scope.dragover = function(event) { 39 | nodedraggingservice.dragover(event); 40 | edgedraggingservice.dragover(event); 41 | FlowchartCanvasService._notifyDragover(event); 42 | }; 43 | 44 | $scope.edgeClick = function(event, edge) { 45 | $scope.modelservice.edges.handleEdgeMouseClick(edge, event.ctrlKey); 46 | // Don't let the chart handle the mouse down. 47 | event.stopPropagation(); 48 | event.preventDefault(); 49 | }; 50 | 51 | $scope.edgeDoubleClick = $scope.userCallbacks.edgeDoubleClick || angular.noop; 52 | $scope.edgeMouseOver = $scope.userCallbacks.edgeMouseOver || angular.noop; 53 | 54 | $scope.userNodeCallbacks = $scope.userCallbacks.nodeCallbacks; 55 | $scope.callbacks = { 56 | nodeDragstart: nodedraggingservice.dragstart, 57 | nodeDragend: nodedraggingservice.dragend, 58 | edgeDragstart: edgedraggingservice.dragstart, 59 | edgeDragend: edgedraggingservice.dragend, 60 | edgeDrop: edgedraggingservice.drop, 61 | edgeDragoverConnector: edgedraggingservice.dragoverConnector, 62 | edgeDragoverMagnet: edgedraggingservice.dragoverMagnet, 63 | edgeDragleaveMagnet: edgedraggingservice.dragleaveMagnet, 64 | nodeMouseOver: mouseoverservice.nodeMouseOver, 65 | nodeMouseOut: mouseoverservice.nodeMouseOut, 66 | connectorMouseEnter: mouseoverservice.connectorMouseEnter, 67 | connectorMouseLeave: mouseoverservice.connectorMouseLeave, 68 | nodeClicked: function(node) { 69 | return function(event) { 70 | $scope.modelservice.nodes.handleClicked(node, event.ctrlKey); 71 | $scope.$apply(); 72 | 73 | // Don't let the chart handle the mouse down. 74 | event.stopPropagation(); 75 | event.preventDefault(); 76 | } 77 | } 78 | }; 79 | 80 | $scope.getEdgeDAttribute = Edgedrawingservice.getEdgeDAttribute; 81 | } 82 | 83 | angular 84 | .module('flowchart') 85 | .controller('canvasController', canvasController); 86 | 87 | }()); 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/flowchart/canvas-controller_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('test canvas-controller', function() { 4 | var $controller; 5 | var $rootScope; 6 | 7 | beforeEach(module('flowchart')); 8 | 9 | beforeEach(inject(function(_$controller_, _$rootScope_) { 10 | $rootScope = _$rootScope_; 11 | this.$scope = $rootScope.$new(); 12 | this.Mouseoverfactory = jasmine.createSpy('mouseoverfactory').and.returnValue(jasmine.createSpyObj('mouseoverservice', ['nodeMouseOver', 'nodeMouseOut', 'connectorMouseEnter', 13 | 'connectorMouseLeave', 'edgeMouseEnter', 'edgeMouseLeave'])); 14 | this.Nodedraggingfactory = jasmine.createSpy('Nodedraggingfactory').and.returnValue(jasmine.createSpyObj('nodedragging', ['drop', 'dragstart', 'dragend', 'dragover'])); 15 | this.Modelfactory = jasmine.createSpy('Modelfactory').and.returnValue(jasmine.createSpyObj('modelservice', ['deselectAll'])); 16 | this.Edgedraggingfactory = jasmine.createSpy('Edgedraggingfactory').and.returnValue(jasmine.createSpyObj('edgeDraggingservice', ['dragstart', 'drop', 'dragover', 'dragoverConnector', 'dragend', 'dragoverMagnet'])); 17 | this.edgeDrawingService = jasmine.createSpy('edgeDrawingService'); 18 | 19 | $controller = _$controller_; 20 | this.controller = $controller('canvasController', { 21 | $scope: this.$scope, 22 | Mouseoverfactory: this.Mouseoverfactory, 23 | Nodedraggingfactory: this.Nodedraggingfactory, 24 | Modelfactory: this.Modelfactory, 25 | edgeDraggingFactory: this.edgeDraggingFactory, 26 | edgeDrawingService: this.edgeDrawingService 27 | }); 28 | })); 29 | 30 | it('should define all the scope variables', function() { 31 | expect(this.$scope.modelservice).toBeDefined(); 32 | expect(this.$scope.nodeDragging).toBeDefined(); 33 | expect(this.$scope.edgeDragging).toBeDefined(); 34 | expect(this.$scope.mouseOver).toBeDefined(); 35 | expect(this.$scope.canvasClick).toEqual(jasmine.any(Function)); 36 | expect(this.$scope.edgeMouseEnter).toEqual(jasmine.any(Function)); 37 | expect(this.$scope.edgeMouseLeave).toEqual(jasmine.any(Function)); 38 | expect(this.$scope.drop).toEqual(jasmine.any(Function)); 39 | expect(this.$scope.dragover).toEqual(jasmine.any(Function)); 40 | expect(this.$scope.edgeClick).toEqual(jasmine.any(Function)); 41 | expect(this.$scope.edgeDoubleClick).toEqual(jasmine.any(Function)); 42 | expect(this.$scope.edgeMouseOver).toEqual(jasmine.any(Function)); 43 | expect(this.$scope.getEdgeDAttribute).toEqual(jasmine.any(Function)); 44 | expect(this.$scope.callbacks.nodeDragstart).toEqual(jasmine.any(Function)); 45 | expect(this.$scope.callbacks.nodeDragend).toEqual(jasmine.any(Function)); 46 | expect(this.$scope.callbacks.edgeDragstart).toEqual(jasmine.any(Function)); 47 | expect(this.$scope.callbacks.edgeDragend).toEqual(jasmine.any(Function)); 48 | expect(this.$scope.callbacks.edgeDrop).toEqual(jasmine.any(Function)); 49 | expect(this.$scope.callbacks.edgeDragoverConnector).toEqual(jasmine.any(Function)); 50 | expect(this.$scope.callbacks.edgeDragoverMagnet).toEqual(jasmine.any(Function)); 51 | expect(this.$scope.callbacks.nodeClicked).toEqual(jasmine.any(Function)); 52 | expect(this.$scope.callbacks.nodeMouseOver).toEqual(jasmine.any(Function)); 53 | expect(this.$scope.callbacks.nodeMouseOut).toEqual(jasmine.any(Function)); 54 | expect(this.$scope.callbacks.connectorMouseEnter).toEqual(jasmine.any(Function)); 55 | expect(this.$scope.callbacks.connectorMouseLeave).toEqual(jasmine.any(Function)); 56 | expect(this.$scope.callbacks.nodeClicked()).toEqual(jasmine.any(Function)); // Should be of type function(node) {return function(event){};} 57 | }); 58 | 59 | it('should set $scope.userCallbacks if not given and control if they are all functionsexcept the nodeCallbacks', function() { 60 | var that = this; 61 | expect(this.$scope.userCallbacks).toBeDefined(); 62 | 63 | var userCallbacks = {edgeDoubleClick: function() {}, nodeCallbacks: {test: 'test'}}; 64 | this.$scope.userCallbacks = angular.copy(userCallbacks); 65 | this.controller = $controller('canvasController', { 66 | $scope: this.$scope, 67 | Mouseoverfactory: this.Mouseoverfactory, 68 | Nodedraggingfactory: this.Nodedraggingfactory, 69 | Modelfactory: this.Modelfactory, 70 | edgeDraggingFactory: this.edgeDraggingFactory, 71 | edgeDrawingService: this.edgeDrawingService 72 | }); 73 | expect(this.$scope.userCallbacks).toEqual(userCallbacks); 74 | expect(this.$scope.userNodeCallbacks).toEqual(userCallbacks.nodeCallbacks); 75 | 76 | this.$scope.userCallbacks.isValidEdge = {}; 77 | expect(function() { $controller('canvasController', { 78 | $scope: that.$scope, 79 | Mouseoverfactory: that.Mouseoverfactory, 80 | Nodedraggingfactory: that.Nodedraggingfactory, 81 | Modelfactory: that.Modelfactory, 82 | edgeDraggingFactory: that.edgeDraggingFactory, 83 | edgeDrawingService: that.edgeDrawingService 84 | });}).toThrowError('All callbacks should be functions.') 85 | 86 | }); 87 | 88 | it('should give the edgeAddedCallback to the modelservice', function() { 89 | expect(this.Modelfactory).toHaveBeenCalledWith(undefined, undefined, angular.noop, angular.noop, angular.noop); 90 | 91 | var edgeAddedCallback = jasmine.createSpy('edgeAddedCallback'); 92 | var edgeRemovedCallback = jasmine.createSpy('edgeRemovedCallback') 93 | var nodeRemovedCallback = jasmine.createSpy('nodeRemovedCallback') 94 | var userCallbacks = {edgeAdded: edgeAddedCallback, edgeRemoved: edgeRemovedCallback, nodeRemoved: nodeRemovedCallback}; 95 | this.$scope.userCallbacks = angular.copy(userCallbacks); 96 | this.controller = $controller('canvasController', { 97 | $scope: this.$scope, 98 | Mouseoverfactory: this.Mouseoverfactory, 99 | Nodedraggingfactory: this.Nodedraggingfactory, 100 | Modelfactory: this.Modelfactory, 101 | edgeDraggingFactory: this.edgeDraggingFactory, 102 | edgeDrawingService: this.edgeDrawingService 103 | }); 104 | expect(this.Modelfactory.calls.argsFor(1)).toEqual([undefined, undefined, edgeAddedCallback, nodeRemovedCallback, edgeRemovedCallback]); 105 | 106 | 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /app/flowchart/canvas-directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function fcCanvas(flowchartConstants, FlowchartCanvasService) { 6 | return { 7 | restrict: 'E', 8 | templateUrl: "flowchart/canvas.html", 9 | replace: true, 10 | scope: { 11 | model: "=", 12 | selectedObjects: "=", 13 | edgeStyle: '@', 14 | userCallbacks: '=?callbacks', 15 | automaticResize: '=?', 16 | dragAnimation: '=?', 17 | nodeWidth: '=?', 18 | nodeHeight: '=?' 19 | }, 20 | controller: 'canvasController', 21 | link: function(scope, element) { 22 | function adjustCanvasSize() { 23 | if (scope.model) { 24 | var maxX = 0; 25 | var maxY = 0; 26 | angular.forEach(scope.model.nodes, function (node, key) { 27 | maxX = Math.max(node.x + scope.nodeWidth, maxX); 28 | maxY = Math.max(node.y + scope.nodeHeight, maxY); 29 | }); 30 | element.css('width', Math.max(maxX, element.prop('offsetWidth')) + 'px'); 31 | element.css('height', Math.max(maxY, element.prop('offsetHeight')) + 'px'); 32 | } 33 | } 34 | if (scope.edgeStyle !== flowchartConstants.curvedStyle && scope.edgeStyle !== flowchartConstants.lineStyle) { 35 | throw new Error('edgeStyle not supported.'); 36 | } 37 | scope.nodeHeight = scope.nodeHeight || 200; 38 | scope.nodeWidth = scope.nodeWidth || 200; 39 | scope.dragAnimation = scope.dragAnimation || 'repaint'; 40 | 41 | scope.flowchartConstants = flowchartConstants; 42 | element.addClass(flowchartConstants.canvasClass); 43 | element.on('dragover', scope.dragover); 44 | element.on('drop', scope.drop); 45 | 46 | scope.$watch('model', adjustCanvasSize); 47 | 48 | FlowchartCanvasService.setCanvasHtmlElement(element[0]); 49 | scope.modelservice.setCanvasHtmlElement(element[0]); 50 | scope.modelservice.setSvgHtmlElement(element[0].querySelector('svg')); 51 | } 52 | }; 53 | } 54 | 55 | angular 56 | .module('flowchart') 57 | .directive('fcCanvas', fcCanvas); 58 | 59 | }()); 60 | 61 | -------------------------------------------------------------------------------- /app/flowchart/canvas-directive_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('the canvas-directive, node drawing is not tested at all to separate from node- and connector-directive', function() { 4 | var EDGE_SOURCE = {x: 0, y: 0}; 5 | var EDGE_DESTINATION = {x: 100, y: 100}; 6 | var EDGE_SOURCE_TANGENT = {x: 10, y: 10}; 7 | var EDGE_DESTINATION_TANGENT = {x: 20, y: 20}; 8 | 9 | var $compile; 10 | var $rootScope; 11 | var controllerScope; 12 | var flowchartConstants; 13 | var modelservice; 14 | 15 | beforeEach(function() { 16 | module('flowchart', function($provide, $controllerProvider) { 17 | $provide.service('modelservice', function() { 18 | this.setCanvasHtmlElement = jasmine.createSpy('modelservice.setCanvasHtmlElement'); 19 | this.setSvgHtmlElement = jasmine.createSpy('modelservice.setSvgHtmlElement'); 20 | 21 | this.edges = jasmine.createSpyObj('modelservice edges', ['sourceCoord', 'destCoord', 'isSelected']); 22 | this.edges.sourceCoord.and.returnValue(EDGE_SOURCE); 23 | this.edges.destCoord.and.returnValue(EDGE_DESTINATION); 24 | this.edges.isSelected.and.returnValue(false); 25 | }); 26 | $controllerProvider.register('canvasController', function($scope, modelservice) { 27 | controllerScope = $scope; 28 | 29 | $scope.modelservice = modelservice; 30 | 31 | $scope.mouseOver = {}; 32 | $scope.mouseOver.edge = null; 33 | 34 | $scope.edgeDragging = {}; 35 | 36 | $scope.edgeClick = jasmine.createSpy('edgeClick listener'); 37 | $scope.edgeMouseEnter = jasmine.createSpy('edgeMouseEnter listener'); 38 | $scope.edgeMouseLeave = jasmine.createSpy('edgeMouseLeave listener'); 39 | $scope.edgeDoubleClick= jasmine.createSpy('edgeDoubleClick listener'); 40 | $scope.edgeMouseOver = jasmine.createSpy('edgeMouseOver listener'); 41 | 42 | $scope.getEdgeDAttribute = jasmine.createSpy('getEdgeDAttribute').and.returnValue('No calculation.'); 43 | $scope.getSourceTangentX = jasmine.createSpy().and.returnValue(EDGE_SOURCE_TANGENT.x); 44 | $scope.getSourceTangentY = jasmine.createSpy().and.returnValue(EDGE_SOURCE_TANGENT.y); 45 | $scope.getDestTangentX = jasmine.createSpy().and.returnValue(EDGE_DESTINATION_TANGENT.x); 46 | $scope.getDestTangentY = jasmine.createSpy().and.returnValue(EDGE_DESTINATION_TANGENT.y); 47 | 48 | $scope.dragover = jasmine.createSpy('dragover listener'); 49 | $scope.drop = jasmine.createSpy('drop listener'); 50 | $scope.canvasMouseMove = jasmine.createSpy('canvasMouseMove listener'); 51 | $scope.canvasClick = jasmine.createSpy('canvasClick listener'); 52 | 53 | }); 54 | }); 55 | 56 | module('flowchart'); 57 | }); 58 | 59 | function compileCanvas(scope, style) { 60 | var canvas = $compile('')(scope); 61 | scope.$digest(); 62 | return canvas; 63 | } 64 | 65 | beforeEach(inject(function(_$compile_, _$rootScope_, _flowchartConstants_, _modelservice_) { 66 | $compile = _$compile_; 67 | $rootScope = _$rootScope_; 68 | flowchartConstants = _flowchartConstants_; 69 | modelservice = _modelservice_; 70 | 71 | this.outerScope = $rootScope.$new(); 72 | this.outerScope.selectedObjects = []; 73 | this.outerScope.model = {nodes: [], edges: []}; 74 | 75 | this.canvas = compileCanvas(this.outerScope, flowchartConstants.lineStyle); 76 | })); 77 | 78 | it('should add the flowchart-canvas class', function() { 79 | expect(this.canvas.hasClass(flowchartConstants.canvasClass)).toBe(true); 80 | }); 81 | 82 | it('should validate the edgestyle', function() { 83 | compileCanvas(this.outerScope, flowchartConstants.lineStyle); 84 | compileCanvas(this.outerScope, flowchartConstants.curvedStyle); 85 | 86 | this.canvas = $compile('')(this.outerScope); 87 | expect(this.outerScope.$digest).toThrow(); 88 | }); 89 | 90 | it('should add a dragoverlistener', function() { 91 | expect(controllerScope.dragover).not.toHaveBeenCalled(); 92 | this.canvas.triggerHandler('dragover'); 93 | expect(controllerScope.dragover).toHaveBeenCalled(); 94 | }); 95 | 96 | it('should add a droplistener', function() { 97 | expect(controllerScope.drop).not.toHaveBeenCalled(); 98 | this.canvas.triggerHandler('drop'); 99 | expect(controllerScope.drop).toHaveBeenCalled(); 100 | }); 101 | 102 | it('should add a mouseclick listener', function() { 103 | expect(controllerScope.canvasClick).not.toHaveBeenCalled(); 104 | this.canvas.triggerHandler('click'); 105 | expect(controllerScope.canvasClick).toHaveBeenCalled(); 106 | }); 107 | 108 | it('should set the html element to the modelservice', function() { 109 | expect(modelservice.setCanvasHtmlElement).toHaveBeenCalledWith(this.canvas[0]); 110 | }); 111 | 112 | describe('test for edgedrawing', function() { 113 | it('should draw edge which are dragged', function() { 114 | controllerScope.edgeDragging.isDragging = true; 115 | controllerScope.edgeDragging.dragPoint1 = {x: 0, y: 0}; 116 | controllerScope.edgeDragging.dragPoint2 = {x: 100, y: 100}; 117 | controllerScope.edgeDragging.dragTangent1 = {x: 10, y: 10}; 118 | controllerScope.edgeDragging.dragTangent2 = {x: 20, y: 20}; 119 | 120 | controllerScope.$apply(); 121 | 122 | var draggedEdge = this.canvas.find('path'); 123 | expect(draggedEdge.length).toEqual(1); 124 | expect(draggedEdge.hasClass(flowchartConstants.edgeClass)).toBe(true); 125 | expect(draggedEdge.hasClass(flowchartConstants.draggingClass)).toBe(true); 126 | 127 | // No dragged edge if no dragging. 128 | controllerScope.edgeDragging.isDragging = false; 129 | controllerScope.$apply(); 130 | expect(this.canvas.find('path').length).toEqual(0); 131 | }); 132 | 133 | it('should draw the edges from the model', function() { 134 | expect(this.canvas.find('path').length).toEqual(0); 135 | 136 | controllerScope.model = {nodes: [], edges: [{'source': 1, 'destination': 2}]}; 137 | controllerScope.$apply(); 138 | var edge = this.canvas.find('path'); 139 | expect(edge.length).toEqual(1); 140 | 141 | 142 | controllerScope.model = {nodes: [], edges: [{'source': 1, 'destination': 2}, {'source': 1, 'destination': 2}]}; 143 | controllerScope.$apply(); 144 | edge = this.canvas.find('path'); 145 | expect(edge.length).toEqual(2); 146 | }); 147 | 148 | it('should set selected classes for the edges', function() { 149 | controllerScope.model = {nodes: [], edges: [{'source': 1, 'destination': 2}]}; 150 | controllerScope.$apply(); 151 | expect(this.canvas.find('path').length).toEqual(1); // Edges were drawn? 152 | expect(this.canvas.find('path').hasClass(flowchartConstants.selectedClass)).toBe(false); 153 | 154 | controllerScope.modelservice.edges.isSelected.and.returnValue(true); 155 | controllerScope.$apply(); 156 | expect(this.canvas.find('path').hasClass(flowchartConstants.selectedClass)).toBe(true); 157 | }); 158 | 159 | it('should set hover classes for the edges', function() { 160 | controllerScope.model = {nodes: [], edges: [{'source': 1, 'destination': 2}, {'source': 1, 'destination': 2}]}; 161 | controllerScope.mouseOver.edge = controllerScope.model.edges[0]; 162 | controllerScope.$apply(); 163 | expect(this.canvas.find('path').length).toEqual(2); // Edges were drawn? 164 | expect(this.canvas.find('path').hasClass(flowchartConstants.hoverClass)).toBe(true); 165 | 166 | controllerScope.mouseOver.edge = controllerScope.model.edges[1]; // .hasClass uses the first found edge. 167 | controllerScope.$apply(); 168 | expect(this.canvas.find('path').hasClass(flowchartConstants.hoverClass)).toBe(false); 169 | }); 170 | 171 | it('should register a click, mouseenter, mouseleave listener for the edges', function() { 172 | controllerScope.model = {nodes: [], edges: [{'source': 1, 'destination': 2}, {'source': 1, 'destination': 2}]}; 173 | controllerScope.$apply(); 174 | 175 | expect(controllerScope.edgeClick).not.toHaveBeenCalled(); 176 | this.canvas.find('path').triggerHandler('click'); 177 | expect(controllerScope.edgeClick).toHaveBeenCalled(); 178 | 179 | expect(controllerScope.edgeMouseEnter).not.toHaveBeenCalled(); 180 | this.canvas.find('path').triggerHandler('mouseenter'); 181 | expect(controllerScope.edgeMouseEnter).toHaveBeenCalled(); 182 | 183 | expect(controllerScope.edgeMouseLeave).not.toHaveBeenCalled(); 184 | this.canvas.find('path').triggerHandler('mouseleave'); 185 | expect(controllerScope.edgeMouseLeave).toHaveBeenCalled(); 186 | }); 187 | 188 | it('should add a doubleclick and a hoverlistener', function() { 189 | controllerScope.model = {nodes: [], edges: [{'source': 1, 'destination': 2}, {'source': 1, 'destination': 2}]}; 190 | controllerScope.$apply(); 191 | 192 | expect(controllerScope.edgeDoubleClick).not.toHaveBeenCalled(); 193 | this.canvas.find('path').triggerHandler('dblclick'); 194 | expect(controllerScope.edgeDoubleClick).toHaveBeenCalled(); 195 | 196 | expect(controllerScope.edgeMouseOver).not.toHaveBeenCalled(); 197 | this.canvas.find('path').triggerHandler('mouseover'); 198 | expect(controllerScope.edgeMouseOver).toHaveBeenCalled(); 199 | }); 200 | }); 201 | 202 | }); 203 | -------------------------------------------------------------------------------- /app/flowchart/canvas-service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function CanvasService($rootScope) { 6 | 7 | var canvasHtmlElement; 8 | 9 | this.setCanvasHtmlElement = function(element) { 10 | canvasHtmlElement = element; 11 | }; 12 | 13 | this.getCanvasHtmlElement = function() { 14 | return canvasHtmlElement; 15 | }; 16 | 17 | this.dragover = function(scope, callback) { 18 | var handler = $rootScope.$on('notifying-dragover-event', callback); 19 | scope.$on('$destroy', handler); 20 | }; 21 | 22 | this._notifyDragover = function(event) { 23 | $rootScope.$emit('notifying-dragover-event', event); 24 | }; 25 | 26 | this.drop = function(scope, callback) { 27 | var handler = $rootScope.$on('notifying-drop-event', callback); 28 | scope.$on('$destroy', handler); 29 | }; 30 | 31 | this._notifyDrop = function(event) { 32 | $rootScope.$emit('notifying-drop-event', event); 33 | }; 34 | } 35 | 36 | angular.module('flowchart') 37 | .service('FlowchartCanvasService', CanvasService); 38 | 39 | }()); 40 | -------------------------------------------------------------------------------- /app/flowchart/canvas.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 12 | 13 | 14 | 15 | 17 | 19 | 20 | 21 | 25 | 26 | 33 |
34 | -------------------------------------------------------------------------------- /app/flowchart/connector-directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function fcConnector(flowchartConstants) { 6 | return { 7 | restrict: 'A', 8 | link: function(scope, element) { 9 | element.attr('draggable', 'true'); 10 | 11 | element.on('dragover', scope.fcCallbacks.edgeDragoverConnector); 12 | element.on('drop', scope.fcCallbacks.edgeDrop(scope.connector)); 13 | element.on('dragend', scope.fcCallbacks.edgeDragend); 14 | element.on('dragstart', scope.fcCallbacks.edgeDragstart(scope.connector)); 15 | element.on('mouseenter', scope.fcCallbacks.connectorMouseEnter(scope.connector)); 16 | element.on('mouseleave', scope.fcCallbacks.connectorMouseLeave(scope.connector)); 17 | 18 | element.addClass(flowchartConstants.connectorClass); 19 | scope.$watch('mouseOverConnector', function(value) { 20 | if (value === scope.connector) { 21 | element.addClass(flowchartConstants.hoverClass); 22 | } else { 23 | element.removeClass(flowchartConstants.hoverClass); 24 | } 25 | }); 26 | 27 | scope.modelservice.connectors.setHtmlElement(scope.connector.id, element[0]); 28 | } 29 | }; 30 | } 31 | 32 | angular 33 | .module('flowchart') 34 | .directive('fcConnector', fcConnector); 35 | 36 | }()); 37 | -------------------------------------------------------------------------------- /app/flowchart/connector-directive_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('test for connector directive', function() { 4 | var $compile; 5 | var $rootScope; 6 | var modelService; 7 | var flowchartConstants; 8 | 9 | var connector = { 10 | type: 'topConnector', 11 | id: 1 12 | }; 13 | 14 | 15 | beforeEach(function() { 16 | module(function($provide) { 17 | $provide.service('modelService', function() { 18 | this.connectors = {}; 19 | this.connectors.setHtmlElement = jasmine.createSpy('setHtmlElement'); 20 | }) 21 | }); 22 | module('flowchart'); 23 | }); 24 | 25 | beforeEach(inject(function(_$compile_, _$rootScope_, _modelService_, _flowchartConstants_) { 26 | $compile = _$compile_; 27 | $rootScope = _$rootScope_; 28 | modelService = _modelService_; 29 | flowchartConstants = _flowchartConstants_; 30 | 31 | $rootScope.connector = connector; 32 | $rootScope.mouseOverConnector = null; 33 | $rootScope.fcCallbacks = jasmine.createSpyObj('callbacks', ['edgeDragend', 'edgeDragoverConnector', 'connectorMouseEnter', 'connectorMouseLeave']); 34 | $rootScope.fcCallbacks.connectorMouseEnter.and.returnValue(function(event) { 35 | }); 36 | $rootScope.fcCallbacks.connectorMouseLeave.and.returnValue(function(event) { 37 | }); 38 | 39 | this.innerDragStart = jasmine.createSpy('innerDragStart'); 40 | $rootScope.fcCallbacks.edgeDragstart = jasmine.createSpy('edgeDragstart').and.returnValue(this.innerDragStart); 41 | 42 | this.innerDrop = jasmine.createSpy('innerDrop'); 43 | $rootScope.fcCallbacks.edgeDrop = jasmine.createSpy('edgeDrop').and.returnValue(this.innerDrop); 44 | 45 | this.innerMouseEnter = jasmine.createSpy('innerMouseEnter'); 46 | $rootScope.fcCallbacks.connectorMouseEnter.and.returnValue(this.innerMouseEnter); 47 | 48 | this.innerMouseLeave = jasmine.createSpy('innerMouseLeave'); 49 | $rootScope.fcCallbacks.connectorMouseLeave.and.returnValue(this.innerMouseLeave); 50 | 51 | $rootScope.modelservice = modelService; 52 | })); 53 | 54 | function getCompiledConnector() { 55 | var connector = $compile('
')($rootScope); 56 | $rootScope.$digest(); 57 | return connector; 58 | } 59 | 60 | it('should be draggable', function() { 61 | var connector = getCompiledConnector(); 62 | expect(connector.attr('draggable')).toBe('true'); 63 | }); 64 | 65 | it('should have a hovered class if hovered', function() { 66 | $rootScope.mouseOverConnector = connector; 67 | var htmlConnector = getCompiledConnector(); 68 | expect(htmlConnector.hasClass(flowchartConstants.hoverClass)).toBe(true); 69 | 70 | $rootScope.mouseOverConnector = null; 71 | $rootScope.$apply(); 72 | expect(htmlConnector.hasClass(flowchartConstants.hoverClass)).toBe(false); 73 | }); 74 | 75 | it('should store the connector html elements', function() { 76 | getCompiledConnector(); 77 | expect(modelService.connectors.setHtmlElement.calls.count()).toBe(1); 78 | expect(modelService.connectors.setHtmlElement).toHaveBeenCalledWith(connector.id, jasmine.any(Object)); 79 | }); 80 | 81 | it('should register the dragstart, dragend, drop, mouseenter, mouseleave and dragover event', function() { 82 | var htmlConnector = getCompiledConnector(); 83 | 84 | expect(this.innerDragStart).not.toHaveBeenCalled(); 85 | htmlConnector.triggerHandler('dragstart'); 86 | expect(this.innerDragStart).toHaveBeenCalled(); 87 | 88 | expect(this.innerDrop).not.toHaveBeenCalled(); 89 | htmlConnector.triggerHandler('drop'); 90 | expect(this.innerDrop).toHaveBeenCalled(); 91 | 92 | expect($rootScope.fcCallbacks.edgeDragend).not.toHaveBeenCalled(); 93 | htmlConnector.triggerHandler('dragend'); 94 | expect($rootScope.fcCallbacks.edgeDragend).toHaveBeenCalled(); 95 | 96 | expect($rootScope.fcCallbacks.edgeDragoverConnector).not.toHaveBeenCalled(); 97 | htmlConnector.triggerHandler('dragover'); 98 | expect($rootScope.fcCallbacks.edgeDragoverConnector).toHaveBeenCalled(); 99 | 100 | expect(this.innerMouseEnter).not.toHaveBeenCalled(); 101 | htmlConnector.triggerHandler('mouseenter'); 102 | expect(this.innerMouseEnter).toHaveBeenCalled(); 103 | 104 | expect(this.innerMouseLeave).not.toHaveBeenCalled(); 105 | htmlConnector.triggerHandler('mouseleave'); 106 | expect(this.innerMouseLeave).toHaveBeenCalled(); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /app/flowchart/edgedragging-service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function Edgedraggingfactory(Modelvalidation, flowchartConstants, Edgedrawingservice) { 6 | function factory(modelservice, model, edgeDragging, isValidEdgeCallback, applyFunction, dragAnimation, edgeStyle) { 7 | if (isValidEdgeCallback === null) { 8 | isValidEdgeCallback = function() { 9 | return true; 10 | }; 11 | } 12 | 13 | var edgedraggingService = {}; 14 | 15 | var draggedEdgeSource = null; 16 | var dragOffset = {}; 17 | 18 | edgeDragging.isDragging = false; 19 | edgeDragging.dragPoint1 = null; 20 | edgeDragging.dragPoint2 = null; 21 | edgeDragging.shadowDragStarted = false; 22 | 23 | var destinationHtmlElement = null; 24 | var oldDisplayStyle = ""; 25 | 26 | edgedraggingService.dragstart = function(connector) { 27 | return function(event) { 28 | 29 | if (connector.type == flowchartConstants.topConnectorType) { 30 | for (var i = 0; i < model.edges.length; i++) { 31 | if (model.edges[i].destination == connector.id) { 32 | var swapConnector = modelservice.connectors.getConnector(model.edges[i].source); 33 | applyFunction(function() { 34 | modelservice.edges.delete(model.edges[i]); 35 | }); 36 | break; 37 | } 38 | } 39 | } 40 | 41 | edgeDragging.isDragging = true; 42 | 43 | if (swapConnector != undefined) { 44 | draggedEdgeSource = swapConnector; 45 | edgeDragging.dragPoint1 = modelservice.connectors.getCenteredCoord(swapConnector.id); 46 | } else { 47 | draggedEdgeSource = connector; 48 | edgeDragging.dragPoint1 = modelservice.connectors.getCenteredCoord(connector.id); 49 | } 50 | 51 | var canvas = modelservice.getCanvasHtmlElement(); 52 | if (!canvas) { 53 | throw new Error('No canvas while edgedraggingService found.'); 54 | } 55 | dragOffset.x = -canvas.getBoundingClientRect().left; 56 | dragOffset.y = -canvas.getBoundingClientRect().top; 57 | 58 | edgeDragging.dragPoint2 = { 59 | x: event.clientX + dragOffset.x, 60 | y: event.clientY + dragOffset.y 61 | }; 62 | 63 | event.dataTransfer.setData('Text', 'Just to support firefox'); 64 | if (event.dataTransfer.setDragImage) { 65 | var invisibleDiv = angular.element('
')[0]; // This divs stays invisible, because it is not in the dom. 66 | event.dataTransfer.setDragImage(invisibleDiv, 0, 0); 67 | } else { 68 | destinationHtmlElement = event.target; 69 | oldDisplayStyle = destinationHtmlElement.style.display; 70 | event.target.style.display = 'none'; // Internetexplorer does not support setDragImage, but it takes an screenshot, from the draggedelement and uses it as dragimage. 71 | // Since angular redraws the element in the next dragover call, display: none never gets visible to the user. 72 | 73 | if (dragAnimation == flowchartConstants.dragAnimationShadow) { 74 | // IE Drag Fix 75 | edgeDragging.shadowDragStarted = true; 76 | } 77 | } 78 | 79 | if (dragAnimation == flowchartConstants.dragAnimationShadow) { 80 | if (edgeDragging.gElement == undefined) { 81 | //set shadow elements once 82 | // IE Support 83 | edgeDragging.gElement = angular.element(document.querySelectorAll('.shadow-svg-class')); 84 | edgeDragging.pathElement = angular.element(document.querySelectorAll('.shadow-svg-class')).find('path'); 85 | edgeDragging.circleElement = angular.element(document.querySelectorAll('.shadow-svg-class')).find('circle'); 86 | } 87 | 88 | edgeDragging.gElement.css('display', 'block'); 89 | edgeDragging.pathElement.attr('d', Edgedrawingservice.getEdgeDAttribute(edgeDragging.dragPoint1, edgeDragging.dragPoint2, edgeStyle)); 90 | edgeDragging.circleElement.attr('cx', edgeDragging.dragPoint2.x); 91 | edgeDragging.circleElement.attr('cy', edgeDragging.dragPoint2.y); 92 | } 93 | event.stopPropagation(); 94 | }; 95 | }; 96 | 97 | edgedraggingService.dragover = function(event) { 98 | 99 | if (edgeDragging.isDragging) { 100 | if (!edgeDragging.magnetActive && dragAnimation == flowchartConstants.dragAnimationShadow) { 101 | if (destinationHtmlElement !== null) { 102 | destinationHtmlElement.style.display = oldDisplayStyle; 103 | } 104 | 105 | if (edgeDragging.shadowDragStarted) { 106 | applyFunction(function() { 107 | edgeDragging.shadowDragStarted = false; 108 | }); 109 | } 110 | 111 | edgeDragging.dragPoint2 = { 112 | x: event.clientX + dragOffset.x, 113 | y: event.clientY + dragOffset.y 114 | }; 115 | 116 | edgeDragging.pathElement.attr('d', Edgedrawingservice.getEdgeDAttribute(edgeDragging.dragPoint1, edgeDragging.dragPoint2, edgeStyle)); 117 | edgeDragging.circleElement.attr('cx', edgeDragging.dragPoint2.x); 118 | edgeDragging.circleElement.attr('cy', edgeDragging.dragPoint2.y); 119 | 120 | } else if (dragAnimation == flowchartConstants.dragAnimationRepaint) { 121 | return applyFunction(function () { 122 | 123 | if (destinationHtmlElement !== null) { 124 | destinationHtmlElement.style.display = oldDisplayStyle; 125 | } 126 | 127 | edgeDragging.dragPoint2 = { 128 | x: event.clientX + dragOffset.x, 129 | y: event.clientY + dragOffset.y 130 | }; 131 | }); 132 | } 133 | } 134 | }; 135 | 136 | edgedraggingService.dragoverConnector = function(connector) { 137 | return function(event) { 138 | 139 | if (edgeDragging.isDragging) { 140 | edgedraggingService.dragover(event); 141 | try { 142 | Modelvalidation.validateEdges(model.edges.concat([{ 143 | source: draggedEdgeSource.id, 144 | destination: connector.id 145 | }]), model.nodes); 146 | } catch (error) { 147 | if (error instanceof Modelvalidation.ModelvalidationError) { 148 | return true; 149 | } else { 150 | throw error; 151 | } 152 | } 153 | if (isValidEdgeCallback(draggedEdgeSource, connector)) { 154 | event.preventDefault(); 155 | event.stopPropagation(); 156 | return false; 157 | } 158 | } 159 | }; 160 | }; 161 | 162 | edgedraggingService.dragleaveMagnet = function (event) { 163 | edgeDragging.magnetActive = false; 164 | }; 165 | 166 | edgedraggingService.dragoverMagnet = function(connector) { 167 | return function(event) { 168 | if (edgeDragging.isDragging) { 169 | edgedraggingService.dragover(event); 170 | try { 171 | Modelvalidation.validateEdges(model.edges.concat([{ 172 | source: draggedEdgeSource.id, 173 | destination: connector.id 174 | }]), model.nodes); 175 | } catch (error) { 176 | if (error instanceof Modelvalidation.ModelvalidationError) { 177 | return true; 178 | } else { 179 | throw error; 180 | } 181 | } 182 | if (isValidEdgeCallback(draggedEdgeSource, connector)) { 183 | if (dragAnimation == flowchartConstants.dragAnimationShadow) { 184 | 185 | edgeDragging.magnetActive = true; 186 | 187 | edgeDragging.dragPoint2 = modelservice.connectors.getCenteredCoord(connector.id); 188 | edgeDragging.pathElement.attr('d', Edgedrawingservice.getEdgeDAttribute(edgeDragging.dragPoint1, edgeDragging.dragPoint2, edgeStyle)); 189 | edgeDragging.circleElement.attr('cx', edgeDragging.dragPoint2.x); 190 | edgeDragging.circleElement.attr('cy', edgeDragging.dragPoint2.y); 191 | 192 | event.preventDefault(); 193 | event.stopPropagation(); 194 | return false; 195 | 196 | } else if (dragAnimation == flowchartConstants.dragAnimationRepaint) { 197 | return applyFunction(function() { 198 | edgeDragging.dragPoint2 = modelservice.connectors.getCenteredCoord(connector.id); 199 | event.preventDefault(); 200 | event.stopPropagation(); 201 | return false; 202 | }); 203 | } 204 | } 205 | } 206 | 207 | } 208 | }; 209 | 210 | edgedraggingService.dragend = function(event) { 211 | if (edgeDragging.isDragging) { 212 | edgeDragging.isDragging = false; 213 | edgeDragging.dragPoint1 = null; 214 | edgeDragging.dragPoint2 = null; 215 | event.stopPropagation(); 216 | 217 | if (dragAnimation == flowchartConstants.dragAnimationShadow) { 218 | edgeDragging.gElement.css('display', 'none'); 219 | } 220 | } 221 | }; 222 | 223 | edgedraggingService.drop = function(targetConnector) { 224 | return function(event) { 225 | if (edgeDragging.isDragging) { 226 | try { 227 | Modelvalidation.validateEdges(model.edges.concat([{ 228 | source: draggedEdgeSource.id, 229 | destination: targetConnector.id 230 | }]), model.nodes); 231 | } catch (error) { 232 | if (error instanceof Modelvalidation.ModelvalidationError) { 233 | return true; 234 | } else { 235 | throw error; 236 | } 237 | } 238 | 239 | if (isValidEdgeCallback(draggedEdgeSource, targetConnector)) { 240 | modelservice.edges._addEdge(draggedEdgeSource, targetConnector); 241 | event.stopPropagation(); 242 | event.preventDefault(); 243 | return false; 244 | } 245 | } 246 | } 247 | }; 248 | return edgedraggingService; 249 | } 250 | 251 | return factory; 252 | } 253 | 254 | angular.module('flowchart') 255 | .factory('Edgedraggingfactory', Edgedraggingfactory); 256 | 257 | }()); 258 | -------------------------------------------------------------------------------- /app/flowchart/edgedragging-service_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('edgedragging-service_test', function() { 4 | 5 | beforeEach(function() { 6 | module('flowchart', function($provide) { 7 | $provide.service('Edgedrawingservice', function() { 8 | this.test = 'test'; 9 | }); 10 | $provide.service('Modelvalidation', function() { 11 | this.validateEdges = jasmine.createSpy('validateEdges'); 12 | this.ModelvalidationError = function(){}; 13 | this.ModelvalidationError.prototype = Object.create(Error.prototype); 14 | this.ModelvalidationError.prototype.constructor = this.ModelvalidationError; 15 | }); 16 | }); 17 | module('flowchart'); 18 | }); 19 | 20 | function createEvent(name, clientX, clientY) { 21 | var event = jasmine.createSpyObj(name, ['stopPropagation', 'preventDefault']); 22 | event.target = angular.element('
')[0]; 23 | event.dataTransfer = jasmine.createSpyObj('datatransfer', ['setDragImage', 'setData']); 24 | event.clientX = clientX; 25 | event.clientY = clientY; 26 | return event; 27 | } 28 | 29 | beforeEach(inject(function(Edgedraggingfactory, flowchartConstants, Modelvalidation, Edgedrawingservice) { 30 | this.Modelvalidation = Modelvalidation; 31 | this.canvasCoords = {top: 200, left: 200}; 32 | this.canvasElement = jasmine.createSpyObj('canvasElement', ['getBoundingClientRect']); 33 | this.canvasElement.getBoundingClientRect.and.returnValue(angular.copy(this.canvasCoords)); 34 | 35 | this.dragAnimation = flowchartConstants.dragAnimationRepaint; 36 | this.edgeStyle = flowchartConstants.lineStyle; 37 | 38 | this.connector = {id: 1, type: flowchartConstants.bottomConnectorType}; 39 | this.destinationConnector = {id: 2, type: flowchartConstants.bottomConnectorType}; 40 | this.connectorCoords = {x: 100, y: 100}; 41 | 42 | this.modelservice = jasmine.createSpyObj('modelservice', ['getCanvasHtmlElement']); 43 | this.modelservice.getCanvasHtmlElement.and.returnValue(this.canvasElement); 44 | this.modelservice.connectors = jasmine.createSpyObj('modelservice.connectors', ['getCenteredCoord']); 45 | this.modelservice.connectors.getCenteredCoord.and.returnValue(angular.copy(this.connectorCoords)); 46 | this.modelservice.edges = {}; 47 | this.modelservice.edges._addEdge = jasmine.createSpy('_addEdge'); 48 | 49 | this.edgeDragging = {}; 50 | this.userIsValidEdgeCallback = jasmine.createSpy('isValidEdge').and.returnValue(true); 51 | this.applyFunction = jasmine.createSpy('apply').and.callFake(function(f) { 52 | return f() 53 | }); 54 | this.edgedraggingService = Edgedraggingfactory(this.modelservice, {nodes: [], edges: []}, this.edgeDragging, this.userIsValidEdgeCallback, this.applyFunction, this.dragAnimation, this.edgeStyle); 55 | 56 | this.startEvent = createEvent('startEvent', this.canvasCoords.left + this.connectorCoords.x, this.canvasCoords.top + this.connectorCoords.y); 57 | this.dragDistance = 20; 58 | this.overEvent = createEvent('overEvent', this.canvasCoords.left + this.connectorCoords.x + this.dragDistance, this.canvasCoords.top + this.connectorCoords.y + this.dragDistance); 59 | this.endEvent = createEvent('endEvent', this.canvasCoords.left + this.connectorCoords.x + this.dragDistance, this.canvasCoords.top + this.connectorCoords.y + this.dragDistance); 60 | this.dropEvent = createEvent('dropEvent', this.canvasCoords.left + this.connectorCoords.x + this.dragDistance, this.canvasCoords.top + this.connectorCoords.y + this.dragDistance); 61 | })); 62 | 63 | it('should initialize the edgeDragging', function() { 64 | expect(this.edgeDragging.isDragging).toBe(false); 65 | expect(this.edgeDragging.dragPoint1).toBeNull(); 66 | expect(this.edgeDragging.dragPoint2).toBeNull(); 67 | }); 68 | 69 | it('dragstart should initialize the scope, set an invisible dragimage, call setData and call stopPropagation.', function() { 70 | this.edgedraggingService.dragstart(angular.copy(this.connector))(this.startEvent); 71 | expect(this.edgeDragging.isDragging).toBe(true); 72 | expect(this.edgeDragging.dragPoint1).toEqual(this.connectorCoords); 73 | expect(this.edgeDragging.dragPoint2).toEqual(this.connectorCoords); 74 | //expect(this.startEvent.dataTransfer.setDragImage).toHaveBeenCalled(); 75 | expect(this.startEvent.dataTransfer.setData).toHaveBeenCalled(); 76 | expect(this.startEvent.stopPropagation).toHaveBeenCalled(); 77 | }); 78 | 79 | it('dragover should update the scope', function() { 80 | this.edgedraggingService.dragstart(angular.copy(this.connector))(this.startEvent); 81 | this.edgedraggingService.dragover(this.overEvent); 82 | expect(this.edgeDragging.isDragging).toBe(true); 83 | expect(this.edgeDragging.dragPoint1).toEqual(this.connectorCoords); 84 | expect(this.edgeDragging.dragPoint2).toEqual({ 85 | x: this.connectorCoords.x + this.dragDistance, 86 | y: this.connectorCoords.y + this.dragDistance 87 | }); 88 | }); 89 | 90 | it('dragover connector should call preventDefault and stopPropagation', function() { 91 | this.edgedraggingService.dragstart(angular.copy(this.connector))(this.startEvent); 92 | 93 | this.userIsValidEdgeCallback.and.returnValue(false); 94 | this.edgedraggingService.dragoverConnector(this.connector)(this.overEvent); 95 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 96 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 97 | expect(this.edgeDragging.dragPoint2).toEqual({x: this.connectorCoords.x + this.dragDistance, y: this.connectorCoords.y + this.dragDistance}); 98 | 99 | this.userIsValidEdgeCallback.and.returnValue(true); 100 | expect(this.edgedraggingService.dragoverConnector(this.connector)(this.overEvent)).toBe(false); 101 | expect(this.overEvent.stopPropagation).toHaveBeenCalled(); 102 | expect(this.overEvent.preventDefault).toHaveBeenCalled(); 103 | expect(this.edgeDragging.dragPoint2).toEqual({x: this.connectorCoords.x + this.dragDistance, y: this.connectorCoords.y + this.dragDistance}); 104 | }); 105 | 106 | it('dragover magnet should call preventDefault and stopPropagation, it should perform user validation.', function() { 107 | this.edgedraggingService.dragstart(angular.copy(this.connector))(this.startEvent); 108 | 109 | this.userIsValidEdgeCallback.and.returnValue(false); 110 | this.edgedraggingService.dragoverMagnet(this.connector)(this.overEvent); 111 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 112 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 113 | expect(this.edgeDragging.dragPoint2).toEqual({x: this.connectorCoords.x + this.dragDistance, y: this.connectorCoords.y + this.dragDistance}); 114 | 115 | this.userIsValidEdgeCallback.and.returnValue(true); 116 | expect(this.edgedraggingService.dragoverMagnet(this.destinationConnector)(this.overEvent)).toBe(false); 117 | expect(this.applyFunction).toHaveBeenCalled(); 118 | expect(this.overEvent.stopPropagation).toHaveBeenCalled(); 119 | expect(this.overEvent.preventDefault).toHaveBeenCalled(); 120 | expect(this.edgeDragging.dragPoint2).toEqual(this.connectorCoords); 121 | }); 122 | 123 | it('dragend should reset the scope and call stopPropagation', function() { 124 | this.edgedraggingService.dragstart(angular.copy(this.connector))(this.startEvent); 125 | this.edgedraggingService.dragend(this.endEvent); 126 | 127 | expect(this.edgeDragging.isDragging).toBe(false); 128 | expect(this.edgeDragging.dragPoint1).toBeNull(); 129 | expect(this.edgeDragging.dragPoint2).toBeNull(); 130 | expect(this.endEvent.stopPropagation).toHaveBeenCalled(); 131 | }); 132 | 133 | it('drop should add a new edge, if sourceconnector and targetconnector differ. Also preventDefault and stopPropagation must be called', function() { 134 | this.edgedraggingService.dragstart(angular.copy(this.connector))(this.startEvent); 135 | 136 | this.userIsValidEdgeCallback.and.returnValue(false); 137 | this.edgedraggingService.drop(this.destinationConnector)(this.dropEvent); 138 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 139 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 140 | expect(this.modelservice.edges._addEdge).not.toHaveBeenCalled(); 141 | 142 | this.userIsValidEdgeCallback.and.returnValue(true); 143 | expect(this.edgedraggingService.drop(this.destinationConnector)(this.dropEvent)).toBe(false); 144 | 145 | expect(this.modelservice.edges._addEdge).toHaveBeenCalledWith(this.connector, this.destinationConnector); 146 | expect(this.dropEvent.stopPropagation).toHaveBeenCalled(); 147 | expect(this.dropEvent.preventDefault).toHaveBeenCalled(); 148 | }); 149 | 150 | it('should fix the internet explorer setDragImage bug', function() { 151 | this.startEvent.dataTransfer.setDragImage = null; 152 | this.edgedraggingService.dragstart(this.connector)(this.startEvent); 153 | 154 | expect(this.startEvent.target.style.display).toEqual('none'); 155 | 156 | this.edgedraggingService.dragover(this.overEvent); 157 | expect(this.startEvent.target.style.display).toEqual(''); 158 | }); 159 | 160 | it('dragover and drop should perform modelvalidation', function() { 161 | var that = this; 162 | 163 | this.startEvent.dataTransfer.setDragImage = null; 164 | this.edgedraggingService.dragstart(this.connector)(this.startEvent); 165 | 166 | this.Modelvalidation.validateEdges.and.throwError(new this.Modelvalidation.ModelvalidationError()); 167 | expect(this.edgedraggingService.drop(this.destinationConnector)(this.dropEvent)).toBe(true); 168 | expect(this.dropEvent.preventDefault).not.toHaveBeenCalled(); 169 | expect(this.dropEvent.stopPropagation).not.toHaveBeenCalled(); 170 | expect(this.modelservice.edges._addEdge).not.toHaveBeenCalled(); 171 | 172 | expect(this.edgedraggingService.dragoverConnector(this.destinationConnector)(this.overEvent)).toBe(true); 173 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 174 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 175 | 176 | expect(this.edgedraggingService.dragoverMagnet(this.destinationConnector)(this.overEvent)).toBe(true); 177 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 178 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 179 | expect(this.edgeDragging.dragPoint2).toEqual({x: this.connectorCoords.x + this.dragDistance, y: this.connectorCoords.y + this.dragDistance}); 180 | 181 | 182 | this.Modelvalidation.validateEdges.and.throwError(new Error('Test')); 183 | expect(function() {that.edgedraggingService.drop(that.destinationConnector)(that.dropEvent);}).toThrowError('Test'); 184 | expect(this.dropEvent.preventDefault).not.toHaveBeenCalled(); 185 | expect(this.dropEvent.stopPropagation).not.toHaveBeenCalled(); 186 | expect(this.modelservice.edges._addEdge).not.toHaveBeenCalled(); 187 | 188 | expect(function() {that.edgedraggingService.dragoverConnector(that.destinationConnector)(that.overEvent);}).toThrowError('Test'); 189 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 190 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 191 | 192 | expect(function() {that.edgedraggingService.dragoverMagnet(that.destinationConnector)(that.overEvent);}).toThrowError('Test'); 193 | expect(this.overEvent.preventDefault).not.toHaveBeenCalled(); 194 | expect(this.overEvent.stopPropagation).not.toHaveBeenCalled(); 195 | expect(this.edgeDragging.dragPoint2).toEqual({x: this.connectorCoords.x + this.dragDistance, y: this.connectorCoords.y + this.dragDistance}); 196 | }); 197 | 198 | }); 199 | -------------------------------------------------------------------------------- /app/flowchart/edgedrawing-service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function Edgedrawingservice(flowchartConstants) { 6 | function computeEdgeTangentOffset(pt1, pt2) { 7 | return (pt2.y - pt1.y) / 2; 8 | } 9 | 10 | function computeEdgeSourceTangent(pt1, pt2) { 11 | return { 12 | x: pt1.x, 13 | y: pt1.y + computeEdgeTangentOffset(pt1, pt2) 14 | }; 15 | } 16 | 17 | function computeEdgeDestinationTangent(pt1, pt2) { 18 | return { 19 | x: pt2.x, 20 | y: pt2.y - computeEdgeTangentOffset(pt1, pt2) 21 | }; 22 | } 23 | 24 | this.getEdgeDAttribute = function(pt1, pt2, style) { 25 | var dAddribute = 'M ' + pt1.x + ', ' + pt1.y + ' '; 26 | if (style === flowchartConstants.curvedStyle) { 27 | var sourceTangent = computeEdgeSourceTangent(pt1, pt2); 28 | var destinationTangent = computeEdgeDestinationTangent(pt1, pt2); 29 | dAddribute += 'C ' + sourceTangent.x + ', ' + sourceTangent.y + ' ' + destinationTangent.x + ', ' + destinationTangent.y + ' ' + pt2.x + ', ' + pt2.y; 30 | } else { 31 | dAddribute += 'L ' + pt2.x + ', ' + pt2.y; 32 | } 33 | return dAddribute; 34 | }; 35 | } 36 | 37 | angular 38 | .module('flowchart') 39 | .service('Edgedrawingservice', Edgedrawingservice); 40 | 41 | }()); 42 | -------------------------------------------------------------------------------- /app/flowchart/edgedrawing-service_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('The edgedrawing service', function() { 4 | 5 | 6 | beforeEach(function() { 7 | module('flowchart'); 8 | }); 9 | 10 | beforeEach(inject(function(Edgedrawingservice, flowchartConstants) { 11 | this.Edgedrawingservice = Edgedrawingservice; 12 | this.flowchartConstants = flowchartConstants; 13 | this.startPoint = {x: 10, y: 10}; 14 | this.endPoint = {x: 50, y: 50}; 15 | 16 | this.LINE_MATCHER = 'M 10, 10 L 50, 50'; 17 | this.CURVE_MATCHER = /^M 10, 10 C (.*) 50, 50$/; // Move to start point, curve and end at endpoint. 18 | 19 | })); 20 | 21 | it('should implement linestyle', function() { 22 | var line = this.Edgedrawingservice.getEdgeDAttribute(angular.copy(this.startPoint), angular.copy(this.endPoint), this.flowchartConstants.lineStyle); 23 | expect(line).toEqual(this.LINE_MATCHER); 24 | }); 25 | 26 | it('should implement curvedstyle', function() { 27 | var curve = this.Edgedrawingservice.getEdgeDAttribute(angular.copy(this.startPoint), angular.copy(this.endPoint), this.flowchartConstants.curvedStyle); 28 | expect(curve).toMatch(this.CURVE_MATCHER); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /app/flowchart/flowchart-constant.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | var constants = { 6 | htmlPrefix: 'fc', 7 | topConnectorType: 'topConnector', 8 | bottomConnectorType: 'bottomConnector', 9 | curvedStyle: 'curved', 10 | lineStyle: 'line', 11 | dragAnimationRepaint: 'repaint', 12 | dragAnimationShadow: 'shadow' 13 | }; 14 | constants.canvasClass = constants.htmlPrefix + '-canvas'; 15 | constants.selectedClass = constants.htmlPrefix + '-selected'; 16 | constants.activeClass = constants.htmlPrefix + '-active'; 17 | constants.hoverClass = constants.htmlPrefix + '-hover'; 18 | constants.draggingClass = constants.htmlPrefix + '-dragging'; 19 | constants.edgeClass = constants.htmlPrefix + '-edge'; 20 | constants.connectorClass = constants.htmlPrefix + '-connector'; 21 | constants.magnetClass = constants.htmlPrefix + '-magnet'; 22 | constants.nodeClass = constants.htmlPrefix + '-node'; 23 | constants.topConnectorClass = constants.htmlPrefix + '-' + constants.topConnectorType + 's'; 24 | constants.bottomConnectorClass = constants.htmlPrefix + '-' + constants.bottomConnectorType + 's'; 25 | constants.canvasResizeThreshold = 200; 26 | constants.canvasResizeStep = 200; 27 | 28 | angular 29 | .module('flowchart') 30 | .constant('flowchartConstants', constants); 31 | 32 | }()); 33 | -------------------------------------------------------------------------------- /app/flowchart/flowchart.css: -------------------------------------------------------------------------------- 1 | .fc-canvas { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .fc-canvas svg { 8 | position: relative; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /app/flowchart/flowchart.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('flowchart', ['flowchart-templates']); 7 | 8 | }()); 9 | -------------------------------------------------------------------------------- /app/flowchart/magnet-directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function fcMagnet(flowchartConstants) { 6 | return { 7 | restrict: 'AE', 8 | link: function(scope, element) { 9 | element.addClass(flowchartConstants.magnetClass); 10 | 11 | element.on('dragover', scope.fcCallbacks.edgeDragoverMagnet(scope.connector)); 12 | element.on('dragleave', scope.fcCallbacks.edgeDragleaveMagnet); 13 | element.on('drop', scope.fcCallbacks.edgeDrop(scope.connector)); 14 | element.on('dragend', scope.fcCallbacks.edgeDragend); 15 | } 16 | } 17 | } 18 | 19 | angular.module('flowchart') 20 | .directive('fcMagnet', fcMagnet); 21 | }()); 22 | -------------------------------------------------------------------------------- /app/flowchart/magnet-directive_test.js: -------------------------------------------------------------------------------- 1 | describe('The magnet-directive', function() { 2 | 3 | var flowchartConstants; 4 | 5 | beforeEach(function() { 6 | module('flowchart'); 7 | }); 8 | 9 | beforeEach(inject(function(_$compile_, _$rootScope_, _flowchartConstants_) { 10 | var $compile = _$compile_; 11 | var $rootScope = _$rootScope_; 12 | flowchartConstants = _flowchartConstants_; 13 | 14 | this.scope = $rootScope.$new(); 15 | this.scope.fcCallbacks = jasmine.createSpyObj('callbacks', ['edgeDragoverMagnet', 'edgeDrop', 'edgeDragend']); 16 | 17 | this.innerEdgeDragoverMagnet = jasmine.createSpy('innerEdgeDragoverMagnet'); 18 | this.scope.fcCallbacks.edgeDragoverMagnet.and.returnValue(this.innerEdgeDragoverMagnet); 19 | 20 | this.innerEdgeDrop = jasmine.createSpy('innerEdgeDrop'); 21 | this.scope.fcCallbacks.edgeDrop.and.returnValue(this.innerEdgeDrop); 22 | 23 | this.magnet = $compile('')(this.scope); 24 | 25 | })); 26 | 27 | it('should register a magnet dragover handler', function() { 28 | expect(this.innerEdgeDragoverMagnet).not.toHaveBeenCalled(); 29 | this.magnet.triggerHandler('dragover'); 30 | expect(this.innerEdgeDragoverMagnet).toHaveBeenCalled(); 31 | }); 32 | 33 | it('should register a magnet drop handler', function() { 34 | expect(this.innerEdgeDrop).not.toHaveBeenCalled(); 35 | this.magnet.triggerHandler('drop'); 36 | expect(this.innerEdgeDrop).toHaveBeenCalled(); 37 | }); 38 | 39 | it('should register a magnet dragend handler', function() { 40 | expect(this.scope.fcCallbacks.edgeDragend).not.toHaveBeenCalled(); 41 | this.magnet.triggerHandler('dragend'); 42 | expect(this.scope.fcCallbacks.edgeDragend).toHaveBeenCalled(); 43 | }); 44 | 45 | it('should add the fc-magnet class', function() { 46 | expect(this.magnet.hasClass(flowchartConstants.magnetClass)).toBe(true); 47 | }) 48 | }); 49 | -------------------------------------------------------------------------------- /app/flowchart/model-service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function Modelfactory(Modelvalidation) { 6 | var connectorsHtmlElements = {}; 7 | var canvasHtmlElement = null; 8 | var svgHtmlElement = null; 9 | 10 | return function innerModelfactory(model, selectedObjects, edgeAddedCallback, nodeRemovedCallback, edgeRemovedCallback) { 11 | Modelvalidation.validateModel(model); 12 | var modelservice = { 13 | selectedObjects: selectedObjects 14 | }; 15 | 16 | modelservice.edgeAddedCallback = edgeAddedCallback || angular.noop; 17 | modelservice.nodeRemovedCallback = nodeRemovedCallback || angular.noop; 18 | modelservice.edgeRemovedCallback = edgeRemovedCallback || angular.noop; 19 | 20 | function selectObject(object) { 21 | if (modelservice.selectedObjects.indexOf(object) === -1) { 22 | modelservice.selectedObjects.push(object); 23 | } 24 | } 25 | 26 | function deselectObject(object) { 27 | var index = modelservice.selectedObjects.indexOf(object); 28 | if (index === -1) { 29 | throw new Error('Tried to deselect an unselected object'); 30 | } 31 | modelservice.selectedObjects.splice(index, 1); 32 | } 33 | 34 | function toggleSelectedObject(object) { 35 | if (isSelectedObject(object)) { 36 | deselectObject(object); 37 | } else { 38 | selectObject(object); 39 | } 40 | } 41 | 42 | function isSelectedObject(object) { 43 | return modelservice.selectedObjects.indexOf(object) !== -1; 44 | } 45 | 46 | modelservice.connectors = { 47 | 48 | getConnector: function(connectorId) { 49 | for(var i=0; i')($rootScope); 90 | $rootScope.$digest(); 91 | return node; 92 | } 93 | 94 | it('should be draggable', function() { 95 | var node = getCompiledNode(); 96 | expect(node.attr('draggable')).toBe('true'); 97 | }); 98 | 99 | it('should have the node class', function() { 100 | var node = getCompiledNode(); 101 | expect(node.hasClass(flowchartConstants.nodeClass)).toBe(true); 102 | }); 103 | 104 | it('should have a selected class if selected', function() { 105 | $rootScope.selected = true; 106 | var node = getCompiledNode(); 107 | expect(node.hasClass(flowchartConstants.selectedClass)).toBe(true); 108 | 109 | $rootScope.selected = false; 110 | $rootScope.$apply(); 111 | expect(node.hasClass(flowchartConstants.selectedClass)).toBe(false); 112 | }); 113 | 114 | it('should have a hovered class if hovered', function() { 115 | $rootScope.underMouse = true; 116 | var node = getCompiledNode(); 117 | expect(node.hasClass(flowchartConstants.hoverClass)).toBe(true); 118 | 119 | $rootScope.underMouse = false; 120 | $rootScope.$apply(); 121 | expect(node.hasClass(flowchartConstants.hoverClass)).toBe(false); 122 | }); 123 | 124 | it('should have a dragging class if dragged', function() { 125 | $rootScope.node2 = node2; 126 | $rootScope.draggedNode = node; 127 | 128 | // This tests works on two nodes, since issue https://github.com/ONE-LOGIC/ngFlowchart/issues/5 129 | var nodes = $compile('' + 130 | '')($rootScope); 131 | $rootScope.$digest(); 132 | 133 | var n1 = angular.element(nodes[0]); 134 | var n2 = angular.element(nodes[1]); 135 | expect(n1.hasClass(flowchartConstants.draggingClass)).toBe(true); 136 | expect(n2.hasClass(flowchartConstants.draggingClass)).toBe(false); 137 | 138 | $rootScope.draggedNode = null; 139 | $rootScope.$apply(); 140 | expect(n1.hasClass(flowchartConstants.draggingClass)).toBe(false); 141 | }); 142 | 143 | 144 | it('should register the dragstart, dragend, mouseenter, mouseleave and click event', function() { 145 | var htmlNode = getCompiledNode(); 146 | 147 | expect(this.innerNodeClicked).not.toHaveBeenCalled(); 148 | htmlNode.triggerHandler('click'); 149 | expect(this.innerNodeClicked).toHaveBeenCalled(); 150 | 151 | expect(this.innerNodeDragStart).not.toHaveBeenCalled(); 152 | htmlNode.triggerHandler('dragstart'); 153 | expect(this.innerNodeDragStart).toHaveBeenCalled(); 154 | 155 | expect(this.innerNodeMouseOver).not.toHaveBeenCalled(); 156 | htmlNode.triggerHandler('mouseover'); 157 | expect(this.innerNodeMouseOver).toHaveBeenCalled(); 158 | 159 | expect(this.innerNodeMouseOut).not.toHaveBeenCalled(); 160 | htmlNode.triggerHandler('mouseout'); 161 | expect(this.innerNodeMouseOut).toHaveBeenCalled(); 162 | 163 | expect($rootScope.callbacks.nodeDragend).not.toHaveBeenCalled(); 164 | htmlNode.triggerHandler('dragend'); 165 | expect($rootScope.callbacks.nodeDragend).toHaveBeenCalled(); 166 | }); 167 | 168 | }) 169 | ; 170 | -------------------------------------------------------------------------------- /app/flowchart/node.html: -------------------------------------------------------------------------------- 1 |
5 |
6 |

{{ node.name }}

7 | 8 |
9 |
11 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 | × 23 |
24 |
25 | -------------------------------------------------------------------------------- /app/flowchart/nodeTemplatePath-provider.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | angular 6 | .module('flowchart') 7 | .provider('NodeTemplatePath', NodeTemplatePath); 8 | 9 | function NodeTemplatePath() { 10 | var templatePath = "flowchart/node.html"; 11 | 12 | this.setTemplatePath = setTemplatePath; 13 | this.$get = NodeTemplatePath; 14 | 15 | function setTemplatePath(path) { 16 | templatePath = path; 17 | } 18 | 19 | function NodeTemplatePath() { 20 | return templatePath; 21 | } 22 | } 23 | 24 | }()); 25 | -------------------------------------------------------------------------------- /app/flowchart/nodedragging-service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | function Nodedraggingfactory(flowchartConstants) { 6 | return function(modelservice, nodeDraggingScope, applyFunction, automaticResize, dragAnimation) { 7 | 8 | var dragOffset = {}; 9 | var draggedElement = null; 10 | nodeDraggingScope.draggedNode = null; 11 | nodeDraggingScope.shadowDragStarted = false; 12 | 13 | var destinationHtmlElement = null; 14 | var oldDisplayStyle = ""; 15 | 16 | function getCoordinate(coordinate, max) { 17 | coordinate = Math.max(coordinate, 0); 18 | coordinate = Math.min(coordinate, max); 19 | return coordinate; 20 | } 21 | function getXCoordinate(x) { 22 | return getCoordinate(x, modelservice.getCanvasHtmlElement().offsetWidth); 23 | } 24 | function getYCoordinate(y) { 25 | return getCoordinate(y, modelservice.getCanvasHtmlElement().offsetHeight); 26 | } 27 | function resizeCanvas(draggedNode, nodeElement) { 28 | if (automaticResize) { 29 | var canvasElement = modelservice.getCanvasHtmlElement(); 30 | if (canvasElement.offsetWidth < draggedNode.x + nodeElement.offsetWidth + flowchartConstants.canvasResizeThreshold) { 31 | canvasElement.style.width = canvasElement.offsetWidth + flowchartConstants.canvasResizeStep + 'px'; 32 | } 33 | if (canvasElement.offsetHeight < draggedNode.y + nodeElement.offsetHeight + flowchartConstants.canvasResizeThreshold) { 34 | canvasElement.style.height = canvasElement.offsetHeight + flowchartConstants.canvasResizeStep + 'px'; 35 | } 36 | } 37 | } 38 | return { 39 | dragstart: function(node) { 40 | return function(event) { 41 | modelservice.deselectAll(); 42 | modelservice.nodes.select(node); 43 | nodeDraggingScope.draggedNode = node; 44 | draggedElement = event.target; 45 | 46 | var element = angular.element(event.target); 47 | dragOffset.x = parseInt(element.css('left')) - event.clientX; 48 | dragOffset.y = parseInt(element.css('top')) - event.clientY; 49 | 50 | if (dragAnimation == flowchartConstants.dragAnimationShadow) { 51 | var shadowElement = angular.element('

'+ nodeDraggingScope.draggedNode.name +'

'); 52 | var targetInnerNode = angular.element(event.target).children()[0]; 53 | shadowElement.children()[0].style.backgroundColor = targetInnerNode.style.backgroundColor; 54 | nodeDraggingScope.shadowElement = shadowElement; 55 | var canvasElement = modelservice.getCanvasHtmlElement(); 56 | canvasElement.appendChild(nodeDraggingScope.shadowElement[0]); 57 | } 58 | 59 | event.dataTransfer.setData('Text', 'Just to support firefox'); 60 | if (event.dataTransfer.setDragImage) { 61 | var invisibleDiv = angular.element('
')[0]; // This divs stays invisible, because it is not in the dom. 62 | event.dataTransfer.setDragImage(invisibleDiv, 0, 0); 63 | } else { 64 | destinationHtmlElement = event.target; 65 | oldDisplayStyle = destinationHtmlElement.style.display; 66 | event.target.style.display = 'none'; // Internetexplorer does not support setDragImage, but it takes an screenshot, from the draggedelement and uses it as dragimage. 67 | // Since angular redraws the element in the next dragover call, display: none never gets visible to the user. 68 | if (dragAnimation == flowchartConstants.dragAnimationShadow) { 69 | // IE Drag Fix 70 | nodeDraggingScope.shadowDragStarted = true; 71 | } 72 | } 73 | }; 74 | }, 75 | 76 | drop: function(event) { 77 | if (nodeDraggingScope.draggedNode) { 78 | return applyFunction(function() { 79 | nodeDraggingScope.draggedNode.x = getXCoordinate(dragOffset.x + event.clientX); 80 | nodeDraggingScope.draggedNode.y = getYCoordinate(dragOffset.y + event.clientY); 81 | event.preventDefault(); 82 | return false; 83 | }) 84 | } 85 | }, 86 | 87 | dragover: function(event) { 88 | if (dragAnimation == flowchartConstants.dragAnimationRepaint) { 89 | if (nodeDraggingScope.draggedNode) { 90 | return applyFunction(function() { 91 | nodeDraggingScope.draggedNode.x = getXCoordinate(dragOffset.x + event.clientX); 92 | nodeDraggingScope.draggedNode.y = getYCoordinate(dragOffset.y + event.clientY); 93 | resizeCanvas(nodeDraggingScope.draggedNode, draggedElement); 94 | event.preventDefault(); 95 | return false; 96 | }); 97 | } 98 | } else if (dragAnimation == flowchartConstants.dragAnimationShadow) { 99 | if (nodeDraggingScope.draggedNode) { 100 | if(nodeDraggingScope.shadowDragStarted) { 101 | applyFunction(function() { 102 | destinationHtmlElement.style.display = oldDisplayStyle; 103 | nodeDraggingScope.shadowDragStarted = false; 104 | }); 105 | } 106 | nodeDraggingScope.shadowElement.css('left', getXCoordinate(dragOffset.x + event.clientX) + 'px'); 107 | nodeDraggingScope.shadowElement.css('top', getYCoordinate(dragOffset.y + event.clientY) + 'px'); 108 | resizeCanvas(nodeDraggingScope.draggedNode, draggedElement); 109 | event.preventDefault(); 110 | } 111 | } 112 | }, 113 | 114 | dragend: function(event) { 115 | applyFunction(function() { 116 | if (nodeDraggingScope.shadowElement) { 117 | nodeDraggingScope.draggedNode.x = parseInt(nodeDraggingScope.shadowElement.css('left').replace('px','')); 118 | nodeDraggingScope.draggedNode.y = parseInt(nodeDraggingScope.shadowElement.css('top').replace('px','')); 119 | 120 | modelservice.getCanvasHtmlElement().removeChild(nodeDraggingScope.shadowElement[0]); 121 | nodeDraggingScope.shadowElement = null; 122 | } 123 | 124 | if (nodeDraggingScope.draggedNode) { 125 | nodeDraggingScope.draggedNode = null; 126 | draggedElement = null; 127 | dragOffset.x = 0; 128 | dragOffset.y = 0; 129 | } 130 | }); 131 | } 132 | }; 133 | }; 134 | } 135 | 136 | angular 137 | .module('flowchart') 138 | .factory('Nodedraggingfactory', Nodedraggingfactory); 139 | 140 | }()); 141 | -------------------------------------------------------------------------------- /app/flowchart/nodedragging-service_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('test the nodedragging service', function() { 4 | 5 | var CANVAS_LENGTH = 1000; 6 | var $rootScope; 7 | var $document; 8 | var nodedragging; 9 | var modelservice; 10 | 11 | 12 | beforeEach(function() { 13 | module(function($provide) { 14 | $provide.service('modelservice', function() { 15 | this.nodes = {}; 16 | this.nodes.select = jasmine.createSpy('modelservice.nodes.select'); 17 | this.deselectAll = jasmine.createSpy('modelservice.deselectAll'); 18 | var canvasHtmlElement = jasmine.createSpyObj('canvasHtmlElement', ['test']); 19 | canvasHtmlElement.offsetHeight = CANVAS_LENGTH; 20 | canvasHtmlElement.offsetWidth = CANVAS_LENGTH; 21 | this.getCanvasHtmlElement = jasmine.createSpy('modelservice.getCanvasHtmlElement').and.returnValue(canvasHtmlElement); 22 | }); 23 | }); 24 | module('flowchart'); 25 | }); 26 | 27 | beforeEach(inject(function(_$document_, _modelservice_, _$rootScope_, Nodedraggingfactory) { 28 | $rootScope = _$rootScope_; 29 | modelservice = _modelservice_; 30 | $document = _$document_; 31 | 32 | this.$scope = $rootScope.$new(); 33 | 34 | this.node = { 35 | id: 1, 36 | x: 0, 37 | y: 0, 38 | name: 'testnode' 39 | }; 40 | 41 | this.automaticResize = false; 42 | this.dragAnimation = 'repaint'; 43 | 44 | this.fakeEvent = { 45 | preventDefault: jasmine.createSpy('preventDefault'), 46 | target: angular.element('
')[0], 47 | dataTransfer: { 48 | setData: jasmine.createSpy('setData'), 49 | setDragImage: jasmine.createSpy('setDragImage') 50 | }, 51 | clientX: 0, 52 | clientY: 0 53 | }; 54 | 55 | this.$scope.draggingNode = {}; 56 | nodedragging = Nodedraggingfactory(modelservice, this.$scope.draggingNode, this.$scope.$apply.bind(this.$scope), this.automaticResize, this.dragAnimation); 57 | })); 58 | 59 | it('dragstart should select the node witch is dragged and deselect all others', function() { 60 | var innerDragStart = nodedragging.dragstart(this.node); 61 | innerDragStart(this.fakeEvent); 62 | 63 | expect(modelservice.deselectAll).toHaveBeenCalled(); 64 | expect(modelservice.nodes.select).toHaveBeenCalledWith(this.node); 65 | }); 66 | 67 | it('dragstart should set $scope.nodeDragging.draggedNode', function() { 68 | this.$scope.draggingNode.draggedNode = null; 69 | 70 | var innerDragStart = nodedragging.dragstart(this.node); 71 | innerDragStart(this.fakeEvent); 72 | 73 | expect(this.$scope.draggingNode.draggedNode).toBe(this.node); 74 | }); 75 | 76 | it('dragstart should call setData to support firefox', function() { 77 | 78 | var innerDragStart = nodedragging.dragstart(this.node); 79 | innerDragStart(this.fakeEvent); 80 | 81 | expect(this.fakeEvent.dataTransfer.setData).toHaveBeenCalled(); 82 | }); 83 | 84 | it('should drop the defaultNode under the mousepointer and prevent default', function() { 85 | var clientX = 100; 86 | var clientY = 100; 87 | this.fakeEvent.clientX = clientX; 88 | this.fakeEvent.clientY = clientY; 89 | 90 | nodedragging.dragstart(this.node)(this.fakeEvent); 91 | expect(nodedragging.drop(this.fakeEvent)).toBe(false); 92 | expect(this.fakeEvent.preventDefault).toHaveBeenCalled(); 93 | 94 | expect(this.node.x).toEqual(clientX); 95 | expect(this.node.y).toEqual(clientY); 96 | }); 97 | 98 | it('dragover should preventdefault, update node coordinates and prevent dragging outside of the canvas', function() { 99 | var clientX = 100; 100 | var clientY = 100; 101 | this.fakeEvent.clientX = clientX; 102 | this.fakeEvent.clientY = clientY; 103 | 104 | nodedragging.dragstart(this.node)(this.fakeEvent); 105 | expect(nodedragging.dragover(this.fakeEvent)).toBe(false); 106 | expect(this.fakeEvent.preventDefault).toHaveBeenCalled(); 107 | 108 | expect(this.node.x).toEqual(clientX); 109 | expect(this.node.y).toEqual(clientY); 110 | 111 | clientX = -2; 112 | clientY = -2; 113 | this.fakeEvent.clientX = clientX; 114 | this.fakeEvent.clientY = clientY; 115 | expect(nodedragging.dragover(this.fakeEvent)).toBe(false); 116 | expect(this.node.x).toEqual(0); 117 | expect(this.node.y).toEqual(0); 118 | 119 | clientX = CANVAS_LENGTH + 1; 120 | clientY = CANVAS_LENGTH + 1; 121 | this.fakeEvent.clientX = clientX; 122 | this.fakeEvent.clientY = clientY; 123 | expect(nodedragging.dragover(this.fakeEvent)).toBe(false); 124 | expect(this.node.x).toEqual(CANVAS_LENGTH); 125 | expect(this.node.y).toEqual(CANVAS_LENGTH); 126 | }); 127 | 128 | it('dragover should prevent dragging outside of the canvas', function() { 129 | 130 | }); 131 | 132 | it('should reset all variables when dragging ends.', function() { 133 | nodedragging.dragstart(this.node)(this.fakeEvent); 134 | nodedragging.dragend(this.fakeEvent); 135 | 136 | expect(this.$scope.draggingNode.draggedNode).toBe(null); 137 | }); 138 | 139 | it('should do nothing if no node is dragged', function() { 140 | this.$scope.draggingNode.draggedNode = null; 141 | 142 | expect(nodedragging.dragend(this.fakeEvent)).not.toBe(false); 143 | expect(this.fakeEvent.preventDefault).not.toHaveBeenCalled(); 144 | 145 | expect(nodedragging.drop(this.fakeEvent)).not.toBe(false); 146 | expect(this.fakeEvent.preventDefault).not.toHaveBeenCalled(); 147 | 148 | expect(nodedragging.dragover(this.fakeEvent)).not.toBe(false); 149 | expect(this.fakeEvent.preventDefault).not.toHaveBeenCalled(); 150 | 151 | expect(this.$scope.draggingNode.draggedNode).toBe(null); 152 | }); 153 | 154 | it('should fix the internet explorer setDragImage bug', function() { 155 | this.fakeEvent.dataTransfer.setDragImage = null; 156 | nodedragging.dragstart(this.node)(this.fakeEvent); 157 | 158 | expect(this.fakeEvent.target.style.display).toEqual('none'); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /app/flowchart/onedatanode.html: -------------------------------------------------------------------------------- 1 |
4 |

{{ node.name }}

5 | 6 |
7 |
9 |
10 |
11 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /app/flowchart/topsort-service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * 7 | * @returns {Function} 8 | * @constructor 9 | */ 10 | function Topsortservice() { 11 | /** 12 | * @returns An array of node ids as string. ['idOfFirstNode', 'idOfSecondNode', ...]. Tbis is not exactly the best way to return ids, but until now there is no need for a better return. 13 | */ 14 | return function(graph) { 15 | 16 | // Build adjacent list with incoming and outgoing edges. 17 | var adjacentList = {}; 18 | angular.forEach(graph.nodes, function(node) { 19 | adjacentList[node.id] = {incoming: 0, outgoing: []}; 20 | }); 21 | angular.forEach(graph.edges, function(edge) { 22 | var sourceNode = graph.nodes.filter(function(node) { 23 | return node.connectors.some(function(connector) { 24 | return connector.id === edge.source; 25 | }) 26 | })[0]; 27 | var destinationNode = graph.nodes.filter(function(node) { 28 | return node.connectors.some(function(connector) { 29 | return connector.id === edge.destination; 30 | }) 31 | })[0]; 32 | 33 | adjacentList[sourceNode.id].outgoing.push(destinationNode.id); 34 | adjacentList[destinationNode.id].incoming++; 35 | }); 36 | 37 | var orderedNodes = []; 38 | var sourceNodes = []; 39 | angular.forEach(adjacentList, function(edges, node) { 40 | if (edges.incoming === 0) { 41 | sourceNodes.push(node); 42 | } 43 | }); 44 | while (sourceNodes.length !== 0) { 45 | var sourceNode = sourceNodes.pop(); 46 | for (var i = 0; i < adjacentList[sourceNode].outgoing.length; i++) { 47 | var destinationNode = adjacentList[sourceNode].outgoing[i]; 48 | adjacentList[destinationNode].incoming--; 49 | if (adjacentList[destinationNode].incoming === 0) { 50 | sourceNodes.push('' + destinationNode); 51 | } 52 | adjacentList[sourceNode].outgoing.splice(i, 1); 53 | i--; 54 | } 55 | orderedNodes.push(sourceNode); 56 | } 57 | 58 | var hasEdges = false; 59 | angular.forEach(adjacentList, function(edges) { 60 | if (edges.incoming !== 0) { 61 | hasEdges = true; 62 | } 63 | }); 64 | if (hasEdges) { 65 | return null; 66 | } else { 67 | return orderedNodes; 68 | } 69 | 70 | } 71 | } 72 | 73 | angular.module('flowchart') 74 | .factory('Topsortservice', Topsortservice); 75 | })(); 76 | -------------------------------------------------------------------------------- /app/flowchart/topsort-service_test.js: -------------------------------------------------------------------------------- 1 | describe('The topsort service', function() { 2 | 3 | beforeEach(module('flowchart')); 4 | 5 | beforeEach(inject(function(Topsortservice) { 6 | this.Topsortservice = Topsortservice; 7 | })); 8 | 9 | it('should find direct circles', function() { 10 | var that = this; 11 | var circularModel = { 12 | nodes: [ 13 | {id: 1, name: '', x: 0, y: 0, connectors: [{id: 1, type: ''}, {id: 2, type: ''}]}, 14 | {id: 2, name: '', x: 0, y: 0, connectors: [{id: 3, type: ''}]} 15 | ], 16 | edges: [ 17 | {source: 1, destination: 3}, 18 | {source: 3, destination: 2} 19 | ] 20 | }; 21 | expect(that.Topsortservice(circularModel)).toBeNull(); 22 | }); 23 | 24 | it('should find indirect circles', function() { 25 | var that = this; 26 | 27 | var circularModel = { 28 | nodes: [ 29 | {id: 1, name: '', x: 0, y: 0, connectors: [{id: 1, type: ''}, {id: 2, type: ''}]}, 30 | {id: 2, name: '', x: 0, y: 0, connectors: [{id: 3, type: ''}]}, 31 | {id: 3, name: '', x: 0, y: 0, connectors: [{id: 4, type: ''}]} 32 | ], 33 | edges: [ 34 | {source: 1, destination: 3}, 35 | {source: 3, destination: 4}, 36 | {source: 4, destination: 1} 37 | ] 38 | }; 39 | 40 | expect(that.Topsortservice(circularModel)).toBeNull(); 41 | }); 42 | 43 | it('should find circles in graph with source node', function() { 44 | var that = this; 45 | 46 | var circularModel = { 47 | nodes: [ 48 | {id: 1, name: '', x: 0, y: 0, connectors: [{id: 1, type: ''}, {id: 2, type: ''}]}, 49 | {id: 2, name: '', x: 0, y: 0, connectors: [{id: 3, type: ''}]}, 50 | {id: 3, name: '', x: 0, y: 0, connectors: [{id: 4, type: ''}]}, 51 | {id: 4, name: '', x: 0, y: 0, connectors: [{id: 5, type: ''}]}, 52 | {id: 5, name: '', x: 0, y: 0, connectors: [{id: 6, type: ''}]}, 53 | {id: 6, name: '', x: 0, y: 0, connectors: [{id: 7, type: ''}]} 54 | ], 55 | edges: [ 56 | {source: 1, destination: 3}, 57 | {source: 3, destination: 4}, 58 | {source: 4, destination: 1}, 59 | {source: 5, destination: 1}, 60 | {source: 6, destination: 5}, 61 | {source: 7, destination: 1} 62 | ] 63 | }; 64 | 65 | expect(that.Topsortservice(circularModel)).toBe(null); 66 | }); 67 | 68 | it('should work on empty graphs', function() { 69 | expect(this.Topsortservice({nodes: [], edges: []})).toEqual([]); 70 | }); 71 | 72 | it('should work on unconnected graphs', function() { 73 | var noEdgesModel = { 74 | nodes: [{id: 1, name: '', x: 0, y: 0, connectors: [{id: 1, type: ''}]}, 75 | {id: 2, name: '', x: 0, y: 0, connectors: [{id: 2, type: ''}]} 76 | ], 77 | edges: [] 78 | }; 79 | var orderedNodes = this.Topsortservice(noEdgesModel); 80 | expect(orderedNodes).toContain('1'); 81 | expect(orderedNodes).toContain('2'); 82 | }); 83 | 84 | it('should sort easy graphes correct', function() { 85 | var noEdgesModel = { 86 | nodes: [{id: 1, name: '', x: 0, y: 0, connectors: [{id: 1, type: ''}]}, 87 | {id: 2, name: '', x: 0, y: 0, connectors: [{id: 2, type: ''}]} 88 | ], 89 | edges: [{source: 1, destination: 2}] 90 | }; 91 | var orderedNodes = this.Topsortservice(noEdgesModel); 92 | expect(orderedNodes).toEqual(['1', '2']); 93 | }); 94 | 95 | }); 96 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngFlowchart", 3 | "version": "0.5.1", 4 | "homepage": "https://github.com/ONE-LOGIC/ngFlowchart", 5 | "authors": [ 6 | "per.fuchs@onelogic.de" 7 | ], 8 | "description": "Customizable drag & drop flowchart directive for AngularJS", 9 | "main": [ 10 | "dist/ngFlowchart.js", 11 | "app/flowchart/flowchart.css" 12 | ], 13 | "keywords": [ 14 | "angular", 15 | "angularjs", 16 | "flowchart", 17 | "graph" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "app/bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "dependencies": { 29 | "angular": "1.4.7", 30 | "bind-polyfill": "1.0.0" 31 | }, 32 | "devDependencies": { 33 | "angular-mocks": "1.4.7" 34 | }, 35 | "resolutions": { 36 | "angular": "1.4.7" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dist/app.js: -------------------------------------------------------------------------------- 1 | angular.module('app', ['flowchart']) 2 | .factory('prompt', function () { 3 | return prompt; 4 | }) 5 | .config(function (NodeTemplatePathProvider) { 6 | NodeTemplatePathProvider.setTemplatePath("flowchart/node.html"); 7 | }) 8 | 9 | .controller('AppCtrl', function AppCtrl($scope, prompt, Modelfactory, flowchartConstants) { 10 | 11 | var deleteKeyCode = 46; 12 | var ctrlKeyCode = 17; 13 | var aKeyCode = 65; 14 | var escKeyCode = 27; 15 | var nextNodeID = 10; 16 | var nextConnectorID = 20; 17 | var ctrlDown = false; 18 | 19 | var model = { 20 | nodes: [ 21 | { 22 | name: "ngFlowchart", 23 | id: 2, 24 | x: 400, 25 | y: 100, 26 | color: '#000', 27 | borderColor: '#000', 28 | connectors: [ 29 | { 30 | type: flowchartConstants.bottomConnectorType, 31 | id: 9 32 | }, 33 | { 34 | type: flowchartConstants.bottomConnectorType, 35 | id: 10 36 | } 37 | ] 38 | }, 39 | { 40 | name: "Implemented with AngularJS", 41 | id: 3, 42 | x: 400, 43 | y: 300, 44 | color: '#F15B26', 45 | connectors: [ 46 | { 47 | type: flowchartConstants.topConnectorType, 48 | id: 1 49 | }, 50 | { 51 | type: flowchartConstants.topConnectorType, 52 | id: 2 53 | }, 54 | { 55 | type: flowchartConstants.topConnectorType, 56 | id: 3 57 | }, 58 | { 59 | type: flowchartConstants.bottomConnectorType, 60 | id: 4 61 | }, 62 | { 63 | type: flowchartConstants.bottomConnectorType, 64 | id: 5 65 | }, 66 | { 67 | type: flowchartConstants.bottomConnectorType, 68 | id: 12 69 | } 70 | ] 71 | }, 72 | { 73 | name: "Easy Integration", 74 | id: 4, 75 | x: 200, 76 | y: 600, 77 | color: '#000', 78 | borderColor: '#000', 79 | connectors: [ 80 | { 81 | type: flowchartConstants.topConnectorType, 82 | id: 13 83 | }, 84 | { 85 | type: flowchartConstants.topConnectorType, 86 | id: 14 87 | }, 88 | { 89 | type: flowchartConstants.topConnectorType, 90 | id: 15 91 | } 92 | ] 93 | }, 94 | { 95 | name: "Customizable templates", 96 | id: 5, 97 | x: 600, 98 | y: 600, 99 | color: '#000', 100 | borderColor: '#000', 101 | connectors: [ 102 | { 103 | type: flowchartConstants.topConnectorType, 104 | id: 16 105 | }, 106 | { 107 | type: flowchartConstants.topConnectorType, 108 | id: 17 109 | }, 110 | { 111 | type: flowchartConstants.topConnectorType, 112 | id: 18 113 | } 114 | ] 115 | } 116 | ], 117 | edges: [ 118 | { 119 | source: 10, 120 | destination: 1 121 | }, 122 | { 123 | source: 5, 124 | destination: 14 125 | }, 126 | { 127 | source: 5, 128 | destination: 17 129 | } 130 | ] 131 | }; 132 | 133 | $scope.flowchartselected = []; 134 | var modelservice = Modelfactory(model, $scope.flowchartselected); 135 | 136 | $scope.model = model; 137 | $scope.modelservice = modelservice; 138 | 139 | $scope.keyDown = function (evt) { 140 | if (evt.keyCode === ctrlKeyCode) { 141 | ctrlDown = true; 142 | evt.stopPropagation(); 143 | evt.preventDefault(); 144 | } 145 | }; 146 | 147 | $scope.keyUp = function (evt) { 148 | 149 | if (evt.keyCode === deleteKeyCode) { 150 | modelservice.deleteSelected(); 151 | } 152 | 153 | if (evt.keyCode == aKeyCode && ctrlDown) { 154 | modelservice.selectAll(); 155 | } 156 | 157 | if (evt.keyCode == escKeyCode) { 158 | modelservice.deselectAll(); 159 | } 160 | 161 | if (evt.keyCode === ctrlKeyCode) { 162 | ctrlDown = false; 163 | evt.stopPropagation(); 164 | evt.preventDefault(); 165 | } 166 | }; 167 | 168 | $scope.addNewNode = function () { 169 | var nodeName = prompt("Enter a node name:", "New node"); 170 | if (!nodeName) { 171 | return; 172 | } 173 | 174 | var newNode = { 175 | name: nodeName, 176 | id: nextNodeID++, 177 | x: 200, 178 | y: 100, 179 | color: '#F15B26', 180 | connectors: [ 181 | { 182 | id: nextConnectorID++, 183 | type: flowchartConstants.topConnectorType 184 | }, 185 | { 186 | id: nextConnectorID++, 187 | type: flowchartConstants.topConnectorType 188 | }, 189 | { 190 | id: nextConnectorID++, 191 | type: flowchartConstants.bottomConnectorType 192 | }, 193 | { 194 | id: nextConnectorID++, 195 | type: flowchartConstants.bottomConnectorType 196 | } 197 | ] 198 | }; 199 | 200 | model.nodes.push(newNode); 201 | }; 202 | 203 | $scope.activateWorkflow = function() { 204 | angular.forEach($scope.model.edges, function(edge) { 205 | edge.active = !edge.active; 206 | }); 207 | }; 208 | 209 | $scope.addNewInputConnector = function () { 210 | var connectorName = prompt("Enter a connector name:", "New connector"); 211 | if (!connectorName) { 212 | return; 213 | } 214 | 215 | var selectedNodes = modelservice.nodes.getSelectedNodes($scope.model); 216 | for (var i = 0; i < selectedNodes.length; ++i) { 217 | var node = selectedNodes[i]; 218 | node.connectors.push({id: nextConnectorID++, type: flowchartConstants.topConnectorType}); 219 | } 220 | }; 221 | 222 | $scope.addNewOutputConnector = function () { 223 | var connectorName = prompt("Enter a connector name:", "New connector"); 224 | if (!connectorName) { 225 | return; 226 | } 227 | 228 | var selectedNodes = modelservice.nodes.getSelectedNodes($scope.model); 229 | for (var i = 0; i < selectedNodes.length; ++i) { 230 | var node = selectedNodes[i]; 231 | node.connectors.push({id: nextConnectorID++, type: flowchartConstants.bottomConnectorType}); 232 | } 233 | }; 234 | 235 | $scope.deleteSelected = function () { 236 | modelservice.deleteSelected(); 237 | }; 238 | 239 | $scope.callbacks = { 240 | edgeDoubleClick: function () { 241 | console.log('Edge double clicked.'); 242 | }, 243 | edgeMouseOver: function () { 244 | console.log('mouserover') 245 | }, 246 | isValidEdge: function (source, destination) { 247 | return source.type === flowchartConstants.bottomConnectorType && destination.type === flowchartConstants.topConnectorType; 248 | }, 249 | edgeAdded: function (edge) { 250 | console.log("edge added"); 251 | console.log(edge); 252 | }, 253 | nodeRemoved: function (node) { 254 | console.log("node removed"); 255 | console.log(node); 256 | }, 257 | edgeRemoved: function (edge) { 258 | console.log("edge removed"); 259 | console.log(edge); 260 | }, 261 | nodeCallbacks: { 262 | 'doubleClick': function (event) { 263 | console.log('Node was doubleclicked.') 264 | } 265 | } 266 | }; 267 | modelservice.registerCallbacks($scope.callbacks.edgeAdded, $scope.callbacks.nodeRemoved, $scope.callbacks.edgeRemoved); 268 | 269 | }) 270 | ; 271 | -------------------------------------------------------------------------------- /dist/flowchart.css: -------------------------------------------------------------------------------- 1 | .fc-canvas { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .fc-canvas svg { 8 | position: relative; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AngularJS-FlowChart 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 29 |
30 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /dist/onedatastyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | .button-overlay { 6 | position: absolute; 7 | top: 40px; 8 | left: 40px; 9 | z-index: 10; 10 | } 11 | 12 | .button-overlay button { 13 | display: block; 14 | padding: 10px; 15 | margin-bottom: 15px; 16 | border-radius: 10px; 17 | border: none; 18 | box-shadow: none; 19 | color: #fff; 20 | font-size: 20px; 21 | background-color: #F15B26; 22 | } 23 | 24 | .button-overlay button:hover:not(:disabled) { 25 | border: 4px solid #b03911; 26 | border-radius: 5px; 27 | 28 | margin: -4px; 29 | margin-bottom: 11px; 30 | } 31 | 32 | .button-overlay button:disabled { 33 | -webkit-filter: brightness(70%); 34 | filter: brightness(70%); 35 | } 36 | 37 | .fc-node { 38 | z-index: 1; 39 | } 40 | 41 | .innerNode { 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | min-width: 100px; 46 | border-radius: 5px; 47 | 48 | background-color: #F15B26; 49 | color: #fff; 50 | font-size: 20px; 51 | } 52 | 53 | .fc-node.fc-hover { 54 | -webkit-filter: brightness(70%); 55 | filter: brightness(70%);; 56 | } 57 | 58 | .fc-node.fc-selected { 59 | -webkit-filter: brightness(70%); 60 | filter: brightness(70%); 61 | } 62 | 63 | .fc-node.fc-dragging { 64 | z-index: 10; 65 | } 66 | 67 | .fc-node p { 68 | padding: 0 15px; 69 | text-align: center; 70 | } 71 | 72 | .fc-topConnectors, .fc-bottomConnectors { 73 | position: absolute; 74 | left: 0; 75 | width: 100%; 76 | 77 | display: flex; 78 | flex-direction: row; 79 | 80 | z-index: -10; 81 | } 82 | 83 | .fc-topConnectors { 84 | top: -40px; 85 | } 86 | 87 | .fc-bottomConnectors { 88 | bottom: -40px; 89 | } 90 | 91 | .fc-magnet { 92 | display: flex; 93 | flex-grow: 1; 94 | height: 60px; 95 | 96 | justify-content: center; 97 | } 98 | 99 | .fc-topConnectors .fc-magnet { 100 | align-items: flex-end; 101 | } 102 | 103 | .fc-bottomConnectors .fc-magnet { 104 | align-items: flex-start; 105 | } 106 | 107 | .fc-connector { 108 | width: 18px; 109 | height: 18px; 110 | 111 | border: 10px solid transparent; 112 | -moz-background-clip: padding; /* Firefox 3.6 */ 113 | -webkit-background-clip: padding; /* Safari 4? Chrome 6? */ 114 | background-clip: padding-box; 115 | border-radius: 50% 50%; 116 | background-color: #F7A789; 117 | color: #fff; 118 | } 119 | 120 | .fc-connector.fc-hover { 121 | background-color: #000; 122 | } 123 | 124 | .fc-edge { 125 | stroke: gray; 126 | stroke-width: 4; 127 | fill: transparent; 128 | } 129 | 130 | .fc-edge.fc-hover { 131 | stroke: gray; 132 | stroke-width: 6; 133 | fill: transparent; 134 | } 135 | 136 | @keyframes dash { 137 | from { 138 | stroke-dashoffset: 500; 139 | } 140 | } 141 | 142 | .fc-edge.fc-selected { 143 | stroke: red; 144 | stroke-width: 4; 145 | fill: transparent; 146 | } 147 | 148 | .fc-edge.fc-active { 149 | animation: dash 3s linear infinite; 150 | stroke-dasharray: 20; 151 | } 152 | 153 | .fc-edge.fc-dragging { 154 | pointer-events: none; 155 | } 156 | 157 | .edge-endpoint { 158 | fill: gray; 159 | } 160 | 161 | .fc-nodedelete { 162 | display: none; 163 | } 164 | 165 | .fc-selected .fc-nodedelete { 166 | display: block; 167 | position: absolute; 168 | right: -13px; 169 | top: -13px; 170 | border: solid 2px white; 171 | 172 | border-radius: 50%; 173 | font-weight: 600; 174 | font-size: 20px; 175 | 176 | height: 25px; 177 | padding-top: 2px; 178 | width: 27px; 179 | 180 | background: #494949; 181 | color: #fff; 182 | text-align: center; 183 | vertical-align: bottom; 184 | } 185 | 186 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | concat = require('gulp-concat'), 5 | merge2 = require('merge2'), 6 | del = require('del'), 7 | open = require('gulp-open'), 8 | connect = require('gulp-connect'), 9 | ngAnnotate = require('gulp-ng-annotate'), 10 | ngFilesort = require('gulp-angular-filesort'), 11 | ngHtml2Js = require('gulp-ng-html2js'), 12 | bowerFiles = require('main-bower-files'), 13 | karma = require('karma').server; 14 | //postcss = require('gulp-postcss'), 15 | //sourcemaps = require('gulp-sourcemaps'), 16 | //autoprefixer = require('autoprefixer-core'); 17 | 18 | var safeReload = 0; // Semaphore for the reload task, should not run at same time as build tasks. If 0 it is save to run reload. 19 | 20 | var jsFilter = { 21 | filter: /\.js$/i 22 | }; 23 | 24 | gulp.task('vendorScripts', function() { 25 | safeReload++; 26 | var ret = gulp.src(bowerFiles(jsFilter)) 27 | .pipe(concat('vendor.js')) 28 | .pipe(gulp.dest('dist/')); 29 | safeReload--; 30 | return ret; 31 | }); 32 | 33 | gulp.task('flowchartScripts', function() { 34 | safeReload++; 35 | var ret = merge2( 36 | gulp.src(['app/flowchart/*.js', 'app/bower_components/bind-polyfill/index.js', '!app/flowchart/*_test.js']) 37 | .pipe(ngAnnotate()) 38 | .pipe(ngFilesort()), 39 | gulp.src('app/flowchart/*.html') 40 | .pipe(ngHtml2Js({ 41 | moduleName: 'flowchart-templates', 42 | prefix: 'flowchart/' 43 | })) 44 | ) 45 | .pipe(concat('ngFlowchart.js')) 46 | .pipe(gulp.dest('dist')); 47 | safeReload--; 48 | return ret; 49 | }); 50 | 51 | gulp.task('connect', ['build'], function() { 52 | connect.server({ 53 | root: ['dist'], 54 | port: 8000, 55 | livereload: true 56 | }); 57 | }); 58 | 59 | 60 | gulp.task('open', function() { 61 | var options = { 62 | url: 'http://localhost:' + 8000 63 | }; 64 | gulp.src('dist/index.html') 65 | .pipe(open('', options)); 66 | }); 67 | 68 | gulp.task('watch', function() { 69 | gulp.watch('app/flowchart/flowchart.css', ['flowchartCss']); 70 | gulp.watch(['app/flowchart/*.js', '!app/flowchart/*_test.js', 'app/flowchart/*.html'], ['flowchartScripts']); 71 | gulp.watch('dist/**', ['reload']); 72 | }); 73 | 74 | gulp.task('reload', function() { 75 | if (safeReload === 0) { 76 | return gulp.src('dist/**') 77 | .pipe(connect.reload()); 78 | } 79 | }); 80 | 81 | gulp.task('flowchartCss', function() { 82 | gulp.src('app/flowchart/flowchart.css') 83 | .pipe(gulp.dest('dist')); 84 | //gulp.src('dist/onedatastyle.css') 85 | // .pipe(postcss([autoprefixer({ browsers: ['last 2 version'] }) ])) 86 | // .pipe(gulp.dest('dist/compiled/')) 87 | }); 88 | 89 | gulp.task('test', function(done) { 90 | karma.start({ 91 | configFile: __dirname + '/karma.conf.js', 92 | singleRun: true 93 | }, function() { 94 | done(); 95 | }); 96 | }); 97 | 98 | gulp.task('clean', function(done) { 99 | del(['dist/ngFlowchart.js', 'dist/vendor.js', 'dist/flowchart.css'], done); 100 | }); 101 | 102 | gulp.task('build', ['flowchartScripts', 'flowchartCss', 'vendorScripts']); 103 | gulp.task('default', ['connect', 'open', 'watch']); 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": [""], 3 | "disallowKeywords": ["with"], 4 | "disallowKeywordsOnNewLine": ["else"], 5 | "disallowMixedSpacesAndTabs": true, 6 | "disallowMultipleLineStrings": true, 7 | "disallowNewlineBeforeBlockStatements": true, 8 | "disallowSpaceAfterObjectKeys": true, 9 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 10 | "disallowSpaceBeforeBinaryOperators": [","], 11 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 12 | "disallowSpacesInAnonymousFunctionExpression": { 13 | "beforeOpeningRoundBrace": true 14 | }, 15 | "disallowSpacesInCallExpression": true, 16 | "disallowSpacesInFunctionDeclaration": { 17 | "beforeOpeningRoundBrace": true 18 | }, 19 | "disallowSpacesInNamedFunctionExpression": { 20 | "beforeOpeningRoundBrace": true 21 | }, 22 | "disallowSpacesInsideArrayBrackets": true, 23 | "requireSpaceBeforeKeywords": [ 24 | "else", 25 | "while", 26 | "catch" 27 | ], 28 | "disallowSpacesInsideParentheses": true, 29 | "disallowTrailingComma": true, 30 | "disallowTrailingWhitespace": true, 31 | "requireCommaBeforeLineBreak": true, 32 | "requireLineFeedAtFileEnd": true, 33 | "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 34 | "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 35 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 36 | "requireSpaceBeforeBlockStatements": true, 37 | "requireSpacesInConditionalExpression": { 38 | "afterTest": true, 39 | "beforeConsequent": true, 40 | "afterConsequent": true, 41 | "beforeAlternate": true 42 | }, 43 | "requireSpacesInForStatement": true, 44 | "requireSpacesInFunction": { 45 | "beforeOpeningCurlyBrace": true 46 | }, 47 | "validateLineBreaks": "LF" 48 | } 49 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Apr 23 2015 14:37:29 GMT+0200 (Mitteleuropäische Sommerzeit) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: 'app', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['jasmine'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'bower_components/angular/angular.js', 17 | 'bower_components/angular-loader/angular-loader.min.js', 18 | 'bower_components/angular-mocks/angular-mocks.js', 19 | 'bower_components/angular-route/angular-route.min.js', 20 | 'bower_components/bind-polyfill/index.js', 21 | 'flowchart/flowchart.js', 22 | 'flowchart/**/*.js', 23 | 'flowchart/**.html' 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | '**/*.md', 30 | ], 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | '**/*.html': ['ng-html2js'] 36 | }, 37 | 38 | ngHtml2JsPreprocessor: { 39 | // prepend this to the 40 | prependPrefix: '', 41 | // setting this option will create only a single module that contains templates 42 | // from all the files, so you can load them all with module('foo') 43 | moduleName: 'flowchart-templates' 44 | }, 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | reporters: ['progress'], 50 | 51 | 52 | // web server port 53 | port: 9876, 54 | 55 | 56 | // enable / disable colors in the output (reporters and logs) 57 | colors: true, 58 | 59 | 60 | // level of logging 61 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 62 | logLevel: config.LOG_INFO, 63 | 64 | 65 | // enable / disable watching file and executing tests whenever any file changes 66 | autoWatch: false, 67 | 68 | 69 | // start these browsers 70 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 71 | browsers: ['PhantomJS', 72 | 'Firefox', 73 | 'Chrome', 74 | 'IE'], 75 | 76 | 77 | // Continuous Integration mode 78 | // if true, Karma captures browsers, runs the tests and exits 79 | singleRun: false 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /liveDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaHaiz/ngFlowchart/cf65822ce379dc889b67261bca889884075165a2/liveDemo.gif -------------------------------------------------------------------------------- /ngFlowchartDependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaHaiz/ngFlowchart/cf65822ce379dc889b67261bca889884075165a2/ngFlowchartDependency.png -------------------------------------------------------------------------------- /ngFlowchartDependencyGraph.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaHaiz/ngFlowchart/cf65822ce379dc889b67261bca889884075165a2/ngFlowchartDependencyGraph.dia -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngFlowchart", 3 | "version": "0.5.1", 4 | "description": "Customizable drag & drop flowchart directive for AngularJS", 5 | "repository": "https://github.com/ONE-LOGIC/ngFlowchart", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "bower": "^1.3.1", 9 | "del": "^1.1.1", 10 | "gulp": "^3.8.11", 11 | "gulp-angular-filesort": "^1.1.1", 12 | "gulp-concat": "^2.5.2", 13 | "gulp-connect": "^2.2.0", 14 | "gulp-ng-annotate": "^0.5.3", 15 | "gulp-ng-html2js": "^0.2.0", 16 | "gulp-open": "^0.3.2", 17 | "jasmine-core": "^2.2.0", 18 | "karma": "^0.12.31", 19 | "karma-chrome-launcher": "^0.1.8", 20 | "karma-firefox-launcher": "^0.1.4", 21 | "karma-ie-launcher": "^0.1.5", 22 | "karma-jasmine": "^0.3.5", 23 | "karma-junit-reporter": "^0.2.2", 24 | "karma-ng-html2js-preprocessor": "^0.1.2", 25 | "karma-phantomjs-launcher": "^0.1.4", 26 | "main-bower-files": "^2.7.0", 27 | "merge2": "^0.3.5", 28 | "wallaby-ng-html2js-preprocessor": "^0.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | 4 | testFramework: 'jasmine@2.2.1', 5 | files: [ 6 | 'app/bower_components/angular/angular.js', 7 | 'app/bower_components/angular-loader/angular-loader.min.js', 8 | 'app/bower_components/angular-mocks/angular-mocks.js', 9 | 'app/bower_components/angular-route/angular-route.min.js', 10 | 'app/bower_components/bind-polyfill/index.js', 11 | 'app/*.js', 12 | 'dist/onedatastyle.css', 13 | 'app/flowchart/**/*.html', 14 | 'app/flowchart/flowchart.js', 15 | 'app/flowchart/**/*.js', 16 | '!app/flowchart/**/*_test.js', 17 | '!app/server.js' 18 | ], 19 | tests: [ 20 | 'app/flowchart/**/*_test.js' 21 | ], 22 | preprocessors: { 23 | 'app/**/*.html': function (file) { 24 | return require('wallaby-ng-html2js-preprocessor').transform(file, { 25 | // strip this from the file path 26 | stripPrefix: 'app/', 27 | //stripSufix: '.ext', 28 | // prepend this to the 29 | //prependPrefix: '/', 30 | 31 | // setting this option will create only a single module that contains templates 32 | // from all the files, so you can load them all with module('foo') 33 | moduleName: 'flowchart-templates' 34 | }) 35 | }, 36 | } 37 | }; 38 | } 39 | ; 40 | --------------------------------------------------------------------------------