├── pages
├── demo.html
├── index.html
├── demo-snap.html
├── demo-compound.html
├── cytoscape-edgehandles.js
└── demo-compound-snap.html
├── .eslintignore
├── .babelrc
├── .gitignore
├── .npmignore
├── img
└── preview.png
├── src
├── core.js
├── edgehandles
│ ├── enabling.js
│ ├── edge-events-toggle.js
│ ├── draw-mode.js
│ ├── defaults.js
│ ├── cy-listeners.js
│ ├── cy-gestures-toggle.js
│ ├── listeners.js
│ ├── index.js
│ ├── drawing.js
│ └── gesture-lifecycle.js
├── assign.js
└── index.js
├── .eslintrc
├── bower.json
├── .github
└── stale.yml
├── LICENSE
├── webpack.config.js
├── package.json
├── demo-snap.html
├── README.md
├── demo.html
├── cytoscape-edgehandles.js
├── demo-compound.html
└── demo-compound-snap.html
/pages/demo.html:
--------------------------------------------------------------------------------
1 | ../demo.html
--------------------------------------------------------------------------------
/pages/index.html:
--------------------------------------------------------------------------------
1 | ../demo.html
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 |
--------------------------------------------------------------------------------
/pages/demo-snap.html:
--------------------------------------------------------------------------------
1 | ../demo-snap.html
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/pages/demo-compound.html:
--------------------------------------------------------------------------------
1 | ../demo-compound.html
--------------------------------------------------------------------------------
/pages/cytoscape-edgehandles.js:
--------------------------------------------------------------------------------
1 | ../cytoscape-edgehandles.js
--------------------------------------------------------------------------------
/pages/demo-compound-snap.html:
--------------------------------------------------------------------------------
1 | ../demo-compound-snap.html
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/img/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cytoscape/cytoscape.js-edgehandles/HEAD/img/preview.png
--------------------------------------------------------------------------------
/src/core.js:
--------------------------------------------------------------------------------
1 | const Edgehandles = require('./edgehandles');
2 | const assign = require('./assign');
3 |
4 | module.exports = function( options ){
5 | let cy = this;
6 |
7 | return new Edgehandles( assign({ cy }, options) );
8 | };
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "node": true,
6 | "amd": true,
7 | "es6": true
8 | },
9 | "extends": "eslint:recommended",
10 | "rules": {
11 | "semi": "error"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/edgehandles/enabling.js:
--------------------------------------------------------------------------------
1 | function enable(){
2 | this.enabled = true;
3 |
4 | this.emit('enable');
5 |
6 | return this;
7 | }
8 |
9 | function disable(){
10 | this.enabled = false;
11 |
12 | this.emit('disable');
13 |
14 | return this;
15 | }
16 |
17 | module.exports = { enable, disable };
18 |
--------------------------------------------------------------------------------
/src/assign.js:
--------------------------------------------------------------------------------
1 | // Simple, internal Object.assign() polyfill for options objects etc.
2 |
3 | module.exports = Object.assign != null ? Object.assign.bind( Object ) : function( tgt, ...srcs ){
4 | srcs.filter( src => src != null ).forEach( src => {
5 | Object.keys( src ).forEach( k => tgt[k] = src[k] );
6 | } );
7 |
8 | return tgt;
9 | };
10 |
--------------------------------------------------------------------------------
/src/edgehandles/edge-events-toggle.js:
--------------------------------------------------------------------------------
1 | function disableEdgeEvents(){
2 | if( this.options.noEdgeEventsInDraw ){
3 | this.cy.edges().style('events', 'no');
4 | }
5 |
6 | return this;
7 | }
8 |
9 | function enableEdgeEvents(){
10 | if( this.options.noEdgeEventsInDraw ){
11 | this.cy.edges().style('events', '');
12 | }
13 |
14 | return this;
15 | }
16 |
17 | module.exports = { disableEdgeEvents, enableEdgeEvents };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const impl = require('./core');
2 |
3 | // registers the extension on a cytoscape lib ref
4 | let register = function( cytoscape ){
5 | if( !cytoscape ){ return; } // can't register if cytoscape unspecified
6 |
7 | cytoscape( 'core', 'edgehandles', impl ); // register with cytoscape.js
8 | };
9 |
10 | if( typeof cytoscape !== 'undefined' ){ // expose to global cytoscape (i.e. window.cytoscape)
11 | register( cytoscape ); // eslint-disable-line no-undef
12 | }
13 |
14 | module.exports = register;
15 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cytoscape-edgehandles",
3 | "description": "Edge creation UI extension for Cytoscape",
4 | "main": "cytoscape-edgehandles.js",
5 | "dependencies": {
6 | "cytoscape": "^3.2.0"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/cytoscape/cytoscape.js-edgehandles.git"
11 | },
12 | "ignore": [
13 | "**/.*",
14 | "node_modules",
15 | "bower_components",
16 | "test",
17 | "tests"
18 | ],
19 | "keywords": [
20 | "cytoscape",
21 | "cytoscape-extension"
22 | ],
23 | "license": "MIT"
24 | }
25 |
--------------------------------------------------------------------------------
/src/edgehandles/draw-mode.js:
--------------------------------------------------------------------------------
1 | function toggleDrawMode( bool ){
2 | let { cy } = this;
3 |
4 | this.drawMode = bool != null ? bool : !this.drawMode;
5 |
6 | if( this.drawMode ){
7 | this.prevUngrabifyState = cy.autoungrabify();
8 |
9 | cy.autoungrabify( true );
10 |
11 | this.emit('drawon');
12 | } else {
13 | cy.autoungrabify( this.prevUngrabifyState );
14 |
15 | this.emit('drawoff');
16 | }
17 |
18 | return this;
19 | }
20 |
21 | function enableDrawMode(){
22 | return this.toggleDrawMode( true );
23 | }
24 |
25 | function disableDrawMode(){
26 | return this.toggleDrawMode( false );
27 | }
28 |
29 | module.exports = { toggleDrawMode, enableDrawMode, disableDrawMode };
30 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 14
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | # Label to use when marking an issue as stale
9 | staleLabel: stale
10 | # Comment to post when marking an issue as stale. Set to `false` to disable
11 | markComment: >
12 | This issue has been automatically marked as stale, because it has not had
13 | activity within the past 30 days. It will be closed if no further activity
14 | occurs within the next 30 days. If a feature request is important to you,
15 | please consider making a pull request. Thank you for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 |
3 | Copyright (c) 2016-2019, 2021, The Cytoscape Consortium.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the “Software”), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 | of the Software, and to permit persons to whom the Software is furnished to do
10 | 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.
--------------------------------------------------------------------------------
/src/edgehandles/defaults.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | let defaults = {
3 | canConnect: function( sourceNode, targetNode ){
4 | // whether an edge can be created between source and target
5 | return !sourceNode.same(targetNode); // e.g. disallow loops
6 | },
7 | edgeParams: function( sourceNode, targetNode ){
8 | // for edges between the specified source and target
9 | // return element object to be passed to cy.add() for edge
10 | return {};
11 | },
12 | hoverDelay: 150, // time spent hovering over a target node before it is considered selected
13 | snap: true, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs)
14 | snapThreshold: 50, // the target node must be less than or equal to this many pixels away from the cursor/finger
15 | snapFrequency: 15, // the number of times per second (Hz) that snap checks done (lower is less expensive)
16 | noEdgeEventsInDraw: true, // set events:no to edges during draws, prevents mouseouts on compounds
17 | disableBrowserGestures: true // during an edge drawing gesture, disable browser gestures such as two-finger trackpad swipe and pinch-to-zoom
18 | };
19 | /* eslint-enable */
20 |
21 | module.exports = defaults;
22 |
--------------------------------------------------------------------------------
/src/edgehandles/cy-listeners.js:
--------------------------------------------------------------------------------
1 | function addCytoscapeListeners(){
2 | let { cy, options } = this;
3 |
4 | // grabbing nodes
5 | this.addListener( cy, 'drag', () => this.grabbingNode = true );
6 | this.addListener( cy, 'free', () => this.grabbingNode = false );
7 |
8 | // start on tapstart handle
9 | // start on tapstart node (draw mode)
10 | // toggle on source node
11 | this.addListener( cy, 'tapstart', 'node', e => {
12 | let node = e.target;
13 |
14 | if( this.drawMode ){
15 | this.start( node );
16 | }
17 | } );
18 |
19 | // update line on drag
20 | this.addListener( cy, 'tapdrag', e => {
21 | this.update( e.position );
22 | } );
23 |
24 | // hover over preview
25 | this.addListener( cy, 'tapdragover', 'node', e => {
26 | if( options.snap ){
27 | // then ignore events like mouseover
28 | } else {
29 | this.preview( e.target );
30 | }
31 | } );
32 |
33 | // hover out unpreview
34 | this.addListener( cy, 'tapdragout', 'node', e => {
35 | if( options.snap ){
36 | // then keep the preview
37 | } else {
38 | this.unpreview( e.target );
39 | }
40 | } );
41 |
42 | // stop gesture on tapend
43 | this.addListener( cy, 'tapend', () => {
44 | this.stop();
45 | } );
46 |
47 | return this;
48 | }
49 |
50 | module.exports = { addCytoscapeListeners };
51 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pkg = require('./package.json');
3 | const camelcase = require('camelcase');
4 | const process = require('process');
5 | const webpack = require('webpack');
6 | const env = process.env;
7 | const NODE_ENV = env.NODE_ENV;
8 | const MIN = env.MIN;
9 | const PROD = NODE_ENV === 'production';
10 |
11 | let config = {
12 | devtool: PROD ? false : 'inline-source-map',
13 | entry: './src/index.js',
14 | output: {
15 | path: path.join( __dirname ),
16 | filename: pkg.name + '.js',
17 | library: camelcase( pkg.name ),
18 | libraryTarget: 'umd'
19 | },
20 | module: {
21 | rules: [
22 | { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' }
23 | ]
24 | },
25 | externals: {
26 | 'lodash.memoize': {
27 | commonjs: 'lodash.memoize',
28 | commonjs2: 'lodash.memoize',
29 | amd: 'lodash.memoize',
30 | root: ['_', 'memoize']
31 | },
32 | 'lodash.throttle': {
33 | commonjs: 'lodash.throttle',
34 | commonjs2: 'lodash.throttle',
35 | amd: 'lodash.throttle',
36 | root: ['_', 'throttle']
37 | }
38 | },
39 | plugins: MIN ? [
40 | new webpack.optimize.UglifyJsPlugin({
41 | compress: {
42 | warnings: false,
43 | drop_console: false,
44 | }
45 | })
46 | ] : []
47 | };
48 |
49 | module.exports = config;
50 |
--------------------------------------------------------------------------------
/src/edgehandles/cy-gestures-toggle.js:
--------------------------------------------------------------------------------
1 | function disableGestures(){
2 | this.saveGestureState();
3 |
4 | ( this.cy
5 | .zoomingEnabled( false )
6 | .panningEnabled( false )
7 | .boxSelectionEnabled( false )
8 | );
9 |
10 | if( this.options.disableBrowserGestures ){
11 | let wlOpts = this.windowListenerOptions;
12 |
13 | window.addEventListener('touchstart', this.preventDefault, wlOpts);
14 | window.addEventListener('touchmove', this.preventDefault, wlOpts);
15 | window.addEventListener('wheel', this.preventDefault, wlOpts);
16 | }
17 |
18 | return this;
19 | }
20 |
21 | function resetGestures(){
22 | ( this.cy
23 | .zoomingEnabled( this.lastZoomingEnabled )
24 | .panningEnabled( this.lastPanningEnabled )
25 | .boxSelectionEnabled( this.lastBoxSelectionEnabled )
26 | );
27 |
28 | if( this.options.disableBrowserGestures ){
29 | let wlOpts = this.windowListenerOptions;
30 |
31 | window.removeEventListener('touchstart', this.preventDefault, wlOpts);
32 | window.removeEventListener('touchmove', this.preventDefault, wlOpts);
33 | window.removeEventListener('wheel', this.preventDefault, wlOpts);
34 | }
35 |
36 | return this;
37 | }
38 |
39 | function saveGestureState(){
40 | let { cy } = this;
41 |
42 | this.lastPanningEnabled = cy.panningEnabled();
43 | this.lastZoomingEnabled = cy.zoomingEnabled();
44 | this.lastBoxSelectionEnabled = cy.boxSelectionEnabled();
45 |
46 | return this;
47 | }
48 |
49 | module.exports = { disableGestures, resetGestures, saveGestureState };
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cytoscape-edgehandles",
3 | "version": "4.0.1",
4 | "description": "Edge creation UI extension for Cytoscape",
5 | "main": "cytoscape-edgehandles.js",
6 | "author": {
7 | "name": "Max Franz",
8 | "email": "maxkfranz@gmail.com"
9 | },
10 | "scripts": {
11 | "postpublish": "run-s gh-pages",
12 | "gh-pages": "gh-pages -d pages",
13 | "copyright": "update license",
14 | "lint": "eslint src",
15 | "build": "cross-env NODE_ENV=production webpack",
16 | "build:min": "cross-env NODE_ENV=production MIN=true webpack",
17 | "build:release": "run-s build copyright",
18 | "watch": "webpack --progress --watch",
19 | "dev": "webpack-dev-server --open",
20 | "test": "mocha"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/cytoscape/cytoscape.js-edgehandles.git"
25 | },
26 | "keywords": [
27 | "cytoscape",
28 | "cytoscape-extension"
29 | ],
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/cytoscape/cytoscape.js-edgehandles/issues"
33 | },
34 | "homepage": "https://github.com/cytoscape/cytoscape.js-edgehandles",
35 | "devDependencies": {
36 | "babel-core": "^6.24.1",
37 | "babel-loader": "^7.0.0",
38 | "babel-preset-env": "^1.5.1",
39 | "camelcase": "^4.1.0",
40 | "chai": "4.0.2",
41 | "cpy-cli": "^1.0.1",
42 | "cross-env": "^5.0.0",
43 | "eslint": "^3.9.1",
44 | "gh-pages": "^1.0.0",
45 | "mocha": "3.4.2",
46 | "npm-run-all": "^4.1.2",
47 | "rimraf": "^2.6.2",
48 | "update": "^0.7.4",
49 | "updater-license": "^1.0.0",
50 | "webpack": "^2.6.1",
51 | "webpack-dev-server": "^2.4.5"
52 | },
53 | "peerDependencies": {
54 | "cytoscape": "^3.2.0"
55 | },
56 | "dependencies": {
57 | "lodash.memoize": "^4.1.2",
58 | "lodash.throttle": "^4.1.1"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/edgehandles/listeners.js:
--------------------------------------------------------------------------------
1 | function addListeners(){
2 | this.addCytoscapeListeners();
3 |
4 | this.addListener( this.cy, 'destroy', () => this.destroy() );
5 |
6 | return this;
7 | }
8 |
9 | function removeListeners(){
10 | for( let i = this.listeners.length - 1; i >= 0; i-- ){
11 | let l = this.listeners[i];
12 |
13 | this.removeListener( l.target, l.event, l.selector, l.callback, l.options );
14 | }
15 |
16 | return this;
17 | }
18 |
19 | function getListener( target, event, selector, callback, options ){
20 | if( typeof selector !== typeof '' ){
21 | callback = selector;
22 | options = callback;
23 | selector = null;
24 | }
25 |
26 | if( options == null ){
27 | options = false;
28 | }
29 |
30 | return { target, event, selector, callback, options };
31 | }
32 |
33 | function isDom( target ){
34 | return target instanceof Element;
35 | }
36 |
37 | function addListener( target, event, selector, callback, options ){
38 | let l = getListener( target, event, selector, callback, options );
39 |
40 | this.listeners.push( l );
41 |
42 | if( isDom( l.target ) ){
43 | l.target.addEventListener( l.event, l.callback, l.options );
44 | } else {
45 | if( l.selector ){
46 | l.target.addListener( l.event, l.selector, l.callback, l.options );
47 | } else {
48 | l.target.addListener( l.event, l.callback, l.options );
49 | }
50 | }
51 |
52 | return this;
53 | }
54 |
55 | function removeListener( target, event, selector, callback, options ){
56 | let l = getListener( target, event, selector, callback, options );
57 |
58 | for( let i = this.listeners.length - 1; i >= 0; i-- ){
59 | let l2 = this.listeners[i];
60 |
61 | if(
62 | l.target === l2.target
63 | && l.event === l2.event
64 | && ( l.selector == null || l.selector === l2.selector )
65 | && ( l.callback == null || l.callback === l2.callback )
66 | ){
67 | this.listeners.splice( i, 1 );
68 |
69 | if( isDom( l.target ) ){
70 | l.target.removeEventListener( l.event, l.callback, l.options );
71 | } else {
72 | if( l.selector ){
73 | l.target.removeListener( l.event, l.selector, l.callback, l.options );
74 | } else {
75 | l.target.removeListener( l.event, l.callback, l.options );
76 | }
77 | }
78 |
79 | break;
80 | }
81 | }
82 |
83 | return this;
84 | }
85 |
86 | function emit( type, position, ...args ){
87 | let { cy } = this;
88 |
89 | cy.emit( { type: `eh${type}`, position }, args );
90 |
91 | return this;
92 | }
93 |
94 | module.exports = { addListener, addListeners, removeListener, removeListeners, emit };
95 |
--------------------------------------------------------------------------------
/src/edgehandles/index.js:
--------------------------------------------------------------------------------
1 | const defaults = require('./defaults');
2 | const assign = require('../assign');
3 | const throttle = require('lodash.throttle');
4 |
5 | const cyGesturesToggle = require('./cy-gestures-toggle');
6 | const cyListeners = require('./cy-listeners');
7 | const drawMode = require('./draw-mode');
8 | const drawing = require('./drawing');
9 | const enabling = require('./enabling');
10 | const gestureLifecycle = require('./gesture-lifecycle');
11 | const listeners = require('./listeners');
12 | const edgeEvents = require('./edge-events-toggle');
13 |
14 | function Edgehandles( options ){
15 | let cy = options.cy;
16 |
17 | this.cy = cy;
18 | this.listeners = [];
19 |
20 | // edgehandles gesture state
21 | this.enabled = true;
22 | this.drawMode = false;
23 | this.active = false;
24 | this.grabbingNode = false;
25 |
26 | // edgehandles elements
27 | this.clearCollections();
28 |
29 | // mouse position
30 | this.mx = 0;
31 | this.my = 0;
32 |
33 | this.options = assign( {}, defaults, options );
34 |
35 | this.saveGestureState();
36 | this.addListeners();
37 |
38 | this.throttledSnap = throttle( this.snap.bind(this), 1000/options.snapFrequency );
39 |
40 | this.preventDefault = e => e.preventDefault();
41 |
42 | // disabled until start()
43 | this.canConnect = () => false;
44 |
45 | let supportsPassive = false;
46 | try {
47 | let opts = Object.defineProperty( {}, 'passive', {
48 | get: function(){
49 | supportsPassive = true;
50 | }
51 | } );
52 |
53 | window.addEventListener( 'test', null, opts );
54 | } catch( err ){
55 | // swallow
56 | }
57 |
58 | if( supportsPassive ){
59 | this.windowListenerOptions = { capture: true, passive: false };
60 | } else {
61 | this.windowListenerOptions = true;
62 | }
63 | }
64 |
65 | let proto = Edgehandles.prototype = {};
66 | let extend = obj => assign( proto, obj );
67 |
68 | proto.destroy = function(){
69 | this.removeListeners();
70 | };
71 |
72 | proto.setOptions = function( options ){
73 | assign( this.options, options );
74 | };
75 |
76 | proto.mp = function(){
77 | return { x: this.mx, y: this.my };
78 | };
79 |
80 | proto.hp = function(){
81 | return { x: this.hx, y: this.hy };
82 | };
83 |
84 | proto.clearCollections = function(){
85 | let { cy } = this;
86 |
87 | this.previewEles = cy.collection();
88 | this.ghostEles = cy.collection();
89 | this.ghostNode = cy.collection();
90 | this.sourceNode = cy.collection();
91 | this.targetNode = cy.collection();
92 | this.presumptiveTargets = cy.collection();
93 | };
94 |
95 | [
96 | cyGesturesToggle,
97 | cyListeners,
98 | drawMode,
99 | drawing,
100 | enabling,
101 | gestureLifecycle,
102 | listeners,
103 | edgeEvents
104 | ].forEach( extend );
105 |
106 | module.exports = Edgehandles;
107 |
--------------------------------------------------------------------------------
/src/edgehandles/drawing.js:
--------------------------------------------------------------------------------
1 | const assign = require('../assign');
2 | const isString = x => typeof x === typeof '';
3 | const isArray = x => typeof x === typeof [] && x.length != null;
4 |
5 | function getEleJson( overrides, params, addedClasses ){
6 | let json = {};
7 |
8 | // basic values
9 | assign( json, params, overrides );
10 |
11 | // make sure params can specify data but that overrides take precedence
12 | assign( json.data, params.data, overrides.data );
13 |
14 | if( isString(params.classes) ){
15 | json.classes = params.classes + ' ' + addedClasses;
16 | } else if( isArray(params.classes) ){
17 | json.classes = params.classes.join(' ') + ' ' + addedClasses;
18 | } else {
19 | json.classes = addedClasses;
20 | }
21 |
22 | return json;
23 | }
24 |
25 | function makeEdges( preview = false ) {
26 | let { cy, options, presumptiveTargets, previewEles, active } = this;
27 |
28 | let source = this.sourceNode;
29 | let target = this.targetNode;
30 | let classes = preview ? 'eh-preview' : '';
31 | let added = cy.collection();
32 | let canConnect = this.canConnect(target);
33 |
34 | // can't make edges outside of regular gesture lifecycle
35 | if( !active ){ return; }
36 |
37 | // must be able to connect
38 | if( !canConnect ){ return; }
39 |
40 | // detect cancel
41 | if( !target || target.size() === 0 ){
42 | previewEles.remove();
43 |
44 | this.emit( 'cancel', this.mp(), source, presumptiveTargets );
45 |
46 | return;
47 | }
48 |
49 | // just remove preview class if we already have the edges
50 | if( !preview ) {
51 | previewEles.removeClass('eh-preview').style('events', '');
52 |
53 | this.emit( 'complete', this.mp(), source, target, previewEles );
54 |
55 | return;
56 | }
57 |
58 | let source2target = cy.add(
59 | getEleJson(
60 | {
61 | group: 'edges',
62 | data: {
63 | source: source.id(),
64 | target: target.id()
65 | }
66 | },
67 | this.edgeParams( target ),
68 | classes
69 | )
70 | );
71 |
72 | added = added.merge( source2target );
73 |
74 | if( preview ) {
75 | this.previewEles = added;
76 |
77 | added.style('events', 'no');
78 | } else {
79 | added.style('events', '');
80 |
81 | this.emit( 'complete', this.mp(), source, target, added );
82 | }
83 |
84 | return this;
85 | }
86 |
87 | function makePreview() {
88 | this.makeEdges( true );
89 |
90 | return this;
91 | }
92 |
93 | function previewShown(){
94 | return this.previewEles.nonempty() && this.previewEles.inside();
95 | }
96 |
97 | function removePreview() {
98 | if( this.previewShown() ){
99 | this.previewEles.remove();
100 | }
101 |
102 | return this;
103 | }
104 |
105 | function updateEdge() {
106 | let { sourceNode, ghostNode, cy, mx, my } = this;
107 | let x = mx;
108 | let y = my;
109 | let ghostEdge, ghostEles;
110 |
111 | // can't draw a line without having the starting node
112 | if( !sourceNode ){ return; }
113 |
114 | if( !ghostNode || ghostNode.length === 0 || ghostNode.removed() ) {
115 | ghostEles = this.ghostEles = cy.collection();
116 |
117 | cy.batch( () => {
118 | ghostNode = this.ghostNode = cy.add( {
119 | group: 'nodes',
120 | classes: 'eh-ghost eh-ghost-node',
121 | position: {
122 | x: 0,
123 | y: 0
124 | }
125 | } );
126 |
127 | ghostNode.style({
128 | 'background-color': 'blue',
129 | 'width': 0.0001,
130 | 'height': 0.0001,
131 | 'opacity': 0,
132 | 'events': 'no'
133 | });
134 |
135 | let ghostEdgeParams = {};
136 |
137 | ghostEdge = cy.add( assign({}, ghostEdgeParams, {
138 | group: 'edges',
139 | data: assign({}, ghostEdgeParams.data, {
140 | source: sourceNode.id(),
141 | target: ghostNode.id()
142 | }),
143 | classes: 'eh-ghost eh-ghost-edge'
144 | }) );
145 |
146 | ghostEdge.style({
147 | 'events': 'no'
148 | });
149 | } );
150 |
151 | ghostEles.merge( ghostNode ).merge( ghostEdge );
152 | }
153 |
154 | ghostNode.position({ x, y });
155 |
156 | return this;
157 | }
158 |
159 | module.exports = {
160 | makeEdges, makePreview, removePreview, previewShown,
161 | updateEdge
162 | };
163 |
--------------------------------------------------------------------------------
/demo-snap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cytoscape-edgehandles.js snapping demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
47 |
48 |
172 |
173 |
174 |
175 | cytoscape-edgehandles (snapping disabled)
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | cytoscape-edgehandles
2 | ================================================================================
3 |
4 | [](https://zenodo.org/badge/latestdoi/16078488)
5 |
6 | 
7 |
8 | ## Description
9 |
10 |
11 | This extension allows for drawing edges between nodes ([demo](https://cytoscape.github.io/cytoscape.js-edgehandles/), [demo (snapping disabled)](https://cytoscape.github.io/cytoscape.js-edgehandles/demo-snap.html), [compound demo](https://cytoscape.github.io/cytoscape.js-edgehandles/demo-compound.html), [compound demo (snapping disabled)](https://cytoscape.github.io/cytoscape.js-edgehandles/demo-compound-snap.html))
12 |
13 | You can use [Popper](https://github.com/cytoscape/cytoscape.js-popper) to create your own handles, as shown in the [demo](https://cytoscape.github.io/cytoscape.js-edgehandles/).
14 |
15 |
16 | ## Dependencies
17 |
18 | * Cytoscape.js 3.x, >= 3.2.0
19 | * Lodash 4.x, >= 4.1
20 | * memoize
21 | * throttle
22 |
23 |
24 | ## Usage instructions
25 |
26 | Download the library:
27 | * via npm: `npm install cytoscape-edgehandles`,
28 | * via bower: `bower install cytoscape-edgehandles`, or
29 | * via direct download in the repository (probably from a tag).
30 |
31 | Import the library as appropriate for your project:
32 |
33 | ES import:
34 |
35 | ```js
36 | import cytoscape from 'cytoscape';
37 | import edgehandles from 'cytoscape-edgehandles';
38 |
39 | cytoscape.use( edgehandles );
40 | ```
41 |
42 | CommonJS require:
43 |
44 | ```js
45 | let cytoscape = require('cytoscape');
46 | let edgehandles = require('cytoscape-edgehandles');
47 |
48 | cytoscape.use( edgehandles ); // register extension
49 | ```
50 |
51 | AMD:
52 |
53 | ```js
54 | require(['cytoscape', 'cytoscape-edgehandles'], function( cytoscape, edgehandles ){
55 | edgehandles( cytoscape ); // register extension
56 | });
57 | ```
58 |
59 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed.
60 |
61 |
62 |
63 | ## Initialisation
64 |
65 | You initialise the extension on the Cytoscape instance:
66 |
67 | ```js
68 |
69 | let cy = cytoscape({
70 | container: document.getElementById('#cy'),
71 | /* ... */
72 | });
73 |
74 | // the default values of each option are outlined below:
75 | let defaults = {
76 | canConnect: function( sourceNode, targetNode ){
77 | // whether an edge can be created between source and target
78 | return !sourceNode.same(targetNode); // e.g. disallow loops
79 | },
80 | edgeParams: function( sourceNode, targetNode ){
81 | // for edges between the specified source and target
82 | // return element object to be passed to cy.add() for edge
83 | return {};
84 | },
85 | hoverDelay: 150, // time spent hovering over a target node before it is considered selected
86 | snap: true, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs)
87 | snapThreshold: 50, // the target node must be less than or equal to this many pixels away from the cursor/finger
88 | snapFrequency: 15, // the number of times per second (Hz) that snap checks done (lower is less expensive)
89 | noEdgeEventsInDraw: true, // set events:no to edges during draws, prevents mouseouts on compounds
90 | disableBrowserGestures: true // during an edge drawing gesture, disable browser gestures such as two-finger trackpad swipe and pinch-to-zoom
91 | };
92 |
93 | let eh = cy.edgehandles( defaults );
94 |
95 | ```
96 |
97 |
98 | ## API
99 |
100 | The object returned by `cy.edgehandles()` has several functions available on it:
101 |
102 | * `start( sourceNode )` : manually start the gesture (as if the handle were already held)
103 | * `stop()` : manually completes or cancels the gesture
104 | * `disable()` : disables edgehandles behaviour
105 | * `enable()` : enables edgehandles behaviour
106 | * `enableDrawMode()` : turn on draw mode (the entire node body acts like the handle)
107 | * `disableDrawMode()` : turn off draw mode
108 | * `destroy()` : remove edgehandles behaviour
109 |
110 |
111 | ## Classes
112 |
113 | These classes can be used for styling the graph as it interacts with the extension:
114 |
115 | * `eh-source` : The source node
116 | * `eh-target` : A target node
117 | * `eh-preview` : Preview edges (i.e. shown before releasing the mouse button and the edge creation is confirmed)
118 | * `eh-hover` : Added to nodes as they are hovered over as targets
119 | * `eh-ghost-node` : The ghost node (target), used when the cursor isn't pointed at a target node yet (i.e. in place of a target node)
120 | * `eh-ghost-edge` : The ghost handle line edge, used when the cursor isn't pointed at a target node yet (i.e. the edge is pointing to empty space)
121 | * `eh-ghost` : A ghost element
122 | * `eh-presumptive-target` : A node that, during an edge drag, may become a target when released
123 | * `eh-preview-active` : Applied to the source, target, and ghost edge when the preview is active
124 |
125 |
126 | ## Events
127 |
128 | During the course of a user's interaction with the extension, several events are generated and triggered on the core. Each event callback has a number of extra parameters, and certain events have associated positions.
129 |
130 | * `ehstart` : when the edge creation gesture starts
131 | * `(event, sourceNode)`
132 | * `event.position` : handle position
133 | * `ehcomplete` : when the edge is created
134 | * `(event, sourceNode, targetNode, addedEdge)`
135 | * `event.position` : cursor/finger position
136 | * `ehstop` : when the edge creation gesture is stopped (either successfully completed or cancelled)
137 | * `(event, sourceNode)`
138 | * `event.position` : cursor/finger position
139 | * `ehcancel` : when the edge creation gesture is cancelled
140 | * `(event, sourceNode, cancelledTargets)`
141 | * `event.position` : cursor/finger position
142 | * `ehhoverover` : when hovering over a target
143 | * `(event, sourceNode, targetNode)`
144 | * `event.position` : cursor/finger position
145 | * `ehhoverout` : when leaving a target node
146 | * `(event, sourceNode, targetNode)`
147 | * `event.position` : cursor/finger position
148 | * `ehpreviewon` : when a preview is shown
149 | * `(event, sourceNode, targetNode, previewEdge)`
150 | * `event.position` : cursor/finger position
151 | * `ehpreviewoff` : when the preview is removed
152 | * `(event, sourceNode, targetNode, previewEdge)`
153 | * `event.position` : cursor/finger position
154 | * `ehdrawon` : when draw mode is enabled
155 | * `(event)`
156 | * `ehdrawoff` : when draw mode is disabled
157 | * `(event)`
158 |
159 | Example binding:
160 |
161 | ```js
162 | cy.on('ehcomplete', (event, sourceNode, targetNode, addedEdge) => {
163 | let { position } = event;
164 |
165 | // ...
166 | });
167 | ```
168 |
169 | ## Build targets
170 |
171 | * `npm run test` : Run Mocha tests in `./test`
172 | * `npm run build` : Build `./src/**` into `cytoscape-edgehandles.js`
173 | * `npm run watch` : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running)
174 | * `npm run dev` : Automatically build on changes with live reloading with webpack dev server
175 | * `npm run lint` : Run eslint on the source
176 |
177 | N.b. all builds use babel, so modern ES features can be used in the `src`.
178 |
179 |
180 | ## Publishing instructions
181 |
182 | This project is set up to automatically be published to npm and bower. To publish:
183 |
184 | 1. Build the extension : `npm run build:release`
185 | 1. Commit the build : `git commit -am "Build for release"`
186 | 1. Bump the version number and tag: `npm version major|minor|patch`
187 | 1. Push to origin: `git push && git push --tags`
188 | 1. Publish to npm: `npm publish .`
189 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-edgehandles https://github.com/cytoscape/edgehandles.git`a
190 | 1. [Make a new release](https://github.com/cytoscape/cytoscape.js-edgehandles/releases/new) for Zenodo.
191 |
--------------------------------------------------------------------------------
/src/edgehandles/gesture-lifecycle.js:
--------------------------------------------------------------------------------
1 | const memoize = require('lodash.memoize');
2 | const sqrt2 = Math.sqrt(2);
3 |
4 | function canStartOn( node ){
5 | const { previewEles, ghostEles } = this;
6 | const isPreview = el => previewEles.anySame(el);
7 | const isGhost = el => ghostEles.anySame(el);
8 | const isTemp = el => isPreview(el) || isGhost(el);
9 |
10 | const { enabled, active, grabbingNode } = this;
11 |
12 | return (
13 | enabled && !active && !grabbingNode
14 | && node != null && node.nonempty() && !isTemp(node)
15 | );
16 | }
17 |
18 | function canStartDrawModeOn( node ){
19 | return this.canStartOn( node ) && this.drawMode;
20 | }
21 |
22 | function canStartNonDrawModeOn( node ){
23 | return this.canStartOn( node ) && !this.drawMode;
24 | }
25 |
26 | function start( node ){
27 | if( !this.canStartOn(node) ){ return; }
28 |
29 | this.active = true;
30 |
31 | this.sourceNode = node;
32 | this.sourceNode.addClass('eh-source');
33 |
34 | this.disableGestures();
35 | this.disableEdgeEvents();
36 |
37 | const getId = n => n.id();
38 |
39 | this.canConnect = memoize(target => {
40 | return this.options.canConnect(this.sourceNode, target);
41 | }, getId);
42 |
43 | this.edgeParams = memoize(target => {
44 | return this.options.edgeParams(this.sourceNode, target);
45 | }, getId);
46 |
47 | this.emit( 'start', this.hp(), node );
48 | }
49 |
50 | function update( pos ){
51 | if( !this.active ){ return; }
52 |
53 | let p = pos;
54 |
55 | this.mx = p.x;
56 | this.my = p.y;
57 |
58 | this.updateEdge();
59 | this.throttledSnap();
60 |
61 | return this;
62 | }
63 |
64 | function snap(){
65 | if( !this.active || !this.options.snap ){ return false; }
66 |
67 | let cy = this.cy;
68 | let tgt = this.targetNode;
69 | let threshold = this.options.snapThreshold;
70 | let mousePos = this.mp();
71 | let { previewEles, ghostNode } = this;
72 |
73 | let radius = n => sqrt2 * Math.max(n.outerWidth(), n.outerHeight())/2; // worst-case enclosure of bb by circle
74 | let sqDist = (x1, y1, x2, y2) => { let dx = x2 - x1; let dy = y2 - y1; return dx*dx + dy*dy; };
75 | let sqDistByPt = (p1, p2) => sqDist(p1.x, p1.y, p2.x, p2.y);
76 | let nodeSqDist = n => sqDistByPt(n.position(), mousePos);
77 |
78 | let sqThreshold = n => { let r = radius(n); let t = r + threshold; return t * t; };
79 | let isWithinThreshold = n => nodeSqDist(n) <= sqThreshold(n);
80 |
81 | let bbSqDist = n => {
82 | let p = n.position();
83 | let halfW = n.outerWidth() / 2;
84 | let halfH = n.outerHeight() / 2;
85 |
86 | // node and mouse positions, line is formed from node to mouse
87 | let nx = p.x;
88 | let ny = p.y;
89 | let mx = mousePos.x;
90 | let my = mousePos.y;
91 |
92 | // bounding box
93 | let x1 = nx - halfW;
94 | let x2 = nx + halfW;
95 | let y1 = ny - halfH;
96 | let y2 = ny + halfH;
97 |
98 | let insideXBounds = x1 <= mx && mx <= x2;
99 | let insideYBounds = y1 <= my && my <= y2;
100 |
101 | if( insideXBounds && insideYBounds ){ // inside box
102 | return 0;
103 | } else if( insideXBounds ){ // perpendicular distance to box, top or bottom
104 | let dy1 = my - y1;
105 | let dy2 = my - y2;
106 |
107 | return Math.min(dy1 * dy1, dy2 * dy2);
108 | } else if( insideYBounds ){ // perpendicular distance to box, left or right
109 | let dx1 = mx - x1;
110 | let dx2 = mx - x2;
111 |
112 | return Math.min(dx1 * dx1, dx2 * dx2);
113 | } else if( mx < x1 && my < y1 ){ // top-left corner distance
114 | return sqDist(mx, my, x1, y1);
115 | } else if( mx > x2 && my < y1 ){ // top-right corner distance
116 | return sqDist(mx, my, x2, y1);
117 | } else if( mx < x1 && my > y2 ){ // bottom-left corner distance
118 | return sqDist(mx, my, x1, y2);
119 | } else { // bottom-right corner distance
120 | return sqDist(mx, my, x2, y2);
121 | }
122 | };
123 |
124 | let cmpBbSqDist = (n1, n2) => bbSqDist(n1) - bbSqDist(n2);
125 |
126 | let cmp = cmpBbSqDist;
127 |
128 | let allowHoverDelay = false;
129 |
130 | let mouseIsInside = n => {
131 | let mp = mousePos;
132 | let w = n.outerWidth();
133 | let halfW = w/2;
134 | let h = n.outerHeight();
135 | let halfH = h/2;
136 | let p = n.position();
137 | let x1 = p.x - halfW;
138 | let x2 = p.x + halfW;
139 | let y1 = p.y - halfH;
140 | let y2 = p.y + halfH;
141 |
142 | return (
143 | x1 <= mp.x && mp.x <= x2
144 | && y1 <= mp.y && mp.y <= y2
145 | );
146 | };
147 |
148 | let isEhEle = n => n.same(previewEles) || n.same(ghostNode);
149 |
150 | let nodesByDist = cy.nodes(n => !isEhEle(n) && isWithinThreshold(n)).sort(cmp);
151 | let snapped = false;
152 |
153 | if( tgt.nonempty() && !isWithinThreshold(tgt) ){
154 | this.unpreview(tgt);
155 | }
156 |
157 | for(let i = 0; i < nodesByDist.length; i++){
158 | let n = nodesByDist[i];
159 |
160 | // skip a parent node when the mouse is inside it
161 | if( n.isParent() && mouseIsInside(n) ){ continue; }
162 |
163 | // skip a child node when the mouse is not inside the parent
164 | if( n.isChild() && !mouseIsInside(n.parent()) ){ continue; }
165 |
166 | if( n.same(tgt) || this.preview(n, allowHoverDelay) ){
167 | snapped = true;
168 | break;
169 | }
170 | }
171 |
172 | return snapped;
173 | }
174 |
175 | function preview( target, allowHoverDelay = true ){
176 | let { options, sourceNode, ghostNode, ghostEles, presumptiveTargets, previewEles, active } = this;
177 | let source = sourceNode;
178 | let isGhost = target.same( ghostNode );
179 | let noEdge = !this.canConnect( target );
180 | let isExistingTgt = target.same( this.targetNode );
181 |
182 | if(
183 | !active || isGhost || noEdge || isExistingTgt
184 | // || (target.isParent())
185 | ){
186 | return false;
187 | }
188 |
189 | if( this.targetNode.nonempty() ){
190 | this.unpreview( this.targetNode );
191 | }
192 |
193 | clearTimeout( this.previewTimeout );
194 |
195 | let applyPreview = () => {
196 | this.targetNode = target;
197 |
198 | presumptiveTargets.merge( target );
199 |
200 | target.addClass('eh-presumptive-target');
201 | target.addClass('eh-target');
202 |
203 | this.emit( 'hoverover', this.mp(), source, target );
204 |
205 | target.addClass('eh-preview');
206 |
207 | ghostEles.addClass('eh-preview-active');
208 | sourceNode.addClass('eh-preview-active');
209 | target.addClass('eh-preview-active');
210 |
211 | this.makePreview();
212 |
213 | this.emit( 'previewon', this.mp(), source, target, previewEles );
214 | };
215 |
216 | if( allowHoverDelay && options.hoverDelay > 0 ){
217 | this.previewTimeout = setTimeout( applyPreview, options.hoverDelay );
218 | } else {
219 | applyPreview();
220 | }
221 |
222 | return true;
223 | }
224 |
225 | function unpreview( target ) {
226 | if( !this.active ){ return; }
227 |
228 | let { previewTimeout, sourceNode, previewEles, ghostEles, cy } = this;
229 | clearTimeout( previewTimeout );
230 | this.previewTimeout = null;
231 |
232 | let source = sourceNode;
233 |
234 | target.removeClass('eh-preview eh-target eh-presumptive-target eh-preview-active');
235 | ghostEles.removeClass('eh-preview-active');
236 | sourceNode.removeClass('eh-preview-active');
237 |
238 | this.targetNode = cy.collection();
239 |
240 | this.removePreview( source, target );
241 |
242 | this.emit( 'hoverout', this.mp(), source, target );
243 | this.emit( 'previewoff', this.mp(), source, target, previewEles );
244 |
245 | return this;
246 | }
247 |
248 | function stop(){
249 | if( !this.active ){ return; }
250 |
251 | let { sourceNode, targetNode, ghostEles, presumptiveTargets } = this;
252 |
253 | clearTimeout( this.previewTimeout );
254 |
255 | sourceNode.removeClass('eh-source eh-preview-active');
256 | targetNode.removeClass('eh-target eh-preview eh-hover eh-preview-active');
257 | presumptiveTargets.removeClass('eh-presumptive-target');
258 |
259 | this.makeEdges();
260 |
261 | ghostEles.remove();
262 |
263 | this.clearCollections();
264 |
265 | this.resetGestures();
266 | this.enableEdgeEvents();
267 |
268 | this.active = false;
269 |
270 | this.emit( 'stop', this.mp(), sourceNode );
271 |
272 | return this;
273 | }
274 |
275 | module.exports = {
276 | start, update, preview, unpreview, stop, snap,
277 | canStartOn, canStartDrawModeOn, canStartNonDrawModeOn
278 | };
279 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cytoscape-edgehandles.js demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
59 |
60 |
276 |
277 |
278 |
279 | cytoscape-edgehandles demo
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
--------------------------------------------------------------------------------
/cytoscape-edgehandles.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory(require("lodash.memoize"), require("lodash.throttle"));
4 | else if(typeof define === 'function' && define.amd)
5 | define(["lodash.memoize", "lodash.throttle"], factory);
6 | else if(typeof exports === 'object')
7 | exports["cytoscapeEdgehandles"] = factory(require("lodash.memoize"), require("lodash.throttle"));
8 | else
9 | root["cytoscapeEdgehandles"] = factory(root["_"]["memoize"], root["_"]["throttle"]);
10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_13__, __WEBPACK_EXTERNAL_MODULE_14__) {
11 | return /******/ (function(modules) { // webpackBootstrap
12 | /******/ // The module cache
13 | /******/ var installedModules = {};
14 | /******/
15 | /******/ // The require function
16 | /******/ function __webpack_require__(moduleId) {
17 | /******/
18 | /******/ // Check if module is in cache
19 | /******/ if(installedModules[moduleId]) {
20 | /******/ return installedModules[moduleId].exports;
21 | /******/ }
22 | /******/ // Create a new module (and put it into the cache)
23 | /******/ var module = installedModules[moduleId] = {
24 | /******/ i: moduleId,
25 | /******/ l: false,
26 | /******/ exports: {}
27 | /******/ };
28 | /******/
29 | /******/ // Execute the module function
30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
31 | /******/
32 | /******/ // Flag the module as loaded
33 | /******/ module.l = true;
34 | /******/
35 | /******/ // Return the exports of the module
36 | /******/ return module.exports;
37 | /******/ }
38 | /******/
39 | /******/
40 | /******/ // expose the modules object (__webpack_modules__)
41 | /******/ __webpack_require__.m = modules;
42 | /******/
43 | /******/ // expose the module cache
44 | /******/ __webpack_require__.c = installedModules;
45 | /******/
46 | /******/ // identity function for calling harmony imports with the correct context
47 | /******/ __webpack_require__.i = function(value) { return value; };
48 | /******/
49 | /******/ // define getter function for harmony exports
50 | /******/ __webpack_require__.d = function(exports, name, getter) {
51 | /******/ if(!__webpack_require__.o(exports, name)) {
52 | /******/ Object.defineProperty(exports, name, {
53 | /******/ configurable: false,
54 | /******/ enumerable: true,
55 | /******/ get: getter
56 | /******/ });
57 | /******/ }
58 | /******/ };
59 | /******/
60 | /******/ // getDefaultExport function for compatibility with non-harmony modules
61 | /******/ __webpack_require__.n = function(module) {
62 | /******/ var getter = module && module.__esModule ?
63 | /******/ function getDefault() { return module['default']; } :
64 | /******/ function getModuleExports() { return module; };
65 | /******/ __webpack_require__.d(getter, 'a', getter);
66 | /******/ return getter;
67 | /******/ };
68 | /******/
69 | /******/ // Object.prototype.hasOwnProperty.call
70 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
71 | /******/
72 | /******/ // __webpack_public_path__
73 | /******/ __webpack_require__.p = "";
74 | /******/
75 | /******/ // Load entry module and return exports
76 | /******/ return __webpack_require__(__webpack_require__.s = 12);
77 | /******/ })
78 | /************************************************************************/
79 | /******/ ([
80 | /* 0 */
81 | /***/ (function(module, exports, __webpack_require__) {
82 |
83 | "use strict";
84 |
85 |
86 | // Simple, internal Object.assign() polyfill for options objects etc.
87 |
88 | module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
89 | for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
90 | srcs[_key - 1] = arguments[_key];
91 | }
92 |
93 | srcs.filter(function (src) {
94 | return src != null;
95 | }).forEach(function (src) {
96 | Object.keys(src).forEach(function (k) {
97 | return tgt[k] = src[k];
98 | });
99 | });
100 |
101 | return tgt;
102 | };
103 |
104 | /***/ }),
105 | /* 1 */
106 | /***/ (function(module, exports, __webpack_require__) {
107 |
108 | "use strict";
109 |
110 |
111 | var Edgehandles = __webpack_require__(10);
112 | var assign = __webpack_require__(0);
113 |
114 | module.exports = function (options) {
115 | var cy = this;
116 |
117 | return new Edgehandles(assign({ cy: cy }, options));
118 | };
119 |
120 | /***/ }),
121 | /* 2 */
122 | /***/ (function(module, exports, __webpack_require__) {
123 |
124 | "use strict";
125 |
126 |
127 | function disableGestures() {
128 | this.saveGestureState();
129 |
130 | this.cy.zoomingEnabled(false).panningEnabled(false).boxSelectionEnabled(false);
131 |
132 | if (this.options.disableBrowserGestures) {
133 | var wlOpts = this.windowListenerOptions;
134 |
135 | window.addEventListener('touchstart', this.preventDefault, wlOpts);
136 | window.addEventListener('touchmove', this.preventDefault, wlOpts);
137 | window.addEventListener('wheel', this.preventDefault, wlOpts);
138 | }
139 |
140 | return this;
141 | }
142 |
143 | function resetGestures() {
144 | this.cy.zoomingEnabled(this.lastZoomingEnabled).panningEnabled(this.lastPanningEnabled).boxSelectionEnabled(this.lastBoxSelectionEnabled);
145 |
146 | if (this.options.disableBrowserGestures) {
147 | var wlOpts = this.windowListenerOptions;
148 |
149 | window.removeEventListener('touchstart', this.preventDefault, wlOpts);
150 | window.removeEventListener('touchmove', this.preventDefault, wlOpts);
151 | window.removeEventListener('wheel', this.preventDefault, wlOpts);
152 | }
153 |
154 | return this;
155 | }
156 |
157 | function saveGestureState() {
158 | var cy = this.cy;
159 |
160 |
161 | this.lastPanningEnabled = cy.panningEnabled();
162 | this.lastZoomingEnabled = cy.zoomingEnabled();
163 | this.lastBoxSelectionEnabled = cy.boxSelectionEnabled();
164 |
165 | return this;
166 | }
167 |
168 | module.exports = { disableGestures: disableGestures, resetGestures: resetGestures, saveGestureState: saveGestureState };
169 |
170 | /***/ }),
171 | /* 3 */
172 | /***/ (function(module, exports, __webpack_require__) {
173 |
174 | "use strict";
175 |
176 |
177 | function addCytoscapeListeners() {
178 | var _this = this;
179 |
180 | var cy = this.cy,
181 | options = this.options;
182 |
183 | // grabbing nodes
184 |
185 | this.addListener(cy, 'drag', function () {
186 | return _this.grabbingNode = true;
187 | });
188 | this.addListener(cy, 'free', function () {
189 | return _this.grabbingNode = false;
190 | });
191 |
192 | // start on tapstart handle
193 | // start on tapstart node (draw mode)
194 | // toggle on source node
195 | this.addListener(cy, 'tapstart', 'node', function (e) {
196 | var node = e.target;
197 |
198 | if (_this.drawMode) {
199 | _this.start(node);
200 | }
201 | });
202 |
203 | // update line on drag
204 | this.addListener(cy, 'tapdrag', function (e) {
205 | _this.update(e.position);
206 | });
207 |
208 | // hover over preview
209 | this.addListener(cy, 'tapdragover', 'node', function (e) {
210 | if (options.snap) {
211 | // then ignore events like mouseover
212 | } else {
213 | _this.preview(e.target);
214 | }
215 | });
216 |
217 | // hover out unpreview
218 | this.addListener(cy, 'tapdragout', 'node', function (e) {
219 | if (options.snap) {
220 | // then keep the preview
221 | } else {
222 | _this.unpreview(e.target);
223 | }
224 | });
225 |
226 | // stop gesture on tapend
227 | this.addListener(cy, 'tapend', function () {
228 | _this.stop();
229 | });
230 |
231 | return this;
232 | }
233 |
234 | module.exports = { addCytoscapeListeners: addCytoscapeListeners };
235 |
236 | /***/ }),
237 | /* 4 */
238 | /***/ (function(module, exports, __webpack_require__) {
239 |
240 | "use strict";
241 |
242 |
243 | /* eslint-disable no-unused-vars */
244 | var defaults = {
245 | canConnect: function canConnect(sourceNode, targetNode) {
246 | // whether an edge can be created between source and target
247 | return !sourceNode.same(targetNode); // e.g. disallow loops
248 | },
249 | edgeParams: function edgeParams(sourceNode, targetNode) {
250 | // for edges between the specified source and target
251 | // return element object to be passed to cy.add() for edge
252 | return {};
253 | },
254 | hoverDelay: 150, // time spent hovering over a target node before it is considered selected
255 | snap: true, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs)
256 | snapThreshold: 50, // the target node must be less than or equal to this many pixels away from the cursor/finger
257 | snapFrequency: 15, // the number of times per second (Hz) that snap checks done (lower is less expensive)
258 | noEdgeEventsInDraw: true, // set events:no to edges during draws, prevents mouseouts on compounds
259 | disableBrowserGestures: true // during an edge drawing gesture, disable browser gestures such as two-finger trackpad swipe and pinch-to-zoom
260 | };
261 | /* eslint-enable */
262 |
263 | module.exports = defaults;
264 |
265 | /***/ }),
266 | /* 5 */
267 | /***/ (function(module, exports, __webpack_require__) {
268 |
269 | "use strict";
270 |
271 |
272 | function toggleDrawMode(bool) {
273 | var cy = this.cy;
274 |
275 |
276 | this.drawMode = bool != null ? bool : !this.drawMode;
277 |
278 | if (this.drawMode) {
279 | this.prevUngrabifyState = cy.autoungrabify();
280 |
281 | cy.autoungrabify(true);
282 |
283 | this.emit('drawon');
284 | } else {
285 | cy.autoungrabify(this.prevUngrabifyState);
286 |
287 | this.emit('drawoff');
288 | }
289 |
290 | return this;
291 | }
292 |
293 | function enableDrawMode() {
294 | return this.toggleDrawMode(true);
295 | }
296 |
297 | function disableDrawMode() {
298 | return this.toggleDrawMode(false);
299 | }
300 |
301 | module.exports = { toggleDrawMode: toggleDrawMode, enableDrawMode: enableDrawMode, disableDrawMode: disableDrawMode };
302 |
303 | /***/ }),
304 | /* 6 */
305 | /***/ (function(module, exports, __webpack_require__) {
306 |
307 | "use strict";
308 |
309 |
310 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
311 |
312 | var assign = __webpack_require__(0);
313 | var isString = function isString(x) {
314 | return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === _typeof('');
315 | };
316 | var isArray = function isArray(x) {
317 | return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === _typeof([]) && x.length != null;
318 | };
319 |
320 | function getEleJson(overrides, params, addedClasses) {
321 | var json = {};
322 |
323 | // basic values
324 | assign(json, params, overrides);
325 |
326 | // make sure params can specify data but that overrides take precedence
327 | assign(json.data, params.data, overrides.data);
328 |
329 | if (isString(params.classes)) {
330 | json.classes = params.classes + ' ' + addedClasses;
331 | } else if (isArray(params.classes)) {
332 | json.classes = params.classes.join(' ') + ' ' + addedClasses;
333 | } else {
334 | json.classes = addedClasses;
335 | }
336 |
337 | return json;
338 | }
339 |
340 | function makeEdges() {
341 | var preview = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
342 | var cy = this.cy,
343 | options = this.options,
344 | presumptiveTargets = this.presumptiveTargets,
345 | previewEles = this.previewEles,
346 | active = this.active;
347 |
348 |
349 | var source = this.sourceNode;
350 | var target = this.targetNode;
351 | var classes = preview ? 'eh-preview' : '';
352 | var added = cy.collection();
353 | var canConnect = this.canConnect(target);
354 |
355 | // can't make edges outside of regular gesture lifecycle
356 | if (!active) {
357 | return;
358 | }
359 |
360 | // must be able to connect
361 | if (!canConnect) {
362 | return;
363 | }
364 |
365 | // detect cancel
366 | if (!target || target.size() === 0) {
367 | previewEles.remove();
368 |
369 | this.emit('cancel', this.mp(), source, presumptiveTargets);
370 |
371 | return;
372 | }
373 |
374 | // just remove preview class if we already have the edges
375 | if (!preview) {
376 | previewEles.removeClass('eh-preview').style('events', '');
377 |
378 | this.emit('complete', this.mp(), source, target, previewEles);
379 |
380 | return;
381 | }
382 |
383 | var source2target = cy.add(getEleJson({
384 | group: 'edges',
385 | data: {
386 | source: source.id(),
387 | target: target.id()
388 | }
389 | }, this.edgeParams(target), classes));
390 |
391 | added = added.merge(source2target);
392 |
393 | if (preview) {
394 | this.previewEles = added;
395 |
396 | added.style('events', 'no');
397 | } else {
398 | added.style('events', '');
399 |
400 | this.emit('complete', this.mp(), source, target, added);
401 | }
402 |
403 | return this;
404 | }
405 |
406 | function makePreview() {
407 | this.makeEdges(true);
408 |
409 | return this;
410 | }
411 |
412 | function previewShown() {
413 | return this.previewEles.nonempty() && this.previewEles.inside();
414 | }
415 |
416 | function removePreview() {
417 | if (this.previewShown()) {
418 | this.previewEles.remove();
419 | }
420 |
421 | return this;
422 | }
423 |
424 | function updateEdge() {
425 | var _this = this;
426 |
427 | var sourceNode = this.sourceNode,
428 | ghostNode = this.ghostNode,
429 | cy = this.cy,
430 | mx = this.mx,
431 | my = this.my;
432 |
433 | var x = mx;
434 | var y = my;
435 | var ghostEdge = void 0,
436 | ghostEles = void 0;
437 |
438 | // can't draw a line without having the starting node
439 | if (!sourceNode) {
440 | return;
441 | }
442 |
443 | if (!ghostNode || ghostNode.length === 0 || ghostNode.removed()) {
444 | ghostEles = this.ghostEles = cy.collection();
445 |
446 | cy.batch(function () {
447 | ghostNode = _this.ghostNode = cy.add({
448 | group: 'nodes',
449 | classes: 'eh-ghost eh-ghost-node',
450 | position: {
451 | x: 0,
452 | y: 0
453 | }
454 | });
455 |
456 | ghostNode.style({
457 | 'background-color': 'blue',
458 | 'width': 0.0001,
459 | 'height': 0.0001,
460 | 'opacity': 0,
461 | 'events': 'no'
462 | });
463 |
464 | var ghostEdgeParams = {};
465 |
466 | ghostEdge = cy.add(assign({}, ghostEdgeParams, {
467 | group: 'edges',
468 | data: assign({}, ghostEdgeParams.data, {
469 | source: sourceNode.id(),
470 | target: ghostNode.id()
471 | }),
472 | classes: 'eh-ghost eh-ghost-edge'
473 | }));
474 |
475 | ghostEdge.style({
476 | 'events': 'no'
477 | });
478 | });
479 |
480 | ghostEles.merge(ghostNode).merge(ghostEdge);
481 | }
482 |
483 | ghostNode.position({ x: x, y: y });
484 |
485 | return this;
486 | }
487 |
488 | module.exports = {
489 | makeEdges: makeEdges, makePreview: makePreview, removePreview: removePreview, previewShown: previewShown,
490 | updateEdge: updateEdge
491 | };
492 |
493 | /***/ }),
494 | /* 7 */
495 | /***/ (function(module, exports, __webpack_require__) {
496 |
497 | "use strict";
498 |
499 |
500 | function disableEdgeEvents() {
501 | if (this.options.noEdgeEventsInDraw) {
502 | this.cy.edges().style('events', 'no');
503 | }
504 |
505 | return this;
506 | }
507 |
508 | function enableEdgeEvents() {
509 | if (this.options.noEdgeEventsInDraw) {
510 | this.cy.edges().style('events', '');
511 | }
512 |
513 | return this;
514 | }
515 |
516 | module.exports = { disableEdgeEvents: disableEdgeEvents, enableEdgeEvents: enableEdgeEvents };
517 |
518 | /***/ }),
519 | /* 8 */
520 | /***/ (function(module, exports, __webpack_require__) {
521 |
522 | "use strict";
523 |
524 |
525 | function enable() {
526 | this.enabled = true;
527 |
528 | this.emit('enable');
529 |
530 | return this;
531 | }
532 |
533 | function disable() {
534 | this.enabled = false;
535 |
536 | this.emit('disable');
537 |
538 | return this;
539 | }
540 |
541 | module.exports = { enable: enable, disable: disable };
542 |
543 | /***/ }),
544 | /* 9 */
545 | /***/ (function(module, exports, __webpack_require__) {
546 |
547 | "use strict";
548 |
549 |
550 | var memoize = __webpack_require__(13);
551 | var sqrt2 = Math.sqrt(2);
552 |
553 | function canStartOn(node) {
554 | var previewEles = this.previewEles,
555 | ghostEles = this.ghostEles;
556 |
557 | var isPreview = function isPreview(el) {
558 | return previewEles.anySame(el);
559 | };
560 | var isGhost = function isGhost(el) {
561 | return ghostEles.anySame(el);
562 | };
563 | var isTemp = function isTemp(el) {
564 | return isPreview(el) || isGhost(el);
565 | };
566 |
567 | var enabled = this.enabled,
568 | active = this.active,
569 | grabbingNode = this.grabbingNode;
570 |
571 |
572 | return enabled && !active && !grabbingNode && node != null && node.nonempty() && !isTemp(node);
573 | }
574 |
575 | function canStartDrawModeOn(node) {
576 | return this.canStartOn(node) && this.drawMode;
577 | }
578 |
579 | function canStartNonDrawModeOn(node) {
580 | return this.canStartOn(node) && !this.drawMode;
581 | }
582 |
583 | function start(node) {
584 | var _this = this;
585 |
586 | if (!this.canStartOn(node)) {
587 | return;
588 | }
589 |
590 | this.active = true;
591 |
592 | this.sourceNode = node;
593 | this.sourceNode.addClass('eh-source');
594 |
595 | this.disableGestures();
596 | this.disableEdgeEvents();
597 |
598 | var getId = function getId(n) {
599 | return n.id();
600 | };
601 |
602 | this.canConnect = memoize(function (target) {
603 | return _this.options.canConnect(_this.sourceNode, target);
604 | }, getId);
605 |
606 | this.edgeParams = memoize(function (target) {
607 | return _this.options.edgeParams(_this.sourceNode, target);
608 | }, getId);
609 |
610 | this.emit('start', this.hp(), node);
611 | }
612 |
613 | function update(pos) {
614 | if (!this.active) {
615 | return;
616 | }
617 |
618 | var p = pos;
619 |
620 | this.mx = p.x;
621 | this.my = p.y;
622 |
623 | this.updateEdge();
624 | this.throttledSnap();
625 |
626 | return this;
627 | }
628 |
629 | function snap() {
630 | if (!this.active || !this.options.snap) {
631 | return false;
632 | }
633 |
634 | var cy = this.cy;
635 | var tgt = this.targetNode;
636 | var threshold = this.options.snapThreshold;
637 | var mousePos = this.mp();
638 | var previewEles = this.previewEles,
639 | ghostNode = this.ghostNode;
640 |
641 |
642 | var radius = function radius(n) {
643 | return sqrt2 * Math.max(n.outerWidth(), n.outerHeight()) / 2;
644 | }; // worst-case enclosure of bb by circle
645 | var sqDist = function sqDist(x1, y1, x2, y2) {
646 | var dx = x2 - x1;var dy = y2 - y1;return dx * dx + dy * dy;
647 | };
648 | var sqDistByPt = function sqDistByPt(p1, p2) {
649 | return sqDist(p1.x, p1.y, p2.x, p2.y);
650 | };
651 | var nodeSqDist = function nodeSqDist(n) {
652 | return sqDistByPt(n.position(), mousePos);
653 | };
654 |
655 | var sqThreshold = function sqThreshold(n) {
656 | var r = radius(n);var t = r + threshold;return t * t;
657 | };
658 | var isWithinThreshold = function isWithinThreshold(n) {
659 | return nodeSqDist(n) <= sqThreshold(n);
660 | };
661 |
662 | var bbSqDist = function bbSqDist(n) {
663 | var p = n.position();
664 | var halfW = n.outerWidth() / 2;
665 | var halfH = n.outerHeight() / 2;
666 |
667 | // node and mouse positions, line is formed from node to mouse
668 | var nx = p.x;
669 | var ny = p.y;
670 | var mx = mousePos.x;
671 | var my = mousePos.y;
672 |
673 | // bounding box
674 | var x1 = nx - halfW;
675 | var x2 = nx + halfW;
676 | var y1 = ny - halfH;
677 | var y2 = ny + halfH;
678 |
679 | var insideXBounds = x1 <= mx && mx <= x2;
680 | var insideYBounds = y1 <= my && my <= y2;
681 |
682 | if (insideXBounds && insideYBounds) {
683 | // inside box
684 | return 0;
685 | } else if (insideXBounds) {
686 | // perpendicular distance to box, top or bottom
687 | var dy1 = my - y1;
688 | var dy2 = my - y2;
689 |
690 | return Math.min(dy1 * dy1, dy2 * dy2);
691 | } else if (insideYBounds) {
692 | // perpendicular distance to box, left or right
693 | var dx1 = mx - x1;
694 | var dx2 = mx - x2;
695 |
696 | return Math.min(dx1 * dx1, dx2 * dx2);
697 | } else if (mx < x1 && my < y1) {
698 | // top-left corner distance
699 | return sqDist(mx, my, x1, y1);
700 | } else if (mx > x2 && my < y1) {
701 | // top-right corner distance
702 | return sqDist(mx, my, x2, y1);
703 | } else if (mx < x1 && my > y2) {
704 | // bottom-left corner distance
705 | return sqDist(mx, my, x1, y2);
706 | } else {
707 | // bottom-right corner distance
708 | return sqDist(mx, my, x2, y2);
709 | }
710 | };
711 |
712 | var cmpBbSqDist = function cmpBbSqDist(n1, n2) {
713 | return bbSqDist(n1) - bbSqDist(n2);
714 | };
715 |
716 | var cmp = cmpBbSqDist;
717 |
718 | var allowHoverDelay = false;
719 |
720 | var mouseIsInside = function mouseIsInside(n) {
721 | var mp = mousePos;
722 | var w = n.outerWidth();
723 | var halfW = w / 2;
724 | var h = n.outerHeight();
725 | var halfH = h / 2;
726 | var p = n.position();
727 | var x1 = p.x - halfW;
728 | var x2 = p.x + halfW;
729 | var y1 = p.y - halfH;
730 | var y2 = p.y + halfH;
731 |
732 | return x1 <= mp.x && mp.x <= x2 && y1 <= mp.y && mp.y <= y2;
733 | };
734 |
735 | var isEhEle = function isEhEle(n) {
736 | return n.same(previewEles) || n.same(ghostNode);
737 | };
738 |
739 | var nodesByDist = cy.nodes(function (n) {
740 | return !isEhEle(n) && isWithinThreshold(n);
741 | }).sort(cmp);
742 | var snapped = false;
743 |
744 | if (tgt.nonempty() && !isWithinThreshold(tgt)) {
745 | this.unpreview(tgt);
746 | }
747 |
748 | for (var i = 0; i < nodesByDist.length; i++) {
749 | var n = nodesByDist[i];
750 |
751 | // skip a parent node when the mouse is inside it
752 | if (n.isParent() && mouseIsInside(n)) {
753 | continue;
754 | }
755 |
756 | // skip a child node when the mouse is not inside the parent
757 | if (n.isChild() && !mouseIsInside(n.parent())) {
758 | continue;
759 | }
760 |
761 | if (n.same(tgt) || this.preview(n, allowHoverDelay)) {
762 | snapped = true;
763 | break;
764 | }
765 | }
766 |
767 | return snapped;
768 | }
769 |
770 | function preview(target) {
771 | var _this2 = this;
772 |
773 | var allowHoverDelay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
774 | var options = this.options,
775 | sourceNode = this.sourceNode,
776 | ghostNode = this.ghostNode,
777 | ghostEles = this.ghostEles,
778 | presumptiveTargets = this.presumptiveTargets,
779 | previewEles = this.previewEles,
780 | active = this.active;
781 |
782 | var source = sourceNode;
783 | var isGhost = target.same(ghostNode);
784 | var noEdge = !this.canConnect(target);
785 | var isExistingTgt = target.same(this.targetNode);
786 |
787 | if (!active || isGhost || noEdge || isExistingTgt
788 | // || (target.isParent())
789 | ) {
790 | return false;
791 | }
792 |
793 | if (this.targetNode.nonempty()) {
794 | this.unpreview(this.targetNode);
795 | }
796 |
797 | clearTimeout(this.previewTimeout);
798 |
799 | var applyPreview = function applyPreview() {
800 | _this2.targetNode = target;
801 |
802 | presumptiveTargets.merge(target);
803 |
804 | target.addClass('eh-presumptive-target');
805 | target.addClass('eh-target');
806 |
807 | _this2.emit('hoverover', _this2.mp(), source, target);
808 |
809 | target.addClass('eh-preview');
810 |
811 | ghostEles.addClass('eh-preview-active');
812 | sourceNode.addClass('eh-preview-active');
813 | target.addClass('eh-preview-active');
814 |
815 | _this2.makePreview();
816 |
817 | _this2.emit('previewon', _this2.mp(), source, target, previewEles);
818 | };
819 |
820 | if (allowHoverDelay && options.hoverDelay > 0) {
821 | this.previewTimeout = setTimeout(applyPreview, options.hoverDelay);
822 | } else {
823 | applyPreview();
824 | }
825 |
826 | return true;
827 | }
828 |
829 | function unpreview(target) {
830 | if (!this.active) {
831 | return;
832 | }
833 |
834 | var previewTimeout = this.previewTimeout,
835 | sourceNode = this.sourceNode,
836 | previewEles = this.previewEles,
837 | ghostEles = this.ghostEles,
838 | cy = this.cy;
839 |
840 | clearTimeout(previewTimeout);
841 | this.previewTimeout = null;
842 |
843 | var source = sourceNode;
844 |
845 | target.removeClass('eh-preview eh-target eh-presumptive-target eh-preview-active');
846 | ghostEles.removeClass('eh-preview-active');
847 | sourceNode.removeClass('eh-preview-active');
848 |
849 | this.targetNode = cy.collection();
850 |
851 | this.removePreview(source, target);
852 |
853 | this.emit('hoverout', this.mp(), source, target);
854 | this.emit('previewoff', this.mp(), source, target, previewEles);
855 |
856 | return this;
857 | }
858 |
859 | function stop() {
860 | if (!this.active) {
861 | return;
862 | }
863 |
864 | var sourceNode = this.sourceNode,
865 | targetNode = this.targetNode,
866 | ghostEles = this.ghostEles,
867 | presumptiveTargets = this.presumptiveTargets;
868 |
869 |
870 | clearTimeout(this.previewTimeout);
871 |
872 | sourceNode.removeClass('eh-source eh-preview-active');
873 | targetNode.removeClass('eh-target eh-preview eh-hover eh-preview-active');
874 | presumptiveTargets.removeClass('eh-presumptive-target');
875 |
876 | this.makeEdges();
877 |
878 | ghostEles.remove();
879 |
880 | this.clearCollections();
881 |
882 | this.resetGestures();
883 | this.enableEdgeEvents();
884 |
885 | this.active = false;
886 |
887 | this.emit('stop', this.mp(), sourceNode);
888 |
889 | return this;
890 | }
891 |
892 | module.exports = {
893 | start: start, update: update, preview: preview, unpreview: unpreview, stop: stop, snap: snap,
894 | canStartOn: canStartOn, canStartDrawModeOn: canStartDrawModeOn, canStartNonDrawModeOn: canStartNonDrawModeOn
895 | };
896 |
897 | /***/ }),
898 | /* 10 */
899 | /***/ (function(module, exports, __webpack_require__) {
900 |
901 | "use strict";
902 |
903 |
904 | var defaults = __webpack_require__(4);
905 | var assign = __webpack_require__(0);
906 | var throttle = __webpack_require__(14);
907 |
908 | var cyGesturesToggle = __webpack_require__(2);
909 | var cyListeners = __webpack_require__(3);
910 | var drawMode = __webpack_require__(5);
911 | var drawing = __webpack_require__(6);
912 | var enabling = __webpack_require__(8);
913 | var gestureLifecycle = __webpack_require__(9);
914 | var listeners = __webpack_require__(11);
915 | var edgeEvents = __webpack_require__(7);
916 |
917 | function Edgehandles(options) {
918 | var cy = options.cy;
919 |
920 | this.cy = cy;
921 | this.listeners = [];
922 |
923 | // edgehandles gesture state
924 | this.enabled = true;
925 | this.drawMode = false;
926 | this.active = false;
927 | this.grabbingNode = false;
928 |
929 | // edgehandles elements
930 | this.clearCollections();
931 |
932 | // mouse position
933 | this.mx = 0;
934 | this.my = 0;
935 |
936 | this.options = assign({}, defaults, options);
937 |
938 | this.saveGestureState();
939 | this.addListeners();
940 |
941 | this.throttledSnap = throttle(this.snap.bind(this), 1000 / options.snapFrequency);
942 |
943 | this.preventDefault = function (e) {
944 | return e.preventDefault();
945 | };
946 |
947 | // disabled until start()
948 | this.canConnect = function () {
949 | return false;
950 | };
951 |
952 | var supportsPassive = false;
953 | try {
954 | var opts = Object.defineProperty({}, 'passive', {
955 | get: function get() {
956 | supportsPassive = true;
957 | }
958 | });
959 |
960 | window.addEventListener('test', null, opts);
961 | } catch (err) {
962 | // swallow
963 | }
964 |
965 | if (supportsPassive) {
966 | this.windowListenerOptions = { capture: true, passive: false };
967 | } else {
968 | this.windowListenerOptions = true;
969 | }
970 | }
971 |
972 | var proto = Edgehandles.prototype = {};
973 | var extend = function extend(obj) {
974 | return assign(proto, obj);
975 | };
976 |
977 | proto.destroy = function () {
978 | this.removeListeners();
979 | };
980 |
981 | proto.setOptions = function (options) {
982 | assign(this.options, options);
983 | };
984 |
985 | proto.mp = function () {
986 | return { x: this.mx, y: this.my };
987 | };
988 |
989 | proto.hp = function () {
990 | return { x: this.hx, y: this.hy };
991 | };
992 |
993 | proto.clearCollections = function () {
994 | var cy = this.cy;
995 |
996 |
997 | this.previewEles = cy.collection();
998 | this.ghostEles = cy.collection();
999 | this.ghostNode = cy.collection();
1000 | this.sourceNode = cy.collection();
1001 | this.targetNode = cy.collection();
1002 | this.presumptiveTargets = cy.collection();
1003 | };
1004 |
1005 | [cyGesturesToggle, cyListeners, drawMode, drawing, enabling, gestureLifecycle, listeners, edgeEvents].forEach(extend);
1006 |
1007 | module.exports = Edgehandles;
1008 |
1009 | /***/ }),
1010 | /* 11 */
1011 | /***/ (function(module, exports, __webpack_require__) {
1012 |
1013 | "use strict";
1014 |
1015 |
1016 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
1017 |
1018 | function addListeners() {
1019 | var _this = this;
1020 |
1021 | this.addCytoscapeListeners();
1022 |
1023 | this.addListener(this.cy, 'destroy', function () {
1024 | return _this.destroy();
1025 | });
1026 |
1027 | return this;
1028 | }
1029 |
1030 | function removeListeners() {
1031 | for (var i = this.listeners.length - 1; i >= 0; i--) {
1032 | var l = this.listeners[i];
1033 |
1034 | this.removeListener(l.target, l.event, l.selector, l.callback, l.options);
1035 | }
1036 |
1037 | return this;
1038 | }
1039 |
1040 | function getListener(target, event, selector, callback, options) {
1041 | if ((typeof selector === 'undefined' ? 'undefined' : _typeof(selector)) !== _typeof('')) {
1042 | callback = selector;
1043 | options = callback;
1044 | selector = null;
1045 | }
1046 |
1047 | if (options == null) {
1048 | options = false;
1049 | }
1050 |
1051 | return { target: target, event: event, selector: selector, callback: callback, options: options };
1052 | }
1053 |
1054 | function isDom(target) {
1055 | return target instanceof Element;
1056 | }
1057 |
1058 | function addListener(target, event, selector, callback, options) {
1059 | var l = getListener(target, event, selector, callback, options);
1060 |
1061 | this.listeners.push(l);
1062 |
1063 | if (isDom(l.target)) {
1064 | l.target.addEventListener(l.event, l.callback, l.options);
1065 | } else {
1066 | if (l.selector) {
1067 | l.target.addListener(l.event, l.selector, l.callback, l.options);
1068 | } else {
1069 | l.target.addListener(l.event, l.callback, l.options);
1070 | }
1071 | }
1072 |
1073 | return this;
1074 | }
1075 |
1076 | function removeListener(target, event, selector, callback, options) {
1077 | var l = getListener(target, event, selector, callback, options);
1078 |
1079 | for (var i = this.listeners.length - 1; i >= 0; i--) {
1080 | var l2 = this.listeners[i];
1081 |
1082 | if (l.target === l2.target && l.event === l2.event && (l.selector == null || l.selector === l2.selector) && (l.callback == null || l.callback === l2.callback)) {
1083 | this.listeners.splice(i, 1);
1084 |
1085 | if (isDom(l.target)) {
1086 | l.target.removeEventListener(l.event, l.callback, l.options);
1087 | } else {
1088 | if (l.selector) {
1089 | l.target.removeListener(l.event, l.selector, l.callback, l.options);
1090 | } else {
1091 | l.target.removeListener(l.event, l.callback, l.options);
1092 | }
1093 | }
1094 |
1095 | break;
1096 | }
1097 | }
1098 |
1099 | return this;
1100 | }
1101 |
1102 | function emit(type, position) {
1103 | var cy = this.cy;
1104 |
1105 | for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
1106 | args[_key - 2] = arguments[_key];
1107 | }
1108 |
1109 | cy.emit({ type: 'eh' + type, position: position }, args);
1110 |
1111 | return this;
1112 | }
1113 |
1114 | module.exports = { addListener: addListener, addListeners: addListeners, removeListener: removeListener, removeListeners: removeListeners, emit: emit };
1115 |
1116 | /***/ }),
1117 | /* 12 */
1118 | /***/ (function(module, exports, __webpack_require__) {
1119 |
1120 | "use strict";
1121 |
1122 |
1123 | var impl = __webpack_require__(1);
1124 |
1125 | // registers the extension on a cytoscape lib ref
1126 | var register = function register(cytoscape) {
1127 | if (!cytoscape) {
1128 | return;
1129 | } // can't register if cytoscape unspecified
1130 |
1131 | cytoscape('core', 'edgehandles', impl); // register with cytoscape.js
1132 | };
1133 |
1134 | if (typeof cytoscape !== 'undefined') {
1135 | // expose to global cytoscape (i.e. window.cytoscape)
1136 | register(cytoscape); // eslint-disable-line no-undef
1137 | }
1138 |
1139 | module.exports = register;
1140 |
1141 | /***/ }),
1142 | /* 13 */
1143 | /***/ (function(module, exports) {
1144 |
1145 | module.exports = __WEBPACK_EXTERNAL_MODULE_13__;
1146 |
1147 | /***/ }),
1148 | /* 14 */
1149 | /***/ (function(module, exports) {
1150 |
1151 | module.exports = __WEBPACK_EXTERNAL_MODULE_14__;
1152 |
1153 | /***/ })
1154 | /******/ ]);
1155 | });
--------------------------------------------------------------------------------
/demo-compound.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cytoscape-edgehandles.js compound demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
47 |
48 |
1142 |
1143 |
1144 |
1145 | cytoscape-edgehandles compound demo
1146 |
1147 |
1148 |
1149 |
1150 |
1151 |
1152 |
1153 |
1154 |
1155 |
1156 |
1157 |
1158 |
--------------------------------------------------------------------------------
/demo-compound-snap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cytoscape-edgehandles.js compound snapping demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
47 |
48 |
1310 |
1311 |
1312 |
1313 | cytoscape-edgehandles compound (snapping disabled)
1314 |
1315 |
1316 |
1317 |
1318 |
1319 |
1320 |
1321 |
1322 |
1323 |
1324 |
1325 |
1326 |
--------------------------------------------------------------------------------