From bdb2c08b120839c96252a418f38a07b020c91aa1 Mon Sep 17 00:00:00 2001 From: "Emma Suzuki (CONT)" Date: Thu, 27 Apr 2017 11:11:50 -0700 Subject: [PATCH 01/16] Add multiple handle support (#1) * Add multiple handle support * Fix spelling * Highlight handles * Update README.md * Support middle * Trigger complete action --- README.md | 7 +- cytoscape-edgehandles.js | 235 ++++++++++++++++++++++++++++++++++++++++++++--- demo-multi-handle.html | 147 +++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 13 deletions(-) create mode 100644 demo-multi-handle.html diff --git a/README.md b/README.md index 09ac057..9133a37 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ cytoscape-edgehandles ================================================================================ - ![Preview](https://raw.githubusercontent.com/cytoscape/cytoscape.js-edgehandles/master/img/preview.png) @@ -67,6 +66,9 @@ var defaults = { handleOutlineWidth: 0, // the width of the handle outline in pixels handleNodes: 'node', // selector/filter function for whether edges can be made from a given node handlePosition: 'middle top', // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" + handlePositionAngles: '0,180', // sets the position of the handle in the format of csv of angles (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node). You cannot use handlePosition and handlePositionAngles at the same time. + handleHighlightColor: '#ff0000', // the colour to highlight handle on hover + handleHighlightPercentOffset: '1.0', // percent offset respective to handle size hoverDelay: 150, // time spend over a target node before it is considered a target selection cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: true, // whether to start the plugin in the enabled state @@ -95,7 +97,7 @@ var defaults = { start: function( sourceNode ) { // fired when edgehandles interaction starts (drag on handle) }, - complete: function( sourceNode, targetNodes, addedEntities ) { + complete: function( sourceNode, targetNodes, addedEntities, sourceHandleAngle, targetHandleAngle ) { // fired when edgehandles is done and entities are added }, stop: function( sourceNode ) { @@ -163,6 +165,7 @@ All function can be called via `cy.edgehandles('function-name')`: * `cy.edgehandles('start', 'some-node-id')` : start the handle drag state on node with specified id (e.g. `'some-node-id'`) * `cy.edgehandles('drawon')` : enable draw mode * `cy.edgehandles('drawoff')` : disable draw mode + * `cyedgehandles('registerHandle', 'left-right', '0,180')` : register handle with type name and angles. Each node can be bind to specific handle type by setting edgeHandleType = 'left-right' in node data. If node doesn't have edgeHandleType, values from handlePositionAngles is used. ## Publishing instructions diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index 75ff256..c9c35f9 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -177,6 +177,9 @@ SOFTWARE. handleOutlineWidth: 0, // the width of the handle outline in pixels handleNodes: 'node', // selector/filter function for whether edges can be made from a given node handlePosition: 'middle top', // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" + handlePositionAngles: '0,180', // sets the position of the handle in the format of csv of angles (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node) + handleHighlightColor: '#ff0000', // the colour to highlight handle on hover + handleHighlightPercentOffset: '1.0', // percent offset respective to handle size hoverDelay: 150, // time spend over a target node before it is considered a target selection cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: true, // whether to start the plugin in the enabled state @@ -205,7 +208,7 @@ SOFTWARE. start: function( sourceNode ) { // fired when edgehandles interaction starts (drag on handle) }, - complete: function( sourceNode, targetNodes, addedEntities ) { + complete: function( sourceNode, targetNodes, addedEntities, sourceHandleAngle, targetHandleAngle ) { // fired when edgehandles is done and entities are added }, stop: function( sourceNode ) { @@ -246,6 +249,11 @@ SOFTWARE. } var options = data.options; + if (name === 'handlePosition') { + options.handlePositionAngles = ''; + } else if (name === 'handlePositionAngles') { + options.handlePosition = ''; + } if( value === undefined ) { if( typeof name == typeof {} ) { @@ -306,6 +314,18 @@ SOFTWARE. var sourceNode; var drawMode = false; var pxRatio; + var handleAngles = []; + var sourceHandleAngle; + var hoveredTarget; + var hoveredTargetHandle = {}; + + if (params.handlePosition) { + opts.handlePositionAngles = ''; + } else if (params.handlePositionAngles) { + opts.handlePosition = ''; + } else { + opts.handlePositionAngles = ''; + } function getDevicePixelRatio(){ return window.devicePixelRatio || 1; @@ -374,6 +394,14 @@ SOFTWARE. cy.autoungrabify( prevUngrabifyState ); } ); + var registerHandler; + cy.on('cyedgehandles.registerHandle', registerHandler = function ( event, typeName, angles ) { + handleAngles.push({ + name: typeName, + angles: angles + }); + }); + var ctx = $canvas[ 0 ].getContext( '2d' ); // write options to data @@ -480,7 +508,15 @@ SOFTWARE. } - function drawHandle() { + function drawHandle(node) { + if (options().handlePosition) { + drawHandleForPosition(hx, hy); + } else if (node) { + drawHandleForAngles(node); + } + } + + function drawHandleForPosition(hx, hy) { ctx.fillStyle = options().handleColor; ctx.strokeStyle = options().handleOutlineColor; @@ -503,6 +539,83 @@ SOFTWARE. drawsClear = false; } + function drawHandleForAngles(node) { + const positions = getHandlePositionsForAngle(node); + + positions.forEach(function (position) { + drawHandleForPosition(position.posX, position.posY); + }); + } + + function getHandlePositionsForAngle(node) { + var handles = handleAngles.filter(function (handle) { + return handle.name === node.data().edgeHandleType; + }); + + if (handles.length < 1) { + handles = options().handlePositionAngles; + } else { + handles = handles[0].angles; + } + + const angles = handles.split(',').reduce(function (prev, strAngle) { + const angle = parseInt(strAngle); + if (!isNaN(angle)) { + prev.push(angle); + } + + return prev; + }, []); + + const nodePos = node.renderedPosition(); + const nodeWidth = node.renderedWidth(); + const nodeHeight = node.renderedHeight(); + + return angles.map(function (angle) { + if (node.style().shape === 'ellipse' || !node.style().shape) { + return getHandleCenterForCircle(angle, nodePos, nodeWidth, nodeHeight); + } else { + return getHandleCenterForRect(angle, nodePos, nodeWidth, nodeHeight); + } + }) + } + + function getHandleCenterForCircle(angle, pos, width, height) { + if (angle < 0) { + return { angle: angle, posX: pos.x, posY: pos.y }; + } + + const rad = toRadian(angle); + const posX = pos.x + (width / 2) * Math.cos(rad); + const posY = pos.y - (height / 2) * Math.sin(rad); + + return { angle: angle, posX: posX, posY: posY }; + } + + function getHandleCenterForRect(angle, pos, width, height) { + if (angle < 0) { + return { angle: angle, posX: pos.x, posY: pos.y }; + } + + var posX, posY; + const rad = toRadian(angle); + + const abssin = Math.abs(Math.sin(rad)); + const abscos = Math.abs(Math.cos(rad)); + const fracMax = 1 / Math.max(abssin, abscos); + + const rectRadiusX = width / 2 * fracMax; + const rectRadiusY = height / 2 * fracMax; + posX = pos.x + rectRadiusX * Math.cos(rad); + posY = pos.y - rectRadiusY * Math.sin(rad); + + return { angle: angle, posX: posX, posY: posY }; + } + + function toRadian (angle) { + return angle * (Math.PI / 180); + } + var lineDrawRate = 1000 / 60; var drawLine = throttle( function( x, y ) { @@ -619,12 +732,21 @@ SOFTWARE. return; // nothing to do :( } + const hits = hitTest(targets, { x: mx, y: my }); + // just remove preview class if we already have the edges if( !src && !tgt ) { if( !preview && options().preview ) { added = cy.elements( '.edgehandles-preview' ).removeClass( 'edgehandles-preview' ); - options().complete( source, targets, added ); - source.trigger( 'cyedgehandles.complete' ); + + if (hits.length < 1) { + added.remove(); + } else { + const targetHandleAngle = hits[0]; + options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); + source.trigger( 'cyedgehandles.complete' ); + } + return; } else { // remove old previews @@ -719,14 +841,16 @@ SOFTWARE. } if( !preview ) { - options().complete( source, targets, added ); + const targetHandleAngle = hits[0]; + options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); source.trigger( 'cyedgehandles.complete' ); } } function hoverOver( node ) { var target = node; - + hoveredTarget = target; + clearTimeout( hoverTimeout ); hoverTimeout = setTimeout( function() { var source = cy.nodes( '.edgehandles-source' ); @@ -773,6 +897,12 @@ SOFTWARE. removePreview( source, target ); } + + if (target.data().id !== sourceNode.data().id) { + deHighlightHandle( target ); + hoveredTarget = null; + hoveredTargetHandle = {}; + } } function setHandleDimensions( node ){ @@ -782,6 +912,10 @@ SOFTWARE. hr = options().handleSize / 2 * cy.zoom(); + if (options().handlePositionAngles) { + return; + } + // store how much we should move the handle from origin(p.x, p.y) var moveX = 0; var moveY = 0; @@ -801,6 +935,47 @@ SOFTWARE. hy = p.y + moveY; } + function hitTest ( node, touchPos ) { + if (options().handlePosition) { + return []; + } + + const handlePositions = getHandlePositionsForAngle(node); + const nodePos = node.renderedPosition(); + const halfHandleSize = (options().handleIcon ? options().handleIcon.width : hr) * cy.zoom() / 2; + + return handlePositions.filter(function (handle) { + return Math.abs(handle.posX - touchPos.x) <= halfHandleSize && + Math.abs(handle.posY - touchPos.y) <= halfHandleSize; + }); + } + + function highlightHandle(node, handle) { + if(options().handlePositionAngles && options().handleHighlightColor) { + const percentOffset = options().handleHighlightPercentOffset || 1.0; + const hightlightSize = options().handleIcon ? options().handleIcon.width / 2 : hr; + ctx.beginPath(); + ctx.arc( handle.posX, handle.posY, hightlightSize * percentOffset, 0, 2 * Math.PI ); + ctx.closePath(); + + ctx.strokeStyle = options().handleHighlightColor; + ctx.lineWidth = cy.zoom(); + ctx.stroke(); + + drawsClear = false; + } + } + + function deHighlightHandle(node) { + const pos = node.renderedPosition(); + const x = pos.x; + const y = pos.y; + const width = node.renderedOuterWidth(); + const height = node.renderedOuterHeight(); + + ctx.clearRect(x - width, y - height, width * 2, height * 2); + } + cy.ready( function( e ) { lastPanningEnabled = cy.panningEnabled(); lastZoomingEnabled = cy.zoomingEnabled(); @@ -846,7 +1021,7 @@ SOFTWARE. setHandleDimensions( node ); // add new handle - drawHandle(); + drawHandle( node ); node.trigger( 'cyedgehandles.showhandle' ); @@ -865,8 +1040,16 @@ SOFTWARE. return; // sorry, no right clicks allowed } - if( Math.abs( x - hx ) > hrTarget || Math.abs( y - hy ) > hrTarget ) { - return; // only consider this a proper mousedown if on the handle + if (options().handlePosition) { + if( Math.abs( x - hx ) > hrTarget || Math.abs( y - hy ) > hrTarget ) { + return; // only consider this a proper mousedown if on the handle + } + } else { + const hits = hitTest(node, { x: x, y: y }); + if (hits.length < 1) { + return; + } + sourceHandleAngle = hits[0]; } if( inForceStart ) { @@ -882,6 +1065,10 @@ SOFTWARE. sourceNode = node; + hoveredTarget = null; + hoveredTargetHandle = {}; + highlightHandle(sourceNode, sourceHandleAngle); + node.addClass( 'edgehandles-source' ); node.trigger( 'cyedgehandles.start' ); @@ -968,7 +1155,7 @@ SOFTWARE. hoverOut( node ); } - } ).on( 'drag position', 'node', dragNodeHandler = function() { + } ).on( 'drag position', 'node', dragNodeHandler = function(e) { if( drawMode ) { return; } @@ -979,10 +1166,26 @@ SOFTWARE. setTimeout( clearDraws, 50 ); } + if( hoveredTarget ) { + const hits = hitTest( hoveredTarget, node.renderedPosition() ); + + if( hits.length === 0 ) { + deHighlightHandle( hoveredTarget ); + drawHandle( hoveredTarget ); + } else if( hits.length > 0 ) { + deHighlightHandle( hoveredTarget ); + drawHandle( hoveredTarget ); + highlightHandle( hoveredTarget, hits[0] ); + hoveredTargetHandle = hits[0]; + } + } + } ).on( 'grab', 'node', grabHandler = function() { //grabbingNode = true; //setTimeout(function(){ + hoveredTarget = null; + hoveredTargetHandle = {}; clearDraws(); //}, 5); @@ -1248,7 +1451,8 @@ SOFTWARE. .off( 'cxtdrag', cxtdragHandler ) .off( 'cxtdragover', 'node', cxtdragoverHandler ) .off( 'cxtdragout', 'node', cxtdragoutHandler ) - .off( 'tap', 'node', tapToStartHandler ); + .off( 'tap', 'node', tapToStartHandler ) + .off( 'cyedgehandles.registerHandle', registerHandler ); cy.unbind( 'zoom pan', transformHandler ); @@ -1270,6 +1474,15 @@ SOFTWARE. cy.ready( function( e ) { cy.$( '#' + id ).trigger( 'cyedgehandles.forcestart' ); } ); + }, + + /** + * typeName: name to match against node.data().edgeHandleType. + * angles: handle positions in csv format of angle. + * ex) cy.edgehandles('registerHandle', 'left-to-right', '0,90,180'); + */ + registerHandle: function ( typeName, angles ) { + cy.trigger( 'cyedgehandles.registerHandle', [ typeName, angles ]); } }; diff --git a/demo-multi-handle.html b/demo-multi-handle.html new file mode 100644 index 0000000..9307d69 --- /dev/null +++ b/demo-multi-handle.html @@ -0,0 +1,147 @@ + + + + + + cytoscape-edgehandles.js demo + + + + + + + + + + + + + + + + +

cytoscape-edgehandles demo

+ +
+ +
+ + +
+ + + + From 5b5caa2840b83a5d6c872b48bb736683e06494f7 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 5 Jun 2017 16:30:29 -0700 Subject: [PATCH 02/16] Use only handlePosition and parse into angle internally --- README.md | 4 +- cytoscape-edgehandles.js | 221 +++++++++++++++++++++++------------------------ demo-multi-handle.html | 2 +- 3 files changed, 112 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 9133a37..e53f8a8 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ var defaults = { handleOutlineColor: '#000000', // the colour of the handle outline handleOutlineWidth: 0, // the width of the handle outline in pixels handleNodes: 'node', // selector/filter function for whether edges can be made from a given node - handlePosition: 'middle top', // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" - handlePositionAngles: '0,180', // sets the position of the handle in the format of csv of angles (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node). You cannot use handlePosition and handlePositionAngles at the same time. + handlePosition: 'middle top', // sets the position of the handle in the csv format of "X-AXIS Y-AXIS" such as "left top,middle top" OR + angle format "0,180" (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node). handleHighlightColor: '#ff0000', // the colour to highlight handle on hover handleHighlightPercentOffset: '1.0', // percent offset respective to handle size hoverDelay: 150, // time spend over a target node before it is considered a target selection diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index c9c35f9..c4be197 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -177,7 +177,6 @@ SOFTWARE. handleOutlineWidth: 0, // the width of the handle outline in pixels handleNodes: 'node', // selector/filter function for whether edges can be made from a given node handlePosition: 'middle top', // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" - handlePositionAngles: '0,180', // sets the position of the handle in the format of csv of angles (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node) handleHighlightColor: '#ff0000', // the colour to highlight handle on hover handleHighlightPercentOffset: '1.0', // percent offset respective to handle size hoverDelay: 150, // time spend over a target node before it is considered a target selection @@ -248,12 +247,7 @@ SOFTWARE. return; } - var options = data.options; - if (name === 'handlePosition') { - options.handlePositionAngles = ''; - } else if (name === 'handlePositionAngles') { - options.handlePosition = ''; - } + options.handlePosition = parseHandlePosition(options.handlePosition); if( value === undefined ) { if( typeof name == typeof {} ) { @@ -305,7 +299,7 @@ SOFTWARE. var mdownOnHandle = false; var grabbingNode = false; var inForceStart = false; - var hx, hy, hr; + var hr; var mx, my; var hoverTimeout; var drawsClear = true; @@ -314,18 +308,24 @@ SOFTWARE. var sourceNode; var drawMode = false; var pxRatio; - var handleAngles = []; + var registeredHandleAngles = []; var sourceHandleAngle; var hoveredTarget; var hoveredTargetHandle = {}; - if (params.handlePosition) { - opts.handlePositionAngles = ''; - } else if (params.handlePositionAngles) { - opts.handlePosition = ''; - } else { - opts.handlePositionAngles = ''; - } + const positionXYToAngle = { + "right middle": 0, + "right top": 45, + "middle top": 90, + "left top": 135, + "left middle": 180, + "left bottom": 225, + "middle bottom": 270, + "right bottom": 315, + "middle middle": -1 + }; + + opts.handlePosition = parseHandlePosition(params.handlePosition) function getDevicePixelRatio(){ return window.devicePixelRatio || 1; @@ -395,12 +395,12 @@ SOFTWARE. } ); var registerHandler; - cy.on('cyedgehandles.registerHandle', registerHandler = function ( event, typeName, angles ) { - handleAngles.push({ + cy.on( 'cyedgehandles.registerHandle', registerHandler = function ( event, typeName, angles ) { + registeredHandleAngles.push({ name: typeName, - angles: angles + angles: parseHandlePosition( angles ) }); - }); + } ); var ctx = $canvas[ 0 ].getContext( '2d' ); @@ -508,15 +508,46 @@ SOFTWARE. } - function drawHandle(node) { - if (options().handlePosition) { - drawHandleForPosition(hx, hy); - } else if (node) { - drawHandleForAngles(node); + function parseHandlePosition( positions ) { + return positions.split(',').reduce( function ( prev, item ) { + if(isNaN( parseInt(item)) ) { + const sanitized = sanitizePositionFormat( item ); + const angle = positionXYToAngle[sanitized]; + if(angle !== undefined) { + prev.push( angle ); + } + } else { + prev.push( item ); + } + + return prev; + }, [] ).join( ',' ); + } + + function sanitizePositionFormat( pos ) { + const posXY = pos.split( ' ' ); + if( posXY.length !== 2 ) { + return 'middle middle'; + } + + if( posXY[0] !== 'left' && posXY[0] !== 'right' ) { + posXY[0] = 'middle'; + } + + if( posXY[1] !== 'top' && posXY[1] !== 'bottom' ) { + posXY[1] = 'middle'; + } + + return posXY.join( ' ' ); + } + + function drawHandle( node ) { + if( node ) { + drawHandleForAngles( node ); } } - function drawHandleForPosition(hx, hy) { + function drawHandleForPosition( hx, hy ) { ctx.fillStyle = options().handleColor; ctx.strokeStyle = options().handleOutlineColor; @@ -525,95 +556,96 @@ SOFTWARE. ctx.closePath(); ctx.fill(); - if(options().handleOutlineWidth) { + if( options().handleOutlineWidth ) { ctx.lineWidth = options().handleLineWidth * cy.zoom(); ctx.stroke(); } - if(options().handleIcon){ + if( options().handleIcon ){ var icon = options().handleIcon; var width = icon.width*cy.zoom(), height = icon.height*cy.zoom(); - ctx.drawImage(icon, hx-(width/2), hy-(height/2), width, height); + ctx.drawImage( icon, hx-(width/2), hy-(height/2), width, height ); } drawsClear = false; } - function drawHandleForAngles(node) { - const positions = getHandlePositionsForAngle(node); + function drawHandleForAngles( node ) { + const positions = getHandlePositionsForAngle( node ); - positions.forEach(function (position) { + positions.forEach( function(position) { drawHandleForPosition(position.posX, position.posY); - }); + } ); } - function getHandlePositionsForAngle(node) { - var handles = handleAngles.filter(function (handle) { + function getHandlePositionsForAngle( node ) { + var handles = registeredHandleAngles.filter( function(handle) { return handle.name === node.data().edgeHandleType; - }); + } ); + // If there is no match in registeredHandleAngles, use default in handlePosition if (handles.length < 1) { - handles = options().handlePositionAngles; + handles = options().handlePosition; } else { handles = handles[0].angles; } - const angles = handles.split(',').reduce(function (prev, strAngle) { + const angles = handles.split(',').reduce( function(prev, strAngle) { const angle = parseInt(strAngle); if (!isNaN(angle)) { prev.push(angle); } return prev; - }, []); + }, [] ); const nodePos = node.renderedPosition(); const nodeWidth = node.renderedWidth(); const nodeHeight = node.renderedHeight(); - return angles.map(function (angle) { + return angles.map( function(angle) { if (node.style().shape === 'ellipse' || !node.style().shape) { - return getHandleCenterForCircle(angle, nodePos, nodeWidth, nodeHeight); + return getHandleCenterForCircle( angle, nodePos, nodeWidth, nodeHeight ); } else { - return getHandleCenterForRect(angle, nodePos, nodeWidth, nodeHeight); + return getHandleCenterForRect( angle, nodePos, nodeWidth, nodeHeight ); } - }) + } ); } - function getHandleCenterForCircle(angle, pos, width, height) { + function getHandleCenterForCircle( angle, pos, width, height ) { if (angle < 0) { return { angle: angle, posX: pos.x, posY: pos.y }; } - const rad = toRadian(angle); - const posX = pos.x + (width / 2) * Math.cos(rad); - const posY = pos.y - (height / 2) * Math.sin(rad); + const rad = toRadian( angle ); + const posX = pos.x + ( width / 2 ) * Math.cos( rad ); + const posY = pos.y - ( height / 2 ) * Math.sin( rad ); return { angle: angle, posX: posX, posY: posY }; } - function getHandleCenterForRect(angle, pos, width, height) { + function getHandleCenterForRect( angle, pos, width, height ) { if (angle < 0) { return { angle: angle, posX: pos.x, posY: pos.y }; } var posX, posY; - const rad = toRadian(angle); + const rad = toRadian( angle ); - const abssin = Math.abs(Math.sin(rad)); - const abscos = Math.abs(Math.cos(rad)); - const fracMax = 1 / Math.max(abssin, abscos); + const abssin = Math.abs( Math.sin(rad) ); + const abscos = Math.abs( Math.cos(rad) ); + const fracMax = 1 / Math.max( abssin, abscos ); const rectRadiusX = width / 2 * fracMax; const rectRadiusY = height / 2 * fracMax; - posX = pos.x + rectRadiusX * Math.cos(rad); - posY = pos.y - rectRadiusY * Math.sin(rad); + posX = pos.x + rectRadiusX * Math.cos( rad ); + posY = pos.y - rectRadiusY * Math.sin( rad ); return { angle: angle, posX: posX, posY: posY }; } - function toRadian (angle) { - return angle * (Math.PI / 180); + function toRadian( angle ) { + return angle * ( Math.PI / 180 ); } var lineDrawRate = 1000 / 60; @@ -732,17 +764,17 @@ SOFTWARE. return; // nothing to do :( } - const hits = hitTest(targets, { x: mx, y: my }); + const hit = hitTest(targets, { x: mx, y: my }); // just remove preview class if we already have the edges if( !src && !tgt ) { if( !preview && options().preview ) { added = cy.elements( '.edgehandles-preview' ).removeClass( 'edgehandles-preview' ); - if (hits.length < 1) { + if ( !hit ) { added.remove(); } else { - const targetHandleAngle = hits[0]; + const targetHandleAngle = hit; options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); source.trigger( 'cyedgehandles.complete' ); } @@ -906,52 +938,25 @@ SOFTWARE. } function setHandleDimensions( node ){ - var p = node.renderedPosition(); - var h = node.renderedHeight(); - var w = node.renderedWidth(); - hr = options().handleSize / 2 * cy.zoom(); - - if (options().handlePositionAngles) { - return; - } - - // store how much we should move the handle from origin(p.x, p.y) - var moveX = 0; - var moveY = 0; - - // grab axis's - var axisX = options().handlePosition.split(' ')[0].toLowerCase(); - var axisY = options().handlePosition.split(' ')[1].toLowerCase(); - - // based on handlePosition move left/right/top/bottom. Middle/middle will just be normal - if(axisX == 'left') moveX = -(w / 2); - else if(axisX == 'right') moveX = w / 2; - if(axisY == 'top') moveY = -(h / 2); - else if(axisY == 'bottom') moveY = h / 2; - - // set handle x and y based on adjusted positions - hx = p.x + moveX; - hy = p.y + moveY; } function hitTest ( node, touchPos ) { - if (options().handlePosition) { - return []; - } - const handlePositions = getHandlePositionsForAngle(node); const nodePos = node.renderedPosition(); const halfHandleSize = (options().handleIcon ? options().handleIcon.width : hr) * cy.zoom() / 2; - return handlePositions.filter(function (handle) { + const hits = handlePositions.filter(function (handle) { return Math.abs(handle.posX - touchPos.x) <= halfHandleSize && Math.abs(handle.posY - touchPos.y) <= halfHandleSize; }); + + return hits[0]; } - function highlightHandle(node, handle) { - if(options().handlePositionAngles && options().handleHighlightColor) { + function highlightHandle( node, handle ) { + // console.log(node, handle); + if( options().handleHighlightColor ) { const percentOffset = options().handleHighlightPercentOffset || 1.0; const hightlightSize = options().handleIcon ? options().handleIcon.width / 2 : hr; ctx.beginPath(); @@ -1040,17 +1045,11 @@ SOFTWARE. return; // sorry, no right clicks allowed } - if (options().handlePosition) { - if( Math.abs( x - hx ) > hrTarget || Math.abs( y - hy ) > hrTarget ) { - return; // only consider this a proper mousedown if on the handle - } - } else { - const hits = hitTest(node, { x: x, y: y }); - if (hits.length < 1) { - return; - } - sourceHandleAngle = hits[0]; + const hit = hitTest(node, { x: x, y: y }); + if ( !hit ) { + return; } + sourceHandleAngle = hit; if( inForceStart ) { return; // we don't want this going off if we have the forced start to consider @@ -1167,16 +1166,14 @@ SOFTWARE. } if( hoveredTarget ) { - const hits = hitTest( hoveredTarget, node.renderedPosition() ); - - if( hits.length === 0 ) { - deHighlightHandle( hoveredTarget ); - drawHandle( hoveredTarget ); - } else if( hits.length > 0 ) { - deHighlightHandle( hoveredTarget ); - drawHandle( hoveredTarget ); - highlightHandle( hoveredTarget, hits[0] ); - hoveredTargetHandle = hits[0]; + const hit = hitTest( hoveredTarget, node.renderedPosition() ); + + deHighlightHandle( hoveredTarget ); + drawHandle( hoveredTarget ); + + if( hit ) { + highlightHandle( hoveredTarget, hit ); + hoveredTargetHandle = hit; } } diff --git a/demo-multi-handle.html b/demo-multi-handle.html index 9307d69..cd38349 100644 --- a/demo-multi-handle.html +++ b/demo-multi-handle.html @@ -112,7 +112,7 @@ handleNodes: "node", handleSize: 10, handleColor: '#0000ff', - handlePositionAngles: '0,180', + handlePosition: '0,180', handleHighlightPercentOffset: 1.2, edgeType: function(){ return 'flat'; } }); From bfbed7dc6d3cd3104dace7c8ea6305d4df2f18bc Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Tue, 6 Jun 2017 17:05:00 -0700 Subject: [PATCH 03/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e53f8a8..110a3d3 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ var defaults = { handleOutlineWidth: 0, // the width of the handle outline in pixels handleNodes: 'node', // selector/filter function for whether edges can be made from a given node handlePosition: 'middle top', // sets the position of the handle in the csv format of "X-AXIS Y-AXIS" such as "left top,middle top" OR - angle format "0,180" (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node). + // angle format "0,180" (angle: 0,1,...,359,360 where 0 is at right, 180 is at left of node or -1 for middle of the node). handleHighlightColor: '#ff0000', // the colour to highlight handle on hover handleHighlightPercentOffset: '1.0', // percent offset respective to handle size hoverDelay: 150, // time spend over a target node before it is considered a target selection From 7662aa972f8f9a49db85f93df7a1e2d77057ff77 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Tue, 6 Jun 2017 17:08:17 -0700 Subject: [PATCH 04/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 110a3d3..69bd3f0 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ All function can be called via `cy.edgehandles('function-name')`: * `cy.edgehandles('start', 'some-node-id')` : start the handle drag state on node with specified id (e.g. `'some-node-id'`) * `cy.edgehandles('drawon')` : enable draw mode * `cy.edgehandles('drawoff')` : disable draw mode - * `cyedgehandles('registerHandle', 'left-right', '0,180')` : register handle with type name and angles. Each node can be bind to specific handle type by setting edgeHandleType = 'left-right' in node data. If node doesn't have edgeHandleType, values from handlePositionAngles is used. + * `cy.edgehandles('registerHandle', 'left-right', '0,180')` : register a node specific handle with type name and angles. Each node can be bind to specific handle type by setting edgeHandleType = 'left-right' in node data. If node doesn't have edgeHandleType, values from handlePosition is used. ## Publishing instructions From 8daba7db18e77c7c4e2837dbdb9c1b75a77daa31 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Tue, 6 Jun 2017 18:35:50 -0700 Subject: [PATCH 05/16] Fix params --- cytoscape-edgehandles.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index c4be197..f734074 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -247,7 +247,8 @@ SOFTWARE. return; } - options.handlePosition = parseHandlePosition(options.handlePosition); + var options = data.options; + options.handlePosition = parseHandlePosition(data.options.handlePosition); if( value === undefined ) { if( typeof name == typeof {} ) { @@ -325,7 +326,7 @@ SOFTWARE. "middle middle": -1 }; - opts.handlePosition = parseHandlePosition(params.handlePosition) + opts.handlePosition = parseHandlePosition(opts.handlePosition) function getDevicePixelRatio(){ return window.devicePixelRatio || 1; From 959f26999ee7db4c8fe804f51945c287796bffb5 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Tue, 6 Jun 2017 18:47:39 -0700 Subject: [PATCH 06/16] Update cytoscape-edgehandles.js --- cytoscape-edgehandles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index f734074..75041ea 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -874,7 +874,7 @@ SOFTWARE. } if( !preview ) { - const targetHandleAngle = hits[0]; + const targetHandleAngle = hit; options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); source.trigger( 'cyedgehandles.complete' ); } From d33793d9434fada984a296eea4a891c8ede19066 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Fri, 16 Jun 2017 12:00:57 -0700 Subject: [PATCH 07/16] Remove asscociation with data and use function to get handle position --- README.md | 7 +++++++ cytoscape-edgehandles.js | 42 ++++++++++++------------------------------ demo-multi-handle.html | 38 ++++++++++++++++++++++++-------------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 69bd3f0..bb488db 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,13 @@ var defaults = { // NB: i indicates edge index in case of edgeType: 'node' return {}; }, + nodeHandlePosition: function( node ) { + // for handle positions for a specific node. + // set value in handlePosition for default handle positions + // and return a specific positions for node in this function. + // return null to use the default value from handlePosition + return null; + }, start: function( sourceNode ) { // fired when edgehandles interaction starts (drag on handle) }, diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index f734074..6a43327 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -204,6 +204,13 @@ SOFTWARE. // NB: i indicates edge index in case of edgeType: 'node' return {}; }, + nodeHandlePosition: function( node ) { + // for handle positions for a specific node. + // set value in handlePosition for default handle positions + // and return a specific positions for node in this function. + // return null to use the default value from handlePosition + return null; + }, start: function( sourceNode ) { // fired when edgehandles interaction starts (drag on handle) }, @@ -309,7 +316,6 @@ SOFTWARE. var sourceNode; var drawMode = false; var pxRatio; - var registeredHandleAngles = []; var sourceHandleAngle; var hoveredTarget; var hoveredTargetHandle = {}; @@ -395,14 +401,6 @@ SOFTWARE. cy.autoungrabify( prevUngrabifyState ); } ); - var registerHandler; - cy.on( 'cyedgehandles.registerHandle', registerHandler = function ( event, typeName, angles ) { - registeredHandleAngles.push({ - name: typeName, - angles: parseHandlePosition( angles ) - }); - } ); - var ctx = $canvas[ 0 ].getContext( '2d' ); // write options to data @@ -580,16 +578,10 @@ SOFTWARE. } function getHandlePositionsForAngle( node ) { - var handles = registeredHandleAngles.filter( function(handle) { - return handle.name === node.data().edgeHandleType; - } ); - - // If there is no match in registeredHandleAngles, use default in handlePosition - if (handles.length < 1) { - handles = options().handlePosition; - } else { - handles = handles[0].angles; - } + const strHandles = options().nodeHandlePosition( node ); + // If there are specific handle positions is set for this node, use that + // Otherwise, use default handle positions. + const handles = strHandles ? parseHandlePosition( strHandles ) : options().handlePosition; const angles = handles.split(',').reduce( function(prev, strAngle) { const angle = parseInt(strAngle); @@ -1449,8 +1441,7 @@ SOFTWARE. .off( 'cxtdrag', cxtdragHandler ) .off( 'cxtdragover', 'node', cxtdragoverHandler ) .off( 'cxtdragout', 'node', cxtdragoutHandler ) - .off( 'tap', 'node', tapToStartHandler ) - .off( 'cyedgehandles.registerHandle', registerHandler ); + .off( 'tap', 'node', tapToStartHandler ); cy.unbind( 'zoom pan', transformHandler ); @@ -1472,15 +1463,6 @@ SOFTWARE. cy.ready( function( e ) { cy.$( '#' + id ).trigger( 'cyedgehandles.forcestart' ); } ); - }, - - /** - * typeName: name to match against node.data().edgeHandleType. - * angles: handle positions in csv format of angle. - * ex) cy.edgehandles('registerHandle', 'left-to-right', '0,90,180'); - */ - registerHandle: function ( typeName, angles ) { - cy.trigger( 'cyedgehandles.registerHandle', [ typeName, angles ]); } }; diff --git a/demo-multi-handle.html b/demo-multi-handle.html index cd38349..a6c89b6 100644 --- a/demo-multi-handle.html +++ b/demo-multi-handle.html @@ -89,9 +89,9 @@ elements: { nodes: [ { data: { id: 'j', name: 'Jerry' } }, - { data: { id: 'e', name: 'Elaine', edgeHandleType: 'twoleft-tworight' } }, - { data: { id: 'k', name: 'Kramer', edgeHandleType: 'twoleft-right' } }, - { data: { id: 'g', name: 'George', edgeHandleType: 'left' } } + { data: { id: 'e', name: 'Elaine' } }, + { data: { id: 'k', name: 'Kramer' } }, + { data: { id: 'g', name: 'George' } } ], edges: [ { data: { source: 'j', target: 'e' } }, @@ -108,13 +108,27 @@ }); cy.edgehandles({ - toggleOffOnLeave: true, - handleNodes: "node", - handleSize: 10, - handleColor: '#0000ff', - handlePosition: '0,180', - handleHighlightPercentOffset: 1.2, - edgeType: function(){ return 'flat'; } + toggleOffOnLeave: true, + handleNodes: "node", + handleSize: 10, + handleColor: '#0000ff', + handlePosition: '0,180', + handleHighlightPercentOffset: 1.2, + edgeType: function(){ return 'flat'; }, + nodeHandlePosition: function(node) { + var data = node.data(); + + switch (data.id) { + case 'k': + return '0,150,210'; + + case 'g': + return '30,150,210,330'; + + default: + return null; + } + } }); document.querySelector('#draw-on').addEventListener('click', function(){ @@ -124,10 +138,6 @@ document.querySelector('#draw-off').addEventListener('click', function(){ cy.edgehandles('drawoff'); }); - - cy.edgehandles('registerHandle', 'left', '180'); - cy.edgehandles('registerHandle', 'twoleft-right', '150,210,0'); - cy.edgehandles('registerHandle', 'twoleft-tworight', '30,150,210,330'); }); From 8aa1342400ec68d220d2aa751dd54c4c27dc0b20 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 10 Jul 2017 15:35:09 -0700 Subject: [PATCH 08/16] Fix draw mode hover --- cytoscape-edgehandles.js | 36 +++++++++++++++++++++++++----------- demo-multi-handle.html | 5 +++-- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index b8de65b..830a50a 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -569,14 +569,14 @@ SOFTWARE. } function drawHandleForAngles( node ) { - const positions = getHandlePositionsForAngle( node ); + const positions = getHandlePositionsForNode( node ); positions.forEach( function(position) { drawHandleForPosition(position.posX, position.posY); } ); } - function getHandlePositionsForAngle( node ) { + function getHandlePositionsForNode( node ) { const strHandles = options().nodeHandlePosition( node ); // If there are specific handle positions is set for this node, use that // Otherwise, use default handle positions. @@ -653,14 +653,19 @@ SOFTWARE. ctx.lineWidth = options().handleLineWidth; } + const positions = getHandlePositionsForNode(sourceNode); + const pos = positions.filter(function( position ) { + return position.angle === sourceHandleAngle.angle; + })[0]; + const hx = pos.x; + const hy = pos.y; + // draw line based on type switch( options().handleLineType ) { case 'ghost': if( !ghostNode || ghostNode.removed() ) { - drawHandle(); - ghostNode = cy.add( { group: 'nodes', classes: 'edgehandles-ghost edgehandles-ghost-node', @@ -718,7 +723,8 @@ SOFTWARE. } ctx.beginPath(); - ctx.moveTo( hx, hy ); + + ctx.moveTo( pos.x, pos.y ); for( var i = 0; i < linePoints.length; i++ ) { var pt = linePoints[ i ]; @@ -934,7 +940,7 @@ SOFTWARE. } function hitTest ( node, touchPos ) { - const handlePositions = getHandlePositionsForAngle(node); + const handlePositions = getHandlePositionsForNode(node); const nodePos = node.renderedPosition(); const halfHandleSize = (options().handleIcon ? options().handleIcon.width : hr) * cy.zoom() / 2; @@ -1096,8 +1102,17 @@ SOFTWARE. my = y; if( options().handleLineType !== 'ghost' ) { - clearDraws(); - drawHandle(); + if( hoveredTarget ) { + const hit = hitTest( hoveredTarget, {x: mx, y: my} ); + + deHighlightHandle( hoveredTarget ); + drawHandle( hoveredTarget ); + + if( hit ) { + highlightHandle( hoveredTarget, hit ); + hoveredTargetHandle = hit; + } + } } drawLine( x, y ); @@ -1204,7 +1219,7 @@ SOFTWARE. setHandleDimensions( node ); - drawHandle(); + drawHandle( node ); node.trigger( 'cyedgehandles.showhandle' ); @@ -1354,7 +1369,7 @@ SOFTWARE. setHandleDimensions( node ); - drawHandle(); + drawHandle( node ); node.trigger( 'cyedgehandles.showhandle' ); @@ -1432,7 +1447,6 @@ SOFTWARE. // node.trigger( 'cyedgehandles.showhandle' ); // }, 16 ); // } - } ); diff --git a/demo-multi-handle.html b/demo-multi-handle.html index a6c89b6..bfa3624 100644 --- a/demo-multi-handle.html +++ b/demo-multi-handle.html @@ -6,8 +6,8 @@ cytoscape-edgehandles.js demo - - + + @@ -113,6 +113,7 @@ handleSize: 10, handleColor: '#0000ff', handlePosition: '0,180', + handleLineType: 'draw', handleHighlightPercentOffset: 1.2, edgeType: function(){ return 'flat'; }, nodeHandlePosition: function(node) { From a64744fc0da88170863eca6c6f1c1e63c13e4f7d Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Thu, 13 Jul 2017 16:03:15 -0700 Subject: [PATCH 09/16] Fix drawline with straight --- cytoscape-edgehandles.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index 830a50a..26bc845 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -564,8 +564,6 @@ SOFTWARE. var width = icon.width*cy.zoom(), height = icon.height*cy.zoom(); ctx.drawImage( icon, hx-(width/2), hy-(height/2), width, height ); } - - drawsClear = false; } function drawHandleForAngles( node ) { @@ -574,6 +572,8 @@ SOFTWARE. positions.forEach( function(position) { drawHandleForPosition(position.posX, position.posY); } ); + + drawsClear = false; } function getHandlePositionsForNode( node ) { @@ -653,12 +653,12 @@ SOFTWARE. ctx.lineWidth = options().handleLineWidth; } - const positions = getHandlePositionsForNode(sourceNode); - const pos = positions.filter(function( position ) { - return position.angle === sourceHandleAngle.angle; + const handles = getHandlePositionsForNode(sourceNode); + const sourceHandle = handles.filter(function( handle ) { + return handle.angle === sourceHandleAngle.angle; })[0]; - const hx = pos.x; - const hy = pos.y; + const hx = sourceHandle.posX; + const hy = sourceHandle.posY; // draw line based on type switch( options().handleLineType ) { @@ -724,7 +724,7 @@ SOFTWARE. ctx.beginPath(); - ctx.moveTo( pos.x, pos.y ); + ctx.moveTo( hx, hy ); for( var i = 0; i < linePoints.length; i++ ) { var pt = linePoints[ i ]; @@ -1102,6 +1102,9 @@ SOFTWARE. my = y; if( options().handleLineType !== 'ghost' ) { + clearDraws(); + drawHandle( sourceNode ); + if( hoveredTarget ) { const hit = hitTest( hoveredTarget, {x: mx, y: my} ); From 053272c445e2237a726a6d2347adf0efe266ba07 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Thu, 13 Jul 2017 16:19:14 -0700 Subject: [PATCH 10/16] Disable returning angles on complete when toggleOffOnLeave is false --- README.md | 3 +-- cytoscape-edgehandles.js | 12 ++++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bb488db..a5b5e7c 100644 --- a/README.md +++ b/README.md @@ -172,8 +172,7 @@ All function can be called via `cy.edgehandles('function-name')`: * `cy.edgehandles('start', 'some-node-id')` : start the handle drag state on node with specified id (e.g. `'some-node-id'`) * `cy.edgehandles('drawon')` : enable draw mode * `cy.edgehandles('drawoff')` : disable draw mode - * `cy.edgehandles('registerHandle', 'left-right', '0,180')` : register a node specific handle with type name and angles. Each node can be bind to specific handle type by setting edgeHandleType = 'left-right' in node data. If node doesn't have edgeHandleType, values from handlePosition is used. - + ## Publishing instructions diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index 26bc845..bc47762 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -773,7 +773,11 @@ SOFTWARE. added.remove(); } else { const targetHandleAngle = hit; - options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); + if (options().toggleOffOnLeave) { + options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); + } else { + options().complete( source, targets, added ); + } source.trigger( 'cyedgehandles.complete' ); } @@ -872,7 +876,11 @@ SOFTWARE. if( !preview ) { const targetHandleAngle = hit; - options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); + if (options().toggleOffOnLeave) { + options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); + } else { + options().complete( source, targets, added ); + } source.trigger( 'cyedgehandles.complete' ); } } From 63a8bd364a87b0f13757f393c3ac0fe83a830085 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 31 Jul 2017 16:58:49 -0700 Subject: [PATCH 11/16] Make edge creation on hit handle as option --- README.md | 2 ++ cytoscape-edgehandles.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5369df0..f2595f0 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ var defaults = { cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: true, // whether to start the plugin in the enabled state toggleOffOnLeave: false, // whether an edge is cancelled by leaving a node (true), or whether you need to go over again to cancel (false; allows multiple edges in one pass) + addEdgeOnHitHandle: false, // whether an edge is created on drag end only on edge handle (true), or anywhere on node (false) + // addEdgeOnHitHandle true only works with toggleOffOnLeave true edgeType: function( sourceNode, targetNode ) { // can return 'flat' for flat edges between nodes or 'node' for intermediate node between them // returning null/undefined means an edge can't be added between the two nodes diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index fd972fb..258ecaa 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -184,6 +184,8 @@ SOFTWARE. cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: true, // whether to start the plugin in the enabled state toggleOffOnLeave: false, // whether an edge is cancelled by leaving a node (true), or whether you need to go over again to cancel (false; allows multiple edges in one pass) + addEdgeOnHitHandle: false, // whether an edge is created on drag end only on edge handle (true), or anywhere on node (false) + // addEdgeOnHitHandle true only works with toggleOffOnLeave true edgeType: function( sourceNode, targetNode ) { // can return 'flat' for flat edges between nodes or 'node' for intermediate node between them // returning null/undefined means an edge can't be added between the two nodes @@ -259,6 +261,10 @@ SOFTWARE. var options = data.options; options.handlePosition = parseHandlePosition(data.options.handlePosition); + if( opts.addEdgeOnHitHandle ) { + options.toggleOffOnLeave = true; + } + if( value === undefined ) { if( typeof name == typeof {} ) { var newOpts = name; @@ -333,6 +339,10 @@ SOFTWARE. opts.handlePosition = parseHandlePosition(opts.handlePosition) + if( opts.addEdgeOnHitHandle ) { + opts.toggleOffOnLeave = true; + } + function getDevicePixelRatio(){ return window.devicePixelRatio || 1; } @@ -769,10 +779,11 @@ SOFTWARE. if( !preview && options().preview ) { added = cy.elements( '.edgehandles-preview' ).removeClass( 'edgehandles-preview' ); - if ( !hit ) { + if ( !hit && options().addEdgeOnHitHandle) { added.remove(); } else { const targetHandleAngle = hit; + // TODO: Make it possible to track source/target angle when toggleOffOnLeave is false if (options().toggleOffOnLeave) { options().complete( source, targets, added, sourceHandleAngle.angle, targetHandleAngle.angle ); } else { From 7f8f055a7c02a3f574510a859f8a3bf27e9e390d Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 7 Aug 2017 15:51:42 -0700 Subject: [PATCH 12/16] Add highlight option --- README.md | 2 ++ cytoscape-edgehandles.js | 24 ++++++++++++++---------- demo-multi-handle.html | 7 +++---- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f2595f0..95f7c78 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ var defaults = { toggleOffOnLeave: false, // whether an edge is cancelled by leaving a node (true), or whether you need to go over again to cancel (false; allows multiple edges in one pass) addEdgeOnHitHandle: false, // whether an edge is created on drag end only on edge handle (true), or anywhere on node (false) // addEdgeOnHitHandle true only works with toggleOffOnLeave true + highlightHandleOnHover: false, // whether a handle should be highlighted on hover (true), or not (false) + // highlightHandleOnHover true only works with toggleOffOnLeave true edgeType: function( sourceNode, targetNode ) { // can return 'flat' for flat edges between nodes or 'node' for intermediate node between them // returning null/undefined means an edge can't be added between the two nodes diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index 258ecaa..2ff1134 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -184,6 +184,8 @@ SOFTWARE. cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: true, // whether to start the plugin in the enabled state toggleOffOnLeave: false, // whether an edge is cancelled by leaving a node (true), or whether you need to go over again to cancel (false; allows multiple edges in one pass) + highlightHandleOnHover: false, // whether a handle should be highlighted on hover (true), or not (false) + // highlightHandleOnHover true only works with toggleOffOnLeave true addEdgeOnHitHandle: false, // whether an edge is created on drag end only on edge handle (true), or anywhere on node (false) // addEdgeOnHitHandle true only works with toggleOffOnLeave true edgeType: function( sourceNode, targetNode ) { @@ -261,7 +263,7 @@ SOFTWARE. var options = data.options; options.handlePosition = parseHandlePosition(data.options.handlePosition); - if( opts.addEdgeOnHitHandle ) { + if( opts.addEdgeOnHitHandle || opts.highlightHandleOnHover ) { options.toggleOffOnLeave = true; } @@ -339,7 +341,7 @@ SOFTWARE. opts.handlePosition = parseHandlePosition(opts.handlePosition) - if( opts.addEdgeOnHitHandle ) { + if( opts.addEdgeOnHitHandle || opts.highlightHandleOnHover ){ opts.toggleOffOnLeave = true; } @@ -973,7 +975,7 @@ SOFTWARE. function highlightHandle( node, handle ) { // console.log(node, handle); - if( options().handleHighlightColor ) { + if( options().highlightHandleOnHover && options().handleHighlightColor ) { const percentOffset = options().handleHighlightPercentOffset || 1.0; const hightlightSize = options().handleIcon ? options().handleIcon.width / 2 : hr; ctx.beginPath(); @@ -989,13 +991,15 @@ SOFTWARE. } function deHighlightHandle(node) { - const pos = node.renderedPosition(); - const x = pos.x; - const y = pos.y; - const width = node.renderedOuterWidth(); - const height = node.renderedOuterHeight(); - - ctx.clearRect(x - width, y - height, width * 2, height * 2); + if( options().highlightHandleOnHover ) { + const pos = node.renderedPosition(); + const x = pos.x; + const y = pos.y; + const width = node.renderedOuterWidth(); + const height = node.renderedOuterHeight(); + + ctx.clearRect(x - width, y - height, width * 2, height * 2); + } } cy.ready( function() { diff --git a/demo-multi-handle.html b/demo-multi-handle.html index 413548a..00f7df7 100644 --- a/demo-multi-handle.html +++ b/demo-multi-handle.html @@ -108,18 +108,17 @@ }); cy.edgehandles({ - toggleOffOnLeave: false, + toggleOffOnLeave: true, handleNodes: "node", handleSize: 10, handleColor: '#0000ff', handlePosition: '0,180', handleLineType: 'ghost', preview: true, + addEdgeOnHitHandle: true, + highlightHandleOnHover: true, handleHighlightPercentOffset: 1.2, edgeType: function(){ return 'flat'; }, - complete: function (a, b, c, d, e) { - console.log(a, b, d, e); - }, nodeHandlePosition: function(node) { var data = node.data(); From 35d6c875e231a5cddd27310031cbcb55e6ac3a3b Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 7 Aug 2017 16:13:39 -0700 Subject: [PATCH 13/16] Small lint fix --- cytoscape-edgehandles.js | 105 +++++++++++++++++++++++------------------------ package.json | 2 + 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index 2ff1134..43b7658 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -237,6 +237,51 @@ SOFTWARE. var fn = params; var container = cy.container(); + const positionXYToAngle = { + "right middle": 0, + "right top": 45, + "middle top": 90, + "left top": 135, + "left middle": 180, + "left bottom": 225, + "middle bottom": 270, + "right bottom": 315, + "middle middle": -1 + }; + + function sanitizePositionFormat( pos ) { + const posXY = pos.split( ' ' ); + if( posXY.length !== 2 ) { + return 'middle middle'; + } + + if( posXY[0] !== 'left' && posXY[0] !== 'right' ) { + posXY[0] = 'middle'; + } + + if( posXY[1] !== 'top' && posXY[1] !== 'bottom' ) { + posXY[1] = 'middle'; + } + + return posXY.join( ' ' ); + } + + function parseHandlePosition( positions ) { + return positions.split(',').reduce( function ( prev, item ) { + if(isNaN( parseInt(item)) ) { + const sanitized = sanitizePositionFormat( item ); + const angle = positionXYToAngle[sanitized]; + if(angle !== undefined) { + prev.push( angle ); + } + } else { + prev.push( item ); + } + + return prev; + }, [] ).join( ',' ); + } + var functions = { destroy: function() { var $container = $( this ); @@ -263,7 +308,7 @@ SOFTWARE. var options = data.options; options.handlePosition = parseHandlePosition(data.options.handlePosition); - if( opts.addEdgeOnHitHandle || opts.highlightHandleOnHover ) { + if( options.addEdgeOnHitHandle || options.highlightHandleOnHover ) { options.toggleOffOnLeave = true; } @@ -327,19 +372,7 @@ SOFTWARE. var hoveredTarget; var hoveredTargetHandle = {}; - const positionXYToAngle = { - "right middle": 0, - "right top": 45, - "middle top": 90, - "left top": 135, - "left middle": 180, - "left bottom": 225, - "middle bottom": 270, - "right bottom": 315, - "middle middle": -1 - }; - - opts.handlePosition = parseHandlePosition(opts.handlePosition) + opts.handlePosition = parseHandlePosition(opts.handlePosition); if( opts.addEdgeOnHitHandle || opts.highlightHandleOnHover ){ opts.toggleOffOnLeave = true; @@ -518,39 +551,6 @@ SOFTWARE. } - function parseHandlePosition( positions ) { - return positions.split(',').reduce( function ( prev, item ) { - if(isNaN( parseInt(item)) ) { - const sanitized = sanitizePositionFormat( item ); - const angle = positionXYToAngle[sanitized]; - if(angle !== undefined) { - prev.push( angle ); - } - } else { - prev.push( item ); - } - - return prev; - }, [] ).join( ',' ); - } - - function sanitizePositionFormat( pos ) { - const posXY = pos.split( ' ' ); - if( posXY.length !== 2 ) { - return 'middle middle'; - } - - if( posXY[0] !== 'left' && posXY[0] !== 'right' ) { - posXY[0] = 'middle'; - } - - if( posXY[1] !== 'top' && posXY[1] !== 'bottom' ) { - posXY[1] = 'middle'; - } - - return posXY.join( ' ' ); - } - function drawHandle( node ) { if( node ) { drawHandleForAngles( node ); @@ -956,13 +956,12 @@ SOFTWARE. } } - function setHandleDimensions( node ){ + function setHandleDimensions(){ hr = options().handleSize / 2 * cy.zoom(); } function hitTest ( node, touchPos ) { const handlePositions = getHandlePositionsForNode(node); - const nodePos = node.renderedPosition(); const halfHandleSize = (options().handleIcon ? options().handleIcon.width : hr) * cy.zoom() / 2; const hits = handlePositions.filter(function (handle) { @@ -1039,7 +1038,7 @@ SOFTWARE. // remove old handle clearDraws(); - setHandleDimensions( node ); + setHandleDimensions(); // add new handle drawHandle( node ); @@ -1082,7 +1081,7 @@ SOFTWARE. hoveredTarget = null; hoveredTargetHandle = {}; - highlightHandle(sourceNode, sourceHandleAngle); + highlightHandle( sourceNode, sourceHandleAngle ); node.addClass( 'edgehandles-source' ); node.trigger( 'cyedgehandles.start' ); @@ -1243,7 +1242,7 @@ SOFTWARE. var h = node.renderedOuterHeight(); var w = node.renderedOuterWidth(); - setHandleDimensions( node ); + setHandleDimensions(); drawHandle( node ); @@ -1393,7 +1392,7 @@ SOFTWARE. node.trigger( 'cyedgehandles.start' ); node.addClass( 'edgehandles-source' ); - setHandleDimensions( node ); + setHandleDimensions(); drawHandle( node ); diff --git a/package.json b/package.json index 90584f2..bda0df3 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,11 @@ "devDependencies": { "gulp": "^3.8.8", "gulp-eslint": "^3.0.1", + "gulp-jshint": "^2.0.4", "gulp-prompt": "^0.1.1", "gulp-replace": "^0.4.0", "gulp-shell": "^0.2.9", + "jshint": "^2.9.5", "jshint-stylish": "^1.0.0", "run-sequence": "^1.0.0" }, From 3ba53a8a04a5c68bb6732ef3a17400ceacb8d050 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 7 Aug 2017 16:28:46 -0700 Subject: [PATCH 14/16] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 95f7c78..7373c09 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ var defaults = { }, complete: function( sourceNode, targetNodes, addedEntities, sourceHandleAngle, targetHandleAngle ) { // fired when edgehandles is done and entities are added + // sourceHandleAngle, targetHandleAngle are only returned when toggleOffOnLeave is true }, stop: function( sourceNode ) { // fired when edgehandles interaction is stopped (either complete with added edges or incomplete) From 589b2cb303a353bf3705506aad52c1a28ef34a07 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 7 Aug 2017 16:29:25 -0700 Subject: [PATCH 15/16] Update cytoscape-edgehandles.js --- cytoscape-edgehandles.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cytoscape-edgehandles.js b/cytoscape-edgehandles.js index 43b7658..f53d007 100644 --- a/cytoscape-edgehandles.js +++ b/cytoscape-edgehandles.js @@ -221,6 +221,7 @@ SOFTWARE. }, complete: function( sourceNode, targetNodes, addedEntities, sourceHandleAngle, targetHandleAngle ) { // fired when edgehandles is done and entities are added + // sourceHandleAngle, targetHandleAngle are only returned when toggleOffOnLeave is true }, stop: function( sourceNode ) { // fired when edgehandles interaction is stopped (either complete with added edges or incomplete) From 2e33e69828f7bb9fd9cae65a23c085985a72b7f0 Mon Sep 17 00:00:00 2001 From: Emma Suzuki Date: Mon, 7 Aug 2017 16:32:22 -0700 Subject: [PATCH 16/16] Change to space --- demo-multi-handle.html | 310 ++++++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/demo-multi-handle.html b/demo-multi-handle.html index 00f7df7..e313431 100644 --- a/demo-multi-handle.html +++ b/demo-multi-handle.html @@ -2,160 +2,160 @@ - - cytoscape-edgehandles.js demo - - - - - - - - - - - - - - - - -

cytoscape-edgehandles demo

- -
- -
- - -
- - + + cytoscape-edgehandles.js demo + + + + + + + + + + + + + + + + +

cytoscape-edgehandles demo

+ +
+ +
+ + +
+ +