Created
September 17, 2016 23:17
-
-
Save rhalff/08b444f7e981d0da70c97c9eab64f338 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <dom-module id="the-graph"> | |
| <link rel="import" type="css" href="../themes/the-graph-dark.css"> | |
| <link rel="import" type="css" href="../themes/the-graph-light.css"> | |
| <template> | |
| <div id="svgcontainer"></div> | |
| </template> | |
| <script type="text/javascript" src="../dist/static/app.bundle.js"></script> | |
| <script> | |
| (function () { | |
| 'use strict'; | |
| Polymer({ | |
| is: 'the-graph', | |
| properties: { | |
| animatedEdges: { | |
| type: Array, | |
| value: function () { | |
| return []; | |
| }, | |
| notify: true, | |
| observer: 'animatedEdgesChanged' | |
| }, | |
| appView: { value: null }, | |
| autolayout: { | |
| type: Boolean, | |
| value: false, | |
| notify: true, | |
| observer: 'autolayoutChanged' | |
| }, | |
| autolayouter: { value: null }, | |
| debounceLibraryRefeshTimer: { value: null }, | |
| displaySelectionGroup: { | |
| type: Boolean, | |
| value: true, | |
| notify: true, | |
| observer: 'displaySelectionGroupChanged' | |
| }, | |
| editable: { | |
| type: Boolean, | |
| value: true | |
| }, | |
| errorNodes: { | |
| type: Object, | |
| value: function () { | |
| return {}; | |
| }, | |
| notify: true, | |
| observer: 'errorNodesChanged' | |
| }, | |
| fire: { observer: 'fireChanged' }, | |
| forceSelection: { | |
| type: Boolean, | |
| value: false, | |
| notify: true, | |
| observer: 'forceSelectionChanged' | |
| }, | |
| getMenuDef: { | |
| notify: true | |
| }, | |
| graph: { | |
| // value: null, | |
| notify: true, | |
| observer: 'graphChanged' | |
| }, | |
| graphView: { value: null }, | |
| grid: { | |
| type: Number, | |
| value: 72 | |
| }, | |
| height: { | |
| type: Number, | |
| value: 600, | |
| notify: true, | |
| observer: 'heightChanged' | |
| }, | |
| library: { | |
| value: null, | |
| // not sure | |
| // notify: true | |
| }, | |
| maxZoom: { | |
| type: Number, | |
| value: 15, | |
| // notify: true | |
| }, | |
| menus: { | |
| // value: null, | |
| notify: true, | |
| observer: 'menusChanged' | |
| }, | |
| minZoom: { | |
| type: Number, | |
| value: 0.15, | |
| // notify: true | |
| }, | |
| offsetX: { | |
| value: null, | |
| // notify: true | |
| }, | |
| offsetY: { | |
| value: null, | |
| // notify: true | |
| }, | |
| pan: { | |
| notify: true, | |
| observer: 'panChanged' | |
| }, | |
| scale: { | |
| type: Number, | |
| value: 1, | |
| notify: true | |
| }, | |
| selectedEdges: { | |
| type: Array, | |
| value: function () { | |
| return []; | |
| }, | |
| notify: true, | |
| observer: 'selectedEdgesChanged' | |
| }, | |
| selectedNodes: { | |
| type: Array, | |
| /* | |
| value: function () { | |
| return []; | |
| }, | |
| */ | |
| notify: true, | |
| observer: 'selectedNodesChanged' | |
| }, | |
| selectedNodesHash: { | |
| type: Object, | |
| value: function () { | |
| return {}; | |
| }, | |
| observer: 'selectedNodesHashChanged' | |
| }, | |
| snap: { | |
| type: Number, | |
| value: 36 | |
| }, | |
| theme: { | |
| type: String, | |
| value: 'dark', | |
| notify: true, | |
| observer: 'themeChanged' | |
| }, | |
| width: { | |
| type: Number, | |
| value: 800, | |
| notify: true, | |
| observer: 'widthChanged' | |
| } | |
| }, | |
| // created: function () { | |
| ready: function () { | |
| this.library = {}; | |
| // Default pan | |
| this.pan = [ | |
| 0, | |
| 0 | |
| ]; | |
| // Initializes the autolayouter | |
| this.autolayouter = klayNoflo.init({ | |
| onSuccess: this.applyAutolayout.bind(this), | |
| workerScript: '../bower_components/klayjs/klay.js' | |
| }); | |
| }, | |
| ready: function () { | |
| this.themeChanged(); | |
| }, | |
| themeChanged: function () { | |
| this.set('$.svgcontainer.className', 'the-graph-' + this.theme); | |
| }, | |
| graphChanged: function (newGraph, oldGraph) { | |
| if (oldGraph && oldGraph.removeListener) { | |
| oldGraph.removeListener('endTransaction', this.fireChanged); | |
| } | |
| // Listen for graph changes | |
| this.graph.on('endTransaction', this.fireChanged.bind(this)); | |
| // Listen for autolayout changes | |
| if (this.autolayout) { | |
| this.graph.on('addNode', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeNode', this.triggerAutolayout.bind(this)); | |
| this.graph.on('addInport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeInport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('addOutport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeOutport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('addEdge', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeEdge', this.triggerAutolayout.bind(this)); | |
| } | |
| if (this.appView) { | |
| // Remove previous instance | |
| React.unmountComponentAtNode(this.$.svgcontainer); | |
| } | |
| // Setup app | |
| this.set('$.svgcontainer.innerHTML', ''); | |
| this.appView = ReactDOM.render(window.TheGraph.App({ | |
| graph: this.graph, | |
| width: this.width, | |
| minZoom: this.minZoom, | |
| maxZoom: this.maxZoom, | |
| height: this.height, | |
| library: this.library, | |
| menus: this.menus, | |
| editable: this.editable, | |
| onEdgeSelection: this.onEdgeSelection.bind(this), | |
| onNodeSelection: this.onNodeSelection.bind(this), | |
| onPanScale: this.onPanScale.bind(this), | |
| getMenuDef: this.getMenuDef, | |
| displaySelectionGroup: this.displaySelectionGroup, | |
| forceSelection: this.forceSelection, | |
| offsetY: this.offsetY, | |
| offsetX: this.offsetX | |
| }), this.$.svgcontainer); | |
| this.graphView = this.appView.refs.graph; | |
| }, | |
| onPanScale: function (x, y, scale) { | |
| this.set('pan' + ('.' + 0), x); | |
| this.set('pan' + ('.' + 1), y); | |
| this.scale = scale; | |
| }, | |
| onEdgeSelection: function (itemKey, item, toggle) { | |
| if (itemKey === undefined) { | |
| if (this.selectedEdges.length > 0) { | |
| this.selectedEdges = []; | |
| } | |
| this.fire('edges', this.selectedEdges); | |
| return; | |
| } | |
| if (toggle) { | |
| var index = this.selectedEdges.indexOf(item); | |
| var isSelected = index !== -1; | |
| var shallowClone = this.selectedEdges.slice(); | |
| if (isSelected) { | |
| shallowClone.splice(index, 1); | |
| this.selectedEdges = shallowClone; | |
| } else { | |
| shallowClone.push(item); | |
| this.selectedEdges = shallowClone; | |
| } | |
| } else { | |
| this.selectedEdges = [item]; | |
| } | |
| this.fire('edges', this.selectedEdges); | |
| }, | |
| onNodeSelection: function (itemKey, item, toggle) { | |
| if (itemKey === undefined) { | |
| this.selectedNodes = []; | |
| } else if (toggle) { | |
| var index = this.selectedNodes.indexOf(item); | |
| var isSelected = index !== -1; | |
| if (isSelected) { | |
| this.selectedNodes.splice(index, 1); | |
| } else { | |
| this.selectedNodes.push(item); | |
| } | |
| } else { | |
| this.selectedNodes = [item]; | |
| } | |
| this.fire('nodes', this.selectedNodes); | |
| }, | |
| selectedNodesChanged: function () { | |
| var selectedNodesHash = {}; | |
| for (var i = 0, len = this.selectedNodes.length; i < len; i++) { | |
| selectedNodesHash[this.selectedNodes[i].id] = true; | |
| } | |
| this.selectedNodesHash = selectedNodesHash; | |
| this.fire('nodes', this.selectedNodes); | |
| }, | |
| selectedNodesHashChanged: function () { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.setSelectedNodes(this.selectedNodesHash); | |
| }, | |
| errorNodesChanged: function () { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.setErrorNodes(this.errorNodes); | |
| }, | |
| selectedEdgesChanged: function () { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.setSelectedEdges(this.selectedEdges); | |
| this.fire('edges', this.selectedEdges); | |
| }, | |
| animatedEdgesChanged: function () { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.setAnimatedEdges(this.animatedEdges); | |
| }, | |
| fireChanged: function (_, event) { | |
| this.fire('changed', this); | |
| }, | |
| autolayoutChanged: function () { | |
| if (!this.graph) { | |
| return; | |
| } | |
| // Only listen to changes that affect layout | |
| if (this.autolayout) { | |
| this.graph.on('addNode', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeNode', this.triggerAutolayout.bind(this)); | |
| this.graph.on('addInport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeInport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('addOutport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeOutport', this.triggerAutolayout.bind(this)); | |
| this.graph.on('addEdge', this.triggerAutolayout.bind(this)); | |
| this.graph.on('removeEdge', this.triggerAutolayout.bind(this)); | |
| } else { | |
| this.graph.removeListener('addNode', this.triggerAutolayout); | |
| this.graph.removeListener('removeNode', this.triggerAutolayout); | |
| this.graph.removeListener('addInport', this.triggerAutolayout); | |
| this.graph.removeListener('removeInport', this.triggerAutolayout); | |
| this.graph.removeListener('addOutport', this.triggerAutolayout); | |
| this.graph.removeListener('removeOutport', this.triggerAutolayout); | |
| this.graph.removeListener('addEdge', this.triggerAutolayout); | |
| this.graph.removeListener('removeEdge', this.triggerAutolayout); | |
| } | |
| }, | |
| triggerAutolayout: function (event) { | |
| var graph = this.graph; | |
| var portInfo = this.graphView ? this.graphView.portInfo : null; | |
| // Calls the autolayouter | |
| this.autolayouter.layout({ | |
| 'graph': graph, | |
| 'portInfo': portInfo, | |
| 'direction': 'RIGHT', | |
| 'options': { | |
| 'intCoordinates': true, | |
| 'algorithm': 'de.cau.cs.kieler.klay.layered', | |
| 'layoutHierarchy': true, | |
| 'spacing': 36, | |
| 'borderSpacing': 20, | |
| 'edgeSpacingFactor': 0.2, | |
| 'inLayerSpacingFactor': 2, | |
| 'nodePlace': 'BRANDES_KOEPF', | |
| 'nodeLayering': 'NETWORK_SIMPLEX', | |
| 'edgeRouting': 'POLYLINE', | |
| 'crossMin': 'LAYER_SWEEP', | |
| 'direction': 'RIGHT' | |
| } | |
| }); | |
| }, | |
| applyAutolayout: function (layoutedKGraph) { | |
| this.graph.startTransaction('autolayout'); | |
| // Update original graph nodes with the new coordinates from KIELER graph | |
| var children = layoutedKGraph.children.slice(); | |
| var i, len; | |
| for (i = 0, len = children.length; i < len; i++) { | |
| var klayNode = children[i]; | |
| var nofloNode = this.graph.getNode(klayNode.id); | |
| // Encode nodes inside groups | |
| if (klayNode.children) { | |
| var klayChildren = klayNode.children; | |
| var idx; | |
| for (idx in klayChildren) { | |
| var klayChild = klayChildren[idx]; | |
| if (klayChild.id) { | |
| this.graph.setNodeMetadata(klayChild.id, { | |
| x: Math.round((klayNode.x + klayChild.x) / this.snap) * this.snap, | |
| y: Math.round((klayNode.y + klayChild.y) / this.snap) * this.snap | |
| }); | |
| } | |
| } | |
| } | |
| // Encode nodes outside groups | |
| if (nofloNode) { | |
| this.graph.setNodeMetadata(klayNode.id, { | |
| x: Math.round(klayNode.x / this.snap) * this.snap, | |
| y: Math.round(klayNode.y / this.snap) * this.snap | |
| }); | |
| } else { | |
| // Find inport or outport | |
| var idSplit = klayNode.id.split(':::'); | |
| var expDirection = idSplit[0]; | |
| var expKey = idSplit[1]; | |
| if (expDirection === 'inport' && this.graph.inports[expKey]) { | |
| this.graph.setInportMetadata(expKey, { | |
| x: Math.round(klayNode.x / this.snap) * this.snap, | |
| y: Math.round(klayNode.y / this.snap) * this.snap | |
| }); | |
| } else if (expDirection === 'outport' && this.graph.outports[expKey]) { | |
| this.graph.setOutportMetadata(expKey, { | |
| x: Math.round(klayNode.x / this.snap) * this.snap, | |
| y: Math.round(klayNode.y / this.snap) * this.snap | |
| }); | |
| } | |
| } | |
| } | |
| this.graph.endTransaction('autolayout'); | |
| // Fit to window | |
| this.triggerFit(); | |
| }, | |
| triggerFit: function () { | |
| if (this.appView) { | |
| this.appView.triggerFit(); | |
| } | |
| }, | |
| widthChanged: function () { | |
| if (!this.appView) { | |
| return; | |
| } | |
| this.appView.setState({ width: this.width }); | |
| }, | |
| heightChanged: function () { | |
| if (!this.appView) { | |
| return; | |
| } | |
| this.appView.setState({ height: this.height }); | |
| }, | |
| updateIcon: function (nodeId, icon) { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.updateIcon(nodeId, icon); | |
| }, | |
| rerender: function (options) { | |
| // This is throttled with rAF internally | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.markDirty(options); | |
| }, | |
| addNode: function (id, component, metadata) { | |
| if (!this.graph) { | |
| return; | |
| } | |
| this.graph.addNode(id, component, metadata); | |
| }, | |
| getPan: function () { | |
| if (!this.appView) { | |
| return [ | |
| 0, | |
| 0 | |
| ]; | |
| } | |
| return [ | |
| this.appView.state.x, | |
| this.appView.state.y | |
| ]; | |
| }, | |
| panChanged: function () { | |
| // Send pan back to React | |
| if (!this.appView) { | |
| return; | |
| } | |
| this.appView.setState({ | |
| x: this.pan[0], | |
| y: this.pan[1] | |
| }); | |
| }, | |
| getScale: function () { | |
| if (!this.appView) { | |
| return 1; | |
| } | |
| return this.appView.state.scale; | |
| }, | |
| displaySelectionGroupChanged: function () { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.setState({ displaySelectionGroup: this.displaySelectionGroup }); | |
| }, | |
| forceSelectionChanged: function () { | |
| if (!this.graphView) { | |
| return; | |
| } | |
| this.graphView.setState({ forceSelection: this.forceSelection }); | |
| }, | |
| focusNode: function (node) { | |
| this.appView.focusNode(node); | |
| }, | |
| menusChanged: function () { | |
| // Only if the object itself changes, | |
| // otherwise builds menu from reference every time menu shown | |
| if (!this.appView) { | |
| return; | |
| } | |
| this.appView.setProps({ menus: this.menus }); | |
| }, | |
| debounceLibraryRefesh: function () { | |
| // Breaking the "no debounce" rule, this fixes #76 for subgraphs | |
| if (this.debounceLibraryRefeshTimer) { | |
| clearTimeout(this.debounceLibraryRefeshTimer); | |
| } | |
| this.debounceLibraryRefeshTimer = setTimeout(function () { | |
| this.rerender({ libraryDirty: true }); | |
| }.bind(this), 200); | |
| }, | |
| mergeComponentDefinition: function (component, definition) { | |
| // In cases where a component / subgraph ports change, | |
| // we don't want the connections hanging in middle of node | |
| // TODO visually indicate that port is a ghost | |
| if (component === definition) { | |
| return definition; | |
| } | |
| var _i, _j, _len, _len1, exists; | |
| var cInports = component.inports; | |
| var dInports = definition.inports; | |
| if (cInports !== dInports) { | |
| for (_i = 0, _len = cInports.length; _i < _len; _i++) { | |
| var cInport = cInports[_i]; | |
| exists = false; | |
| for (_j = 0, _len1 = dInports.length; _j < _len1; _j++) { | |
| var dInport = dInports[_j]; | |
| if (cInport.name === dInport.name) { | |
| exists = true; | |
| } | |
| } | |
| if (!exists) { | |
| dInports.push(cInport); | |
| } | |
| } | |
| } | |
| var cOutports = component.outports; | |
| var dOutports = definition.outports; | |
| if (cOutports !== dOutports) { | |
| for (_i = 0, _len = cOutports.length; _i < _len; _i++) { | |
| var cOutport = cOutports[_i]; | |
| exists = false; | |
| for (_j = 0, _len1 = dOutports.length; _j < _len1; _j++) { | |
| var dOutport = dOutports[_j]; | |
| if (cOutport.name === dOutport.name) { | |
| exists = true; | |
| } | |
| } | |
| if (!exists) { | |
| dOutports.push(cOutport); | |
| } | |
| } | |
| } | |
| if (definition.icon !== 'cog') { | |
| // Use the latest icon given | |
| component.icon = definition.icon; | |
| } else { | |
| // we should use the icon from the library | |
| definition.icon = component.icon; | |
| } | |
| // a component could also define a svg icon | |
| definition.iconsvg = component.iconsvg; | |
| return definition; | |
| }, | |
| registerComponent: function (definition, generated) { | |
| var component = this.library[definition.name]; | |
| var def = definition; | |
| if (component) { | |
| if (generated) { | |
| // Don't override real one with generated dummy | |
| return; | |
| } | |
| def = this.mergeComponentDefinition(component, definition); | |
| } | |
| this.set('library' + ('.' + definition.name), def); | |
| // So changes are rendered | |
| this.debounceLibraryRefesh(); | |
| }, | |
| getComponent: function (name) { | |
| return this.library[name]; | |
| }, | |
| toJSON: function () { | |
| if (!this.graph) { | |
| return {}; | |
| } | |
| return this.graph.toJSON(); | |
| }, | |
| hostAttributes: { 'touch-action': 'none' } | |
| }); | |
| }()); | |
| </script> | |
| </dom-module> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment