From 0367ec594de1e9be6f96a840871cf2410d147afd Mon Sep 17 00:00:00 2001 From: metincansiper Date: Tue, 14 Feb 2017 20:19:37 +0300 Subject: [PATCH 01/37] Added move by arrow keys property (by @leonarddrv) --- cytoscape-node-resize.js | 170 ++++++++++++++++++++++++++++++++++++++- undoable_demo.html | 125 ++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 undoable_demo.html diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 68d856b..d0ff020 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -633,6 +633,120 @@ drawGrapples(nodeToDrawGrapples); } }; + + function getTopMostNodes(nodes) { + var nodesMap = {}; + for (var i = 0; i < nodes.length; i++) { + nodesMap[nodes[i].id()] = true; + } + var roots = nodes.filter(function (i, ele) { + var parent = ele.parent()[0]; + while(parent != null){ + if(nodesMap[parent.id()]){ + return false; + } + parent = parent.parent()[0]; + } + return true; + }); + + return roots; + } + + function moveNodes(positionDiff, nodes, notCalcTopMostNodes) { + var topMostNodes = notCalcTopMostNodes?nodes:getTopMostNodes(nodes); + for (var i = 0; i < topMostNodes.length; i++) { + var node = topMostNodes[i]; + var oldX = node.position("x"); + var oldY = node.position("y"); + node.position({ + x: oldX + positionDiff.x, + y: oldY + positionDiff.y + }); + var children = node.children(); + moveNodes(positionDiff, children, true); + } + } + + var selectedNodesToMove; + //var selectedNodesPosition; + var nodesMoving = false; + + function keyDown(e) { + //e = e || window.event; + if (e.keyCode < '37' || e.keyCode > '40') { + return; + } + + if (!nodesMoving) + { + selectedNodesToMove = cy.nodes(':selected'); + cy.trigger("noderesize.movestart", [selectedNodesToMove]); + nodesMoving = true; + } + if (e.ctrlKey && e.which == '38') { + // up arrow and ctrl + moveNodes ({x:0, y:-1},selectedNodesToMove); + } + else if (e.ctrlKey && e.which == '40') { + // down arrow and ctrl + moveNodes ({x:0, y:1},selectedNodesToMove); + } + else if (e.ctrlKey && e.which == '37') { + // left arrow and ctrl + moveNodes ({x:-1, y:0},selectedNodesToMove); + } + else if (e.ctrlKey && e.which == '39') { + // right arrow and ctrl + moveNodes ({x:1, y:0},selectedNodesToMove); + } + + else if (e.shiftKey && e.which == '38') { + // up arrow and shift + moveNodes ({x:0, y:-10},selectedNodesToMove); + } + else if (e.shiftKey && e.which == '40') { + // down arrow and shift + moveNodes ({x:0, y:10},selectedNodesToMove); + } + else if (e.shiftKey && e.which == '37') { + // left arrow and shift + moveNodes ({x:-10, y:0},selectedNodesToMove); + + } + else if (e.shiftKey && e.which == '39' ) { + // right arrow and shift + moveNodes ({x:10, y:0},selectedNodesToMove); + } + + else if (e.keyCode == '38') { + // up arrow + moveNodes ({x:0, y:-3},selectedNodesToMove); + } + else if (e.keyCode == '40') { + // down arrow + moveNodes ({x:0, y:3},selectedNodesToMove); + } + else if (e.keyCode == '37') { + // left arrow + moveNodes ({x:-3, y:0},selectedNodesToMove); + } + else if (e.keyCode == '39') { + //right arrow + moveNodes ({x:3, y:0},selectedNodesToMove); + } + } + + function keyUp(e) { + if (e.keyCode < '37' || e.keyCode > '40') { + return; + } + //undo redo part goes here + cy.trigger("noderesize.moveend", [selectedNodesToMove]); + selectedNodesToMove = undefined; + //selectedNodesPosition = undefined; + nodesMoving = false; + } var unBindEvents = function() { cy.off("unselect", "node", eUnselectNode); @@ -730,13 +844,16 @@ refreshGrapples(); } }); - //cy.on("style", "node", redraw); + + document.addEventListener("keydown",keyDown, true); + document.addEventListener("keyup",keyUp, true); }; bindEvents(); if (cy.undoRedo && options.undoable) { var param; + var moveparam; cy.on("noderesize.resizestart", function (e, type, node) { param = { @@ -754,6 +871,32 @@ cy.undoRedo().do("resize", param); param = undefined; }); + + cy.on("noderesize.movestart", function (e, nodes) { + + moveparam = { + firstTime : true, + firstNodePosition: { + x: nodes[0].position('x'), + y: nodes[0].position('y') + }, + nodes: nodes + } + }); + + cy.on("noderesize.moveend", function (e, nodes) { + var initialPos = moveparam.firstNodePosition; + + moveparam.positionDiff = { + x: -nodes[0].position('x') + initialPos.x, + y: -nodes[0].position('y') + initialPos.y + } + + delete moveparam.firstNodePosition; + + cy.undoRedo().do("noderesize.move", moveparam); + moveparam = undefined; + }); var resizeDo = function (arg) { if (arg.firstTime) { @@ -780,8 +923,33 @@ return result; }; + + var moveDo = function (arg) { + if (arg.firstTime) { + delete arg.firstTime; + return arg; + } + + var nodes = arg.nodes; + + var positionDiff = arg.positionDiff; + + var result = { + nodes: nodes, + positionDiff: { + x: -positionDiff.x, + y: -positionDiff.y + } + }; + + + moveNodes (positionDiff,nodes); + + return result; + }; cy.undoRedo().action("resize", resizeDo, resizeDo); + cy.undoRedo().action("noderesize.move", moveDo, moveDo); } diff --git a/undoable_demo.html b/undoable_demo.html new file mode 100644 index 0000000..d41cc8a --- /dev/null +++ b/undoable_demo.html @@ -0,0 +1,125 @@ + + + + + + cytoscape-node-resize.js demo + + + + + + + + + + + + + + + + + + + +

cytoscape-node-resize demo

+ +
+ + + + From aff40af7acbcef9aebe4d56119a3b5e8904fde3e Mon Sep 17 00:00:00 2001 From: leonarddrv Date: Wed, 15 Feb 2017 16:40:04 +0300 Subject: [PATCH 02/37] Scrolling problem on browser is fixed --- cytoscape-node-resize.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index d0ff020..98d527b 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -671,8 +671,20 @@ var selectedNodesToMove; //var selectedNodesPosition; var nodesMoving = false; - + + var keys = {}; function keyDown(e) { + + + keys[e.keyCode] = true; + switch(e.keyCode){ + case 37: case 39: case 38: case 40: // Arrow keys + case 32: e.preventDefault(); break; // Space + default: break; // do not block other keys + } + + + //e = e || window.event; if (e.keyCode < '37' || e.keyCode > '40') { return; @@ -845,8 +857,19 @@ } }); + /*var keys = {}; + window.addEventListener("keydown", + function(e){ + keys[e.keyCode] = true; + switch(e.keyCode){ + case 37: case 39: case 38: case 40: // Arrow keys + case 32: e.preventDefault(); break; // Space + default: break; // do not block other keys + } + }, + false);*/ document.addEventListener("keydown",keyDown, true); - document.addEventListener("keyup",keyUp, true); + document.addEventListener("keyup",keyUp, true); }; bindEvents(); @@ -972,4 +995,4 @@ register(cytoscape, jQuery); } -})(); \ No newline at end of file +})(); From 9767461a2e567252ad18bc321cb1a1e5d97798dc Mon Sep 17 00:00:00 2001 From: metincansiper Date: Mon, 20 Feb 2017 16:03:46 +0300 Subject: [PATCH 03/37] Performance improvement in moveNodes function (This increases the performance in moving nodes by keys). --- cytoscape-node-resize.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 98d527b..f5e971e 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -653,19 +653,19 @@ return roots; } - function moveNodes(positionDiff, nodes, notCalcTopMostNodes) { - var topMostNodes = notCalcTopMostNodes?nodes:getTopMostNodes(nodes); - for (var i = 0; i < topMostNodes.length; i++) { - var node = topMostNodes[i]; - var oldX = node.position("x"); - var oldY = node.position("y"); - node.position({ - x: oldX + positionDiff.x, - y: oldY + positionDiff.y - }); - var children = node.children(); - moveNodes(positionDiff, children, true); - } + function moveNodes(positionDiff, nodes) { + // Get the descendants of top most nodes. Note that node.position() can move just the simple nodes. + var topMostNodes = getTopMostNodes(nodes); + var nodesToMove = topMostNodes.union(topMostNodes.descendants()); + + nodesToMove.positions(function(i, node) { + var oldX = node.position("x"); + var oldY = node.position("y"); + return { + x: oldX + positionDiff.x, + y: oldY + positionDiff.y + }; + }); } var selectedNodesToMove; From 04f79a349e7e8402c630669e130160eaa9b7b2ad Mon Sep 17 00:00:00 2001 From: leonarddrv Date: Tue, 21 Feb 2017 22:49:48 +0300 Subject: [PATCH 04/37] Ctrl function in moving nodes is replaced with Alt --- cytoscape-node-resize.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index f5e971e..45d1245 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -696,20 +696,20 @@ cy.trigger("noderesize.movestart", [selectedNodesToMove]); nodesMoving = true; } - if (e.ctrlKey && e.which == '38') { - // up arrow and ctrl + if (e.altKey && e.which == '38') { + // up arrow and alt moveNodes ({x:0, y:-1},selectedNodesToMove); } - else if (e.ctrlKey && e.which == '40') { - // down arrow and ctrl + else if (e.altKey && e.which == '40') { + // down arrow and alt moveNodes ({x:0, y:1},selectedNodesToMove); } - else if (e.ctrlKey && e.which == '37') { - // left arrow and ctrl + else if (e.altKey && e.which == '37') { + // left arrow and alt moveNodes ({x:-1, y:0},selectedNodesToMove); } - else if (e.ctrlKey && e.which == '39') { - // right arrow and ctrl + else if (e.altKey && e.which == '39') { + // right arrow and alt moveNodes ({x:1, y:0},selectedNodesToMove); } From 31cdf5302fedb521ef458fc2210d2862345bf54a Mon Sep 17 00:00:00 2001 From: leonarddrv Date: Tue, 28 Feb 2017 13:52:20 +0300 Subject: [PATCH 05/37] Arrow movement in textarea/input is fixed --- cytoscape-node-resize.js | 908 +++++++++++++++++++-------------------- 1 file changed, 448 insertions(+), 460 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 45d1245..437578d 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -1,252 +1,252 @@ ;(function () { 'use strict'; - + var debounce = (function(){ - /** - * lodash 3.1.1 (Custom Build) - * Build: `lodash modern modularize exports="npm" -o ./` - * Copyright 2012-2015 The Dojo Foundation - * Based on Underscore.js 1.8.3 - * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ - /** Used as the `TypeError` message for "Functions" methods. */ - var FUNC_ERROR_TEXT = 'Expected a function'; - - /* Native method references for those with the same name as other `lodash` methods. */ - var nativeMax = Math.max, - nativeNow = Date.now; - - /** - * Gets the number of milliseconds that have elapsed since the Unix epoch - * (1 January 1970 00:00:00 UTC). - * - * @static - * @memberOf _ - * @category Date - * @example - * - * _.defer(function(stamp) { + /** + * lodash 3.1.1 (Custom Build) + * Build: `lodash modern modularize exports="npm" -o ./` + * Copyright 2012-2015 The Dojo Foundation + * Based on Underscore.js 1.8.3 + * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + + /* Native method references for those with the same name as other `lodash` methods. */ + var nativeMax = Math.max, + nativeNow = Date.now; + + /** + * Gets the number of milliseconds that have elapsed since the Unix epoch + * (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @category Date + * @example + * + * _.defer(function(stamp) { * console.log(_.now() - stamp); * }, _.now()); - * // => logs the number of milliseconds it took for the deferred function to be invoked - */ - var now = nativeNow || function() { - return new Date().getTime(); - }; - - /** - * Creates a debounced function that delays invoking `func` until after `wait` - * milliseconds have elapsed since the last time the debounced function was - * invoked. The debounced function comes with a `cancel` method to cancel - * delayed invocations. Provide an options object to indicate that `func` - * should be invoked on the leading and/or trailing edge of the `wait` timeout. - * Subsequent calls to the debounced function return the result of the last - * `func` invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked - * on the trailing edge of the timeout only if the the debounced function is - * invoked more than once during the `wait` timeout. - * - * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) - * for details over the differences between `_.debounce` and `_.throttle`. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to debounce. - * @param {number} [wait=0] The number of milliseconds to delay. - * @param {Object} [options] The options object. - * @param {boolean} [options.leading=false] Specify invoking on the leading - * edge of the timeout. - * @param {number} [options.maxWait] The maximum time `func` is allowed to be - * delayed before it's invoked. - * @param {boolean} [options.trailing=true] Specify invoking on the trailing - * edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // avoid costly calculations while the window size is in flux - * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); - * - * // invoke `sendMail` when the click event is fired, debouncing subsequent calls - * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { + * // => logs the number of milliseconds it took for the deferred function to be invoked + */ + var now = nativeNow || function() { + return new Date().getTime(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed invocations. Provide an options object to indicate that `func` + * should be invoked on the leading and/or trailing edge of the `wait` timeout. + * Subsequent calls to the debounced function return the result of the last + * `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked + * on the trailing edge of the timeout only if the the debounced function is + * invoked more than once during the `wait` timeout. + * + * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=false] Specify invoking on the leading + * edge of the timeout. + * @param {number} [options.maxWait] The maximum time `func` is allowed to be + * delayed before it's invoked. + * @param {boolean} [options.trailing=true] Specify invoking on the trailing + * edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // avoid costly calculations while the window size is in flux + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // invoke `sendMail` when the click event is fired, debouncing subsequent calls + * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })); - * - * // ensure `batchLog` is invoked once after 1 second of debounced calls - * var source = new EventSource('/stream'); - * jQuery(source).on('message', _.debounce(batchLog, 250, { + * + * // ensure `batchLog` is invoked once after 1 second of debounced calls + * var source = new EventSource('/stream'); + * jQuery(source).on('message', _.debounce(batchLog, 250, { * 'maxWait': 1000 * })); - * - * // cancel a debounced call - * var todoChanges = _.debounce(batchLog, 1000); - * Object.observe(models.todo, todoChanges); - * - * Object.observe(models, function(changes) { + * + * // cancel a debounced call + * var todoChanges = _.debounce(batchLog, 1000); + * Object.observe(models.todo, todoChanges); + * + * Object.observe(models, function(changes) { * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { * todoChanges.cancel(); * } * }, ['delete']); - * - * // ...at some point `models.todo` is changed - * models.todo.completed = true; - * - * // ...before 1 second has passed `models.todo` is deleted - * // which cancels the debounced `todoChanges` call - * delete models.todo; - */ - function debounce(func, wait, options) { - var args, - maxTimeoutId, - result, - stamp, - thisArg, - timeoutId, - trailingCall, - lastCalled = 0, - maxWait = false, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - wait = wait < 0 ? 0 : (+wait || 0); - if (options === true) { - var leading = true; - trailing = false; - } else if (isObject(options)) { - leading = !!options.leading; - maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - - function cancel() { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (maxTimeoutId) { - clearTimeout(maxTimeoutId); - } - lastCalled = 0; - maxTimeoutId = timeoutId = trailingCall = undefined; - } - - function complete(isCalled, id) { - if (id) { - clearTimeout(id); - } - maxTimeoutId = timeoutId = trailingCall = undefined; - if (isCalled) { - lastCalled = now(); - result = func.apply(thisArg, args); - if (!timeoutId && !maxTimeoutId) { - args = thisArg = undefined; + * + * // ...at some point `models.todo` is changed + * models.todo.completed = true; + * + * // ...before 1 second has passed `models.todo` is deleted + * // which cancels the debounced `todoChanges` call + * delete models.todo; + */ + function debounce(func, wait, options) { + var args, + maxTimeoutId, + result, + stamp, + thisArg, + timeoutId, + trailingCall, + lastCalled = 0, + maxWait = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = wait < 0 ? 0 : (+wait || 0); + if (options === true) { + var leading = true; + trailing = false; + } else if (isObject(options)) { + leading = !!options.leading; + maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); + trailing = 'trailing' in options ? !!options.trailing : trailing; } - } - } - function delayed() { - var remaining = wait - (now() - stamp); - if (remaining <= 0 || remaining > wait) { - complete(trailingCall, maxTimeoutId); - } else { - timeoutId = setTimeout(delayed, remaining); - } - } + function cancel() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (maxTimeoutId) { + clearTimeout(maxTimeoutId); + } + lastCalled = 0; + maxTimeoutId = timeoutId = trailingCall = undefined; + } - function maxDelayed() { - complete(trailing, timeoutId); - } + function complete(isCalled, id) { + if (id) { + clearTimeout(id); + } + maxTimeoutId = timeoutId = trailingCall = undefined; + if (isCalled) { + lastCalled = now(); + result = func.apply(thisArg, args); + if (!timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + } + } - function debounced() { - args = arguments; - stamp = now(); - thisArg = this; - trailingCall = trailing && (timeoutId || !leading); - - if (maxWait === false) { - var leadingCall = leading && !timeoutId; - } else { - if (!maxTimeoutId && !leading) { - lastCalled = stamp; + function delayed() { + var remaining = wait - (now() - stamp); + if (remaining <= 0 || remaining > wait) { + complete(trailingCall, maxTimeoutId); + } else { + timeoutId = setTimeout(delayed, remaining); + } } - var remaining = maxWait - (stamp - lastCalled), - isCalled = remaining <= 0 || remaining > maxWait; - - if (isCalled) { - if (maxTimeoutId) { - maxTimeoutId = clearTimeout(maxTimeoutId); - } - lastCalled = stamp; - result = func.apply(thisArg, args); + + function maxDelayed() { + complete(trailing, timeoutId); } - else if (!maxTimeoutId) { - maxTimeoutId = setTimeout(maxDelayed, remaining); + + function debounced() { + args = arguments; + stamp = now(); + thisArg = this; + trailingCall = trailing && (timeoutId || !leading); + + if (maxWait === false) { + var leadingCall = leading && !timeoutId; + } else { + if (!maxTimeoutId && !leading) { + lastCalled = stamp; + } + var remaining = maxWait - (stamp - lastCalled), + isCalled = remaining <= 0 || remaining > maxWait; + + if (isCalled) { + if (maxTimeoutId) { + maxTimeoutId = clearTimeout(maxTimeoutId); + } + lastCalled = stamp; + result = func.apply(thisArg, args); + } + else if (!maxTimeoutId) { + maxTimeoutId = setTimeout(maxDelayed, remaining); + } + } + if (isCalled && timeoutId) { + timeoutId = clearTimeout(timeoutId); + } + else if (!timeoutId && wait !== maxWait) { + timeoutId = setTimeout(delayed, wait); + } + if (leadingCall) { + isCalled = true; + result = func.apply(thisArg, args); + } + if (isCalled && !timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + return result; } - } - if (isCalled && timeoutId) { - timeoutId = clearTimeout(timeoutId); - } - else if (!timeoutId && wait !== maxWait) { - timeoutId = setTimeout(delayed, wait); - } - if (leadingCall) { - isCalled = true; - result = func.apply(thisArg, args); - } - if (isCalled && !timeoutId && !maxTimeoutId) { - args = thisArg = undefined; - } - return result; + debounced.cancel = cancel; + return debounced; } - debounced.cancel = cancel; - return debounced; - } - - /** - * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. - * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(1); - * // => false - */ - function isObject(value) { - // Avoid a V8 JIT bug in Chrome 19-20. - // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. - var type = typeof value; - return !!value && (type == 'object' || type == 'function'); - } - - return debounce; + + /** + * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // Avoid a V8 JIT bug in Chrome 19-20. + // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + return debounce; })(); - + // registers the extension on a cytoscape lib ref var register = function (cytoscape, $) { - // can't register if required libraries does not exist + // can't register if required libraries does not exist // note that oCanvas is not parametrezid here because it is not commonjs nor amd compatible // it is expected to be defined as a browser global if (!cytoscape || !$ || !oCanvas) { return; - } - + } + var canvas; var options = { @@ -292,21 +292,21 @@ cytoscape('core', 'nodeResize', function (opts) { var cy = this; // Nodes to draw grapples this variable is set if there is just one selected node - var nodeToDrawGrapples; - // We need to keep the number of selected nodes to check if we should draw grapples. + var nodeToDrawGrapples; + // We need to keep the number of selected nodes to check if we should draw grapples. // Calculating it each time decreases performance. var numberOfSelectedNodes; // Events to bind and unbind var eUnselectNode, ePositionNode, eZoom, ePan, eSelectNode, eRemoveNode, eAddNode, eFreeNode; - + // Initilize nodes to draw grapples and the number of selected nodes { - var selectedNodes = cy.nodes(':selected'); - numberOfSelectedNodes = selectedNodes.length; + var selectedNodes = cy.nodes(':selected'); + numberOfSelectedNodes = selectedNodes.length; - if (numberOfSelectedNodes === 1) { - nodeToDrawGrapples = selectedNodes[0]; - } + if (numberOfSelectedNodes === 1) { + nodeToDrawGrapples = selectedNodes[0]; + } } options = $.extend(true, options, opts); @@ -318,48 +318,48 @@ // Resize the canvas var sizeCanvas = debounce( function(){ $canvas - .attr('height', $container.height()) - .attr('width', $container.width()) - .css({ - 'position': 'absolute', - 'top': 0, - 'left': 0, - 'z-index': '999' - }) + .attr('height', $container.height()) + .attr('width', $container.width()) + .css({ + 'position': 'absolute', + 'top': 0, + 'left': 0, + 'z-index': '999' + }) ; setTimeout(function () { - var canvasBb = $canvas.offset(); - var containerBb = $container.offset(); + var canvasBb = $canvas.offset(); + var containerBb = $container.offset(); + + $canvas + .css({ + 'top': -(canvasBb.top - containerBb.top), + 'left': -(canvasBb.left - containerBb.left) + }) + ; + + // If there is a previously created canvas destroy it and reset the canvas + if (canvas) { + canvas.destroy(); + } + // See if old canvas is destroyed + canvas = oCanvas.create({ + canvas: "#node-resize" + }); - $canvas - .css({ - 'top': -(canvasBb.top - containerBb.top), - 'left': -(canvasBb.left - containerBb.left) - }) - ; - - // If there is a previously created canvas destroy it and reset the canvas - if (canvas) { - canvas.destroy(); - } - // See if old canvas is destroyed - canvas = oCanvas.create({ - canvas: "#node-resize" - }); - - // redraw on canvas resize - if(cy){ - refreshGrapples(); - } + // redraw on canvas resize + if(cy){ + refreshGrapples(); + } }, 0); - }, 250 ); + }, 250 ); - sizeCanvas(); + sizeCanvas(); $(window).on('resize', sizeCanvas); - + oCanvas.registerDisplayObject("dashedRectangle", function (settings, core) { @@ -394,10 +394,10 @@ var clearDrawing = function () { // reset the canvas canvas.reset(); - + // Normally canvas.reset() should clear the drawings as well. - // It works as expected id windows is never resized however if it is resized the drawings are not cleared unexpectedly. - // Therefore we need to access the canvas and clear the rectangle (Note that canvas.clear(false) does not work as expected + // It works as expected id windows is never resized however if it is resized the drawings are not cleared unexpectedly. + // Therefore we need to access the canvas and clear the rectangle (Note that canvas.clear(false) does not work as expected // as well so wee need to do it manually.) TODO: Figure out the bug clearly and file it to oCanvas library. var w = $container.width(); var h = $container.height(); @@ -566,7 +566,7 @@ startPos.x = x; startPos.y = y; - + cy.trigger("noderesize.resizedrag", [t, node]); }; @@ -626,138 +626,137 @@ var refreshGrapples = function () { clearDrawing(); - + // If the node to draw grapples is defined it means that there is just one node selected and // we need to draw grapples for that node. if(nodeToDrawGrapples) { drawGrapples(nodeToDrawGrapples); } }; - + function getTopMostNodes(nodes) { - var nodesMap = {}; - for (var i = 0; i < nodes.length; i++) { - nodesMap[nodes[i].id()] = true; - } - var roots = nodes.filter(function (i, ele) { - var parent = ele.parent()[0]; - while(parent != null){ - if(nodesMap[parent.id()]){ - return false; - } - parent = parent.parent()[0]; - } - return true; - }); - - return roots; - } - - function moveNodes(positionDiff, nodes) { + var nodesMap = {}; + for (var i = 0; i < nodes.length; i++) { + nodesMap[nodes[i].id()] = true; + } + var roots = nodes.filter(function (i, ele) { + var parent = ele.parent()[0]; + while(parent != null){ + if(nodesMap[parent.id()]){ + return false; + } + parent = parent.parent()[0]; + } + return true; + }); + + return roots; + } + + function moveNodes(positionDiff, nodes) { // Get the descendants of top most nodes. Note that node.position() can move just the simple nodes. var topMostNodes = getTopMostNodes(nodes); var nodesToMove = topMostNodes.union(topMostNodes.descendants()); - - nodesToMove.positions(function(i, node) { - var oldX = node.position("x"); - var oldY = node.position("y"); - return { - x: oldX + positionDiff.x, - y: oldY + positionDiff.y - }; + + nodesToMove.positions(function(i, node) { + var oldX = node.position("x"); + var oldY = node.position("y"); + return { + x: oldX + positionDiff.x, + y: oldY + positionDiff.y + }; }); - } - - var selectedNodesToMove; - //var selectedNodesPosition; - var nodesMoving = false; - - var keys = {}; - function keyDown(e) { - - - keys[e.keyCode] = true; - switch(e.keyCode){ - case 37: case 39: case 38: case 40: // Arrow keys - case 32: e.preventDefault(); break; // Space - default: break; // do not block other keys - } - - - - //e = e || window.event; - if (e.keyCode < '37' || e.keyCode > '40') { - return; - } - - if (!nodesMoving) - { - selectedNodesToMove = cy.nodes(':selected'); - cy.trigger("noderesize.movestart", [selectedNodesToMove]); - nodesMoving = true; - } - if (e.altKey && e.which == '38') { - // up arrow and alt - moveNodes ({x:0, y:-1},selectedNodesToMove); - } - else if (e.altKey && e.which == '40') { - // down arrow and alt - moveNodes ({x:0, y:1},selectedNodesToMove); - } - else if (e.altKey && e.which == '37') { - // left arrow and alt - moveNodes ({x:-1, y:0},selectedNodesToMove); - } - else if (e.altKey && e.which == '39') { - // right arrow and alt - moveNodes ({x:1, y:0},selectedNodesToMove); - } - - else if (e.shiftKey && e.which == '38') { - // up arrow and shift - moveNodes ({x:0, y:-10},selectedNodesToMove); - } - else if (e.shiftKey && e.which == '40') { - // down arrow and shift - moveNodes ({x:0, y:10},selectedNodesToMove); - } - else if (e.shiftKey && e.which == '37') { - // left arrow and shift - moveNodes ({x:-10, y:0},selectedNodesToMove); - - } - else if (e.shiftKey && e.which == '39' ) { - // right arrow and shift - moveNodes ({x:10, y:0},selectedNodesToMove); - } - - else if (e.keyCode == '38') { - // up arrow - moveNodes ({x:0, y:-3},selectedNodesToMove); - } - else if (e.keyCode == '40') { - // down arrow - moveNodes ({x:0, y:3},selectedNodesToMove); - } - else if (e.keyCode == '37') { - // left arrow - moveNodes ({x:-3, y:0},selectedNodesToMove); - } - else if (e.keyCode == '39') { - //right arrow - moveNodes ({x:3, y:0},selectedNodesToMove); - } - } - - function keyUp(e) { + } + + var selectedNodesToMove; + var nodesMoving = false; + + var keys = {}; + function keyDown(e) { + //Checks if the tagname is textarea or input + var tn = document.activeElement.tagName; + if (tn != "TEXTAREA" && tn != "INPUT") + { + keys[e.keyCode] = true; + switch(e.keyCode){ + case 37: case 39: case 38: case 40: // Arrow keys + case 32: e.preventDefault(); break; // Space + default: break; // do not block other keys + } + + + if (e.keyCode < '37' || e.keyCode > '40') { + return; + } + + if (!nodesMoving) + { + selectedNodesToMove = cy.nodes(':selected'); + cy.trigger("noderesize.movestart", [selectedNodesToMove]); + nodesMoving = true; + } + if (e.altKey && e.which == '38') { + // up arrow and alt + moveNodes ({x:0, y:-1},selectedNodesToMove); + } + else if (e.altKey && e.which == '40') { + // down arrow and alt + moveNodes ({x:0, y:1},selectedNodesToMove); + } + else if (e.altKey && e.which == '37') { + // left arrow and alt + moveNodes ({x:-1, y:0},selectedNodesToMove); + } + else if (e.altKey && e.which == '39') { + // right arrow and alt + moveNodes ({x:1, y:0},selectedNodesToMove); + } + + else if (e.shiftKey && e.which == '38') { + // up arrow and shift + moveNodes ({x:0, y:-10},selectedNodesToMove); + } + else if (e.shiftKey && e.which == '40') { + // down arrow and shift + moveNodes ({x:0, y:10},selectedNodesToMove); + } + else if (e.shiftKey && e.which == '37') { + // left arrow and shift + moveNodes ({x:-10, y:0},selectedNodesToMove); + + } + else if (e.shiftKey && e.which == '39' ) { + // right arrow and shift + moveNodes ({x:10, y:0},selectedNodesToMove); + } + + else if (e.keyCode == '38') { + // up arrow + moveNodes ({x:0, y:-3},selectedNodesToMove); + } + else if (e.keyCode == '40') { + // down arrow + moveNodes ({x:0, y:3},selectedNodesToMove); + } + else if (e.keyCode == '37') { + // left arrow + moveNodes ({x:-3, y:0},selectedNodesToMove); + } + else if (e.keyCode == '39') { + //right arrow + moveNodes ({x:3, y:0},selectedNodesToMove); + } + } + } + + function keyUp(e) { if (e.keyCode < '37' || e.keyCode > '40') { return; } - //undo redo part goes here + cy.trigger("noderesize.moveend", [selectedNodesToMove]); selectedNodesToMove = undefined; - //selectedNodesPosition = undefined; - nodesMoving = false; + nodesMoving = false; } var unBindEvents = function() { @@ -775,18 +774,18 @@ var bindEvents = function() { cy.on("unselect", "node", eUnselectNode = function() { numberOfSelectedNodes = numberOfSelectedNodes - 1; - + if (numberOfSelectedNodes === 1) { - var selectedNodes = cy.nodes(':selected'); + var selectedNodes = cy.nodes(':selected'); - // If user unselects all nodes by tapping to the core etc. then our 'numberOfSelectedNodes' - // may be misleading. Therefore we need to check if the number of nodes to draw grapples is really 1 here. - if (selectedNodes.length === 1) { - nodeToDrawGrapples = selectedNodes[0]; - } - else { - nodeToDrawGrapples = undefined; - } + // If user unselects all nodes by tapping to the core etc. then our 'numberOfSelectedNodes' + // may be misleading. Therefore we need to check if the number of nodes to draw grapples is really 1 here. + if (selectedNodes.length === 1) { + nodeToDrawGrapples = selectedNodes[0]; + } + else { + nodeToDrawGrapples = undefined; + } } else { nodeToDrawGrapples = undefined; @@ -794,7 +793,7 @@ refreshGrapples(); }); - + cy.on("select", "node", eSelectNode = function() { var node = this; @@ -808,7 +807,7 @@ } refreshGrapples(); }); - + cy.on("remove", "node", eRemoveNode = function() { var node = this; // If a selected node is removed we should regard this event just like an unselect event @@ -816,7 +815,7 @@ eUnselectNode(); } }); - + cy.on("add", "node", eAddNode = function() { var node = this; // If a selected node is added we should regard this event just like a select event @@ -824,52 +823,41 @@ eSelectNode(); } }); - + cy.on("position", "node", ePositionNode = function() { var node = this; if ( nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id() ) { refreshGrapples(); } }); - + /* * Interestingly when a node is positioned programatically 'position' event is triggered for its ancestors as well if their position changed. * However it is not triggered for them when the node is freed. Therefore we need to handle "free" case and check if the nodeToGrapples * is an anchestor of the freed node. */ cy.on("free", "node", eFreeNode =function() { - var node = this; - - if( nodeToDrawGrapples && nodeToDrawGrapples.id() !== node.id() && node.ancestors(":selected").id() == nodeToDrawGrapples.id() ) { - refreshGrapples(); - } + var node = this; + + if( nodeToDrawGrapples && nodeToDrawGrapples.id() !== node.id() && node.ancestors(":selected").id() == nodeToDrawGrapples.id() ) { + refreshGrapples(); + } }); - + cy.on("zoom", eZoom = function() { if ( nodeToDrawGrapples ) { - refreshGrapples(); + refreshGrapples(); } }); - + cy.on("pan", ePan = function() { - if ( nodeToDrawGrapples ) { - refreshGrapples(); - } + if ( nodeToDrawGrapples ) { + refreshGrapples(); + } }); - - /*var keys = {}; - window.addEventListener("keydown", - function(e){ - keys[e.keyCode] = true; - switch(e.keyCode){ - case 37: case 39: case 38: case 40: // Arrow keys - case 32: e.preventDefault(); break; // Space - default: break; // do not block other keys - } - }, - false);*/ + document.addEventListener("keydown",keyDown, true); - document.addEventListener("keyup",keyUp, true); + document.addEventListener("keyup",keyUp, true); }; bindEvents(); @@ -888,47 +876,47 @@ position: $.extend({}, node.position()) }; }); - + cy.on("noderesize.resizeend", function (e, type, node) { param.firstTime = true; cy.undoRedo().do("resize", param); param = undefined; }); - + cy.on("noderesize.movestart", function (e, nodes) { - - moveparam = { - firstTime : true, - firstNodePosition: { - x: nodes[0].position('x'), - y: nodes[0].position('y') - }, - nodes: nodes - } + + moveparam = { + firstTime : true, + firstNodePosition: { + x: nodes[0].position('x'), + y: nodes[0].position('y') + }, + nodes: nodes + } }); - - cy.on("noderesize.moveend", function (e, nodes) { - var initialPos = moveparam.firstNodePosition; - - moveparam.positionDiff = { - x: -nodes[0].position('x') + initialPos.x, - y: -nodes[0].position('y') + initialPos.y - } - - delete moveparam.firstNodePosition; - + + cy.on("noderesize.moveend", function (e, nodes) { + var initialPos = moveparam.firstNodePosition; + + moveparam.positionDiff = { + x: -nodes[0].position('x') + initialPos.x, + y: -nodes[0].position('y') + initialPos.y + } + + delete moveparam.firstNodePosition; + cy.undoRedo().do("noderesize.move", moveparam); moveparam = undefined; - }); + }); var resizeDo = function (arg) { if (arg.firstTime) { delete arg.firstTime; return arg; } - + var node = arg.node; - + var result = { node: node, css: { @@ -937,37 +925,37 @@ }, position: $.extend({}, node.position()) }; - + node.position(arg.position) - .css("width", arg.css.width) - .css("height", arg.css.height); - + .css("width", arg.css.width) + .css("height", arg.css.height); + refreshGrapples(); // refresh grapplers after node resize - + return result; }; - + var moveDo = function (arg) { if (arg.firstTime) { delete arg.firstTime; return arg; } - + var nodes = arg.nodes; - - var positionDiff = arg.positionDiff; - + + var positionDiff = arg.positionDiff; + var result = { nodes: nodes, positionDiff: { - x: -positionDiff.x, - y: -positionDiff.y - } + x: -positionDiff.x, + y: -positionDiff.y + } }; - - - moveNodes (positionDiff,nodes); - + + + moveNodes (positionDiff,nodes); + return result; }; From f9d38a67f73343a9e7391358161df36cc321bfd4 Mon Sep 17 00:00:00 2001 From: metincansiper Date: Fri, 24 Mar 2017 15:33:50 +0300 Subject: [PATCH 06/37] Revision for https://github.com/iVis-at-Bilkent/newt/issues/28 --- cytoscape-node-resize.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 437578d..dddfaec 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -639,7 +639,10 @@ for (var i = 0; i < nodes.length; i++) { nodesMap[nodes[i].id()] = true; } - var roots = nodes.filter(function (i, ele) { + var roots = nodes.filter(function (ele, i) { + if(typeof ele === "number") { + ele = i; + } var parent = ele.parent()[0]; while(parent != null){ if(nodesMap[parent.id()]){ From 0a31481e4483334ee29f5919c8be5e18292c37a9 Mon Sep 17 00:00:00 2001 From: kinimesi Date: Mon, 27 Mar 2017 19:07:23 +0300 Subject: [PATCH 07/37] Fixes #12 iVis-at-Bilkent/chise.js/issues/95 --- cytoscape-node-resize.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index dddfaec..e62c86a 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -826,25 +826,25 @@ eSelectNode(); } }); + + // declare old and current positions + var oldPos = {x: undefined, y: undefined}; + var currentPos = {x : 0, y : 0}; + // listens for position event and refreshGrapples if necessary cy.on("position", "node", ePositionNode = function() { var node = this; - if ( nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id() ) { + // if position of selected node or compound changes refreshGrapples + if (nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id()){ refreshGrapples(); } - }); - - /* - * Interestingly when a node is positioned programatically 'position' event is triggered for its ancestors as well if their position changed. - * However it is not triggered for them when the node is freed. Therefore we need to handle "free" case and check if the nodeToGrapples - * is an anchestor of the freed node. - */ - cy.on("free", "node", eFreeNode =function() { - var node = this; - - if( nodeToDrawGrapples && nodeToDrawGrapples.id() !== node.id() && node.ancestors(":selected").id() == nodeToDrawGrapples.id() ) { + // if the position of compund changes by repositioning its children's + // Note: position event for compound is not triggered in this case + else if (nodeToDrawGrapples && (currentPos.x != oldPos.x || currentPos.y != oldPos.y)){ + currentPos = nodeToDrawGrapples.position(); refreshGrapples(); - } + oldPos = {x : currentPos.x, y : currentPos.y}; + }; }); cy.on("zoom", eZoom = function() { From 7591e63672ed7e884db713fdbcc31a1e1cf98c2d Mon Sep 17 00:00:00 2001 From: kinimesi Date: Mon, 27 Mar 2017 19:50:17 +0300 Subject: [PATCH 08/37] Reinitialize compound position variables on unselect --- cytoscape-node-resize.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index e62c86a..3726cc1 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -776,6 +776,10 @@ var bindEvents = function() { cy.on("unselect", "node", eUnselectNode = function() { + // reinitialize old and current compound positions + oldPos = {x: undefined, y: undefined}; + currentPos = {x: 0, y: 0}; + numberOfSelectedNodes = numberOfSelectedNodes - 1; if (numberOfSelectedNodes === 1) { From 098f839faa4c569e384ec1bf3e0375b23e885e9e Mon Sep 17 00:00:00 2001 From: metincansiper Date: Mon, 17 Apr 2017 23:36:42 +0300 Subject: [PATCH 09/37] A bug fix related to cy.js v3 compatibality --- cytoscape-node-resize.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 3726cc1..7569ec3 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -661,7 +661,10 @@ var topMostNodes = getTopMostNodes(nodes); var nodesToMove = topMostNodes.union(topMostNodes.descendants()); - nodesToMove.positions(function(i, node) { + nodesToMove.positions(function(node, i) { + if(typeof node === "number") { + node = i; + } var oldX = node.position("x"); var oldY = node.position("y"); return { From 59c233c9921c6ff123981764b3e420fc000dde88 Mon Sep 17 00:00:00 2001 From: metincansiper Date: Thu, 27 Apr 2017 14:38:53 +0300 Subject: [PATCH 10/37] Added setWith and setHeight options, which are expected to be defined as functions. These functions will be called by the extension internally to set the node dimensions. --- cytoscape-node-resize.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 7569ec3..81068c4 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -270,7 +270,18 @@ var data = node.data("resizeMinHeight"); return data ? data : 15; }, // a function returns min height of node - + + // These optional function will be executed to set the width/height of a node in this extension + // Using node.css() is not a recommended way (http://js.cytoscape.org/#eles.style) to do this. Therefore, overriding these defaults + // so that a data field or something like that will be used to set node dimentions instead of directly calling node.css() + // is highly recommended (Of course this will require a proper setting in the stylesheet). + setWidth: function(node, width) { + node.css('width', width); + }, + setHeight: function(node, height) { + node.css('height', height); + }, + isFixedAspectRatioResizeMode: function (node) { return node.is(".fixedAspectRatioResizeMode") },// with only 4 active grapples (at corners) isNoResizeMode: function (node) { return node.is(".noResizeMode, :parent") }, // no active grapples @@ -544,23 +555,23 @@ if (t.startsWith("top")) { if (node.height() - xHeight > options.minHeight(node)) { node.position("y", nodePos.y + xHeight / 2); - node.css("height", node.height() - xHeight); + options.setHeight(node, node.height() - xHeight); } else if (isAspectedMode) return; } else if (t.startsWith("bottom")) { if (node.height() + xHeight > options.minHeight(node)) { node.position("y", nodePos.y + xHeight / 2); - node.css("height", node.height() + xHeight); + options.setHeight(node, node.height() + xHeight); } else if (isAspectedMode) return; } if (t.endsWith("left") && node.width() - xWidth > options.minWidth(node)) { node.position("x", nodePos.x + xWidth / 2); - node.css("width", node.width() - xWidth); + options.setWidth(node, node.width() - xWidth); } else if (t.endsWith("right") && node.width() + xWidth > options.minWidth(node)) { node.position("x", nodePos.x + xWidth / 2); - node.css("width", node.width() + xWidth); + options.setWidth(node, node.width() + xWidth); } }); @@ -936,9 +947,9 @@ position: $.extend({}, node.position()) }; - node.position(arg.position) - .css("width", arg.css.width) - .css("height", arg.css.height); + node.position(arg.position); + options.setWidth(node, arg.css.width); + options.setHeight(node, arg.css.height); refreshGrapples(); // refresh grapplers after node resize From 5bab2c2c379b88c41e879f3ba7535ebfc300f9e6 Mon Sep 17 00:00:00 2001 From: metincansiper Date: Thu, 27 Apr 2017 14:39:42 +0300 Subject: [PATCH 11/37] Updated README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index d1dc91f..f9e4fc0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,17 @@ Only consists of initilization & default options. var data = node.data("resizeMinHeight"); return data ? data : 15; }, // a function returns min height of node + + // These optional function will be executed to set the width/height of a node in this extension + // Using node.css() is not a recommended way (http://js.cytoscape.org/#eles.style) to do this. Therefore, overriding these defaults + // so that a data field or something like that will be used to set node dimentions instead of directly calling node.css() + // is highly recommended (Of course this will require a proper setting in the stylesheet). + setWidth: function(node, width) { + node.css('width', width); + }, + setHeight: function(node, height) { + node.css('height', height); + }, isFixedAspectRatioResizeMode: function (node) { return node.is(".fixedAspectRatioResizeMode") },// with only 4 active grapples (at corners) isNoResizeMode: function (node) { return node.is(".noResizeMode, :parent") }, // no active grapples From 2b4406387cda4daa899cc98a33fccf255b02086a Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Thu, 1 Jun 2017 11:47:15 +0200 Subject: [PATCH 12/37] separate functions for drawing active and inactive grapple, prevent some unncessary redraw --- cytoscape-node-resize.js | 94 ++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 81068c4..7b7e752 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -425,53 +425,54 @@ return options.padding*Math.max(1, cy.zoom()); }; - var drawGrapple = function (x, y, t, node, cur) { - if (options.isNoResizeMode(node) || (options.isFixedAspectRatioResizeMode(node) && t.indexOf("center") >= 0)) { - var inactiveGrapple = canvas.display.rectangle({ - x: x, - y: y, - height: getGrappleSize(node), - width: getGrappleSize(node), - stroke: options.inactiveGrappleStroke - }); + var drawInactiveGrapple = function (x, y, t, node) { + var inactiveGrapple = canvas.display.rectangle({ + x: x, + y: y, + height: getGrappleSize(node), + width: getGrappleSize(node), + stroke: options.inactiveGrappleStroke + }); - canvas.addChild(inactiveGrapple); + canvas.addChild(inactiveGrapple, false); - var eMouseEnter = function () { - canvas.mouse.cursor(options.cursors.inactive); - inactiveGrapple.bind("touchleave mouseleave", eMouseLeave); - }; + var eMouseEnter = function () { + canvas.mouse.cursor(options.cursors.inactive); + inactiveGrapple.bind("touchleave mouseleave", eMouseLeave); + }; - var eMouseLeave = function () { - canvas.mouse.cursor(options.cursors.default); - inactiveGrapple.unbind("touchleave mouseleave", eMouseLeave); - }; + var eMouseLeave = function () { + canvas.mouse.cursor(options.cursors.default); + inactiveGrapple.unbind("touchleave mouseleave", eMouseLeave); + }; - var eMouseDown = function () { - cy.boxSelectionEnabled(false); - cy.panningEnabled(false); - cy.autounselectify(true); - cy.autoungrabify(true); - canvas.bind("touchend mouseup", eMouseUp); - }; - var eMouseUp = function () { - cy.boxSelectionEnabled(true); - cy.panningEnabled(true); - cy.autounselectify(false); - cy.autoungrabify(false); - setTimeout(function () { - cy.$().unselect(); - node.select(); - }, 0); - canvas.unbind("touchend mouseup", eMouseUp); - }; + var eMouseDown = function () { + cy.boxSelectionEnabled(false); + cy.panningEnabled(false); + cy.autounselectify(true); + cy.autoungrabify(true); + canvas.bind("touchend mouseup", eMouseUp); + }; + var eMouseUp = function () { + cy.boxSelectionEnabled(true); + cy.panningEnabled(true); + cy.autounselectify(false); + cy.autoungrabify(false); + setTimeout(function () { + cy.$().unselect(); + node.select(); + }, 0); + canvas.unbind("touchend mouseup", eMouseUp); + }; - inactiveGrapple.bind("touchstart mousedown", eMouseDown); - inactiveGrapple.bind("touchenter mouseenter", eMouseEnter); + inactiveGrapple.bind("touchstart mousedown", eMouseDown); + inactiveGrapple.bind("touchenter mouseenter", eMouseEnter); - return inactiveGrapple; - } + return inactiveGrapple; + } + + var drawActiveGrapple = function (x, y, t, node, cur) { var grapple = canvas.display.rectangle({ x: x, y: y, @@ -480,7 +481,7 @@ fill: options.grappleColor }); - canvas.addChild(grapple); + canvas.addChild(grapple, false); var startPos = {}; var tmpActiveBgOpacity; @@ -594,8 +595,16 @@ grapple.bind("touchstart mousedown", eMouseDown); grapple.bind("touchenter mouseenter", eMouseEnter); - return grapple; + } + + var drawGrapple = function (x, y, t, node, cur) { + if (options.isNoResizeMode(node) || (options.isFixedAspectRatioResizeMode(node) && t.indexOf("center") >= 0)) { + return drawInactiveGrapple(x, y, t, node); + } + else { + return drawActiveGrapple(x, y, t, node, cur); + } }; var drawGrapples = function (node) { @@ -632,6 +641,7 @@ drawGrapple(startPos.x + width / 2 - gs / 2, startPos.y + height - gs / 2, "bottomcenter", node, options.cursors.s); drawGrapple(startPos.x - gs / 2, startPos.y + height - gs / 2, "bottomleft", node, options.cursors.sw); drawGrapple(startPos.x - gs / 2, startPos.y + height / 2 - gs / 2, "centerleft", node, options.cursors.w); + canvas.redraw(); }; From 442b13e8eab315b3b159029b46c66eb5c44559b8 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Fri, 9 Jun 2017 16:24:50 +0200 Subject: [PATCH 13/37] replace oCanvas by Konva, refactor everything --- cytoscape-node-resize.js | 522 ++++++++++++++++++++++++--------------- demo.html | 2 +- package.json | 3 + undoable_demo.html | 2 +- 4 files changed, 326 insertions(+), 203 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 7b7e752..5573125 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -238,12 +238,10 @@ })(); // registers the extension on a cytoscape lib ref - var register = function (cytoscape, $) { + var register = function (cytoscape, $, Konva) { // can't register if required libraries does not exist - // note that oCanvas is not parametrezid here because it is not commonjs nor amd compatible - // it is expected to be defined as a browser global - if (!cytoscape || !$ || !oCanvas) { + if (!cytoscape || !$ || !Konva) { return; } @@ -304,6 +302,7 @@ var cy = this; // Nodes to draw grapples this variable is set if there is just one selected node var nodeToDrawGrapples; + var controls; // We need to keep the number of selected nodes to check if we should draw grapples. // Calculating it each time decreases performance. var numberOfSelectedNodes; @@ -322,13 +321,23 @@ options = $.extend(true, options, opts); - var $canvas = $(''); + var $canvasElement = $('
'); var $container = $(cy.container()); - $container.append($canvas); + $container.append($canvasElement); + + var stage = new Konva.Stage({ + container: 'node-resize', // id of container
+ width: $container.width(), + height: $container.height() + }); + // then create layer + canvas = new Konva.Layer(); + // add the layer to the stage + stage.add(canvas); // Resize the canvas var sizeCanvas = debounce( function(){ - $canvas + $canvasElement .attr('height', $container.height()) .attr('width', $container.width()) .css({ @@ -336,33 +345,20 @@ 'top': 0, 'left': 0, 'z-index': '999' - }) - ; + }); setTimeout(function () { - var canvasBb = $canvas.offset(); + var canvasBb = $canvasElement.offset(); var containerBb = $container.offset(); - $canvas + $canvasElement .css({ 'top': -(canvasBb.top - containerBb.top), 'left': -(canvasBb.left - containerBb.left) }) ; - - // If there is a previously created canvas destroy it and reset the canvas - if (canvas) { - canvas.destroy(); - } - // See if old canvas is destroyed - canvas = oCanvas.create({ - canvas: "#node-resize" - }); - - // redraw on canvas resize - if(cy){ - refreshGrapples(); - } + canvas.getStage().setWidth($container.width()); + canvas.getStage().setHeight($container.height()); }, 0); }, 250 ); @@ -371,170 +367,230 @@ $(window).on('resize', sizeCanvas); + var ResizeControls = function (node) { + this.parent = node; + this.boundingRectangle = new BoundingRectangle(node); + var grappleLocations = ["topleft", "topcenter", "topright", "centerright", "bottomright", + "bottomcenter", "bottomleft", "centerleft"]; + this.grapples = []; + for(var i=0; i < grappleLocations.length; i++) { + var location = grappleLocations[i]; + console.log("create grapple", location); + var isActive = true; + if (options.isNoResizeMode(node) || (options.isFixedAspectRatioResizeMode(node) && location.indexOf("center") >= 0)) { + isActive = false; + } + this.grapples.push(new Grapple(node, this, location, isActive)) + }; + canvas.draw(); + }; - oCanvas.registerDisplayObject("dashedRectangle", function (settings, core) { - - return oCanvas.extend({ - core: core, - - shapeType: "rectangular", + ResizeControls.prototype.update = function () { + this.boundingRectangle.update(); + for(var i=0; i < this.grapples.length; i++) { + this.grapples[i].update(); + }; + canvas.draw(); + }; - draw: function () { - var canvas = this.core.canvas, - origin = this.getOrigin(), - x = this.abs_x - origin.x + this.lineWidth/2, - y = this.abs_y - origin.y + this.lineWidth/2, - width = this.width, - height = this.height; + ResizeControls.prototype.remove = function () { + this.boundingRectangle.shape.destroy(); + delete this.boundingRectangle; + for(var i=0; i < this.grapples.length; i++) { + this.grapples[i].shape.destroy(); + }; + delete this.grapples; + canvas.draw(); + }; - canvas.beginPath(); + var BoundingRectangle = function (node) { + this.parent = node; + this.shape = null; + var nodePos = node.renderedPosition(); + var width = node.renderedOuterWidth() + getPadding(); + var height = node.renderedOuterHeight() + getPadding(); + var startPos = { + x: nodePos.x - width / 2, + y: nodePos.y - height / 2 + }; + // create our shape + var rect = new Konva.Rect({ + x: startPos.x, + y: startPos.y, + width: width, + height: height, + stroke: options.boundingRectangleLineColor, + strokeWidth: options.boundingRectangleLineWidth, + dash: options.boundingRectangleLineDash + }); + // add the shape to the layer + canvas.add(rect); + this.shape = rect; - if (this.lineWidth > 0) { - canvas.strokeStyle = this.lineColor; - canvas.lineWidth = this.lineWidth; - canvas.setLineDash(this.lineDash); - canvas.strokeRect(x, y, width, height); - } + console.log(startPos, canvas, node.position()); + }; - canvas.closePath(); - } - }, settings); - }); + BoundingRectangle.prototype.update = function () { + var nodePos = this.parent.renderedPosition(); + var width = this.parent.renderedOuterWidth() + getPadding(); + var height = this.parent.renderedOuterHeight() + getPadding(); + var startPos = { + x: nodePos.x - width / 2, + y: nodePos.y - height / 2 + }; + this.shape.x(startPos.x); + this.shape.y(startPos.y); + this.shape.width(width); + this.shape.height(height); + }; - var clearDrawing = function () { - // reset the canvas - canvas.reset(); + var Grapple = function (node, resizeControls, location, isActive) { + this.parent = node; + this.location = location; + this.isActive = isActive; + this.resizeControls = resizeControls; - // Normally canvas.reset() should clear the drawings as well. - // It works as expected id windows is never resized however if it is resized the drawings are not cleared unexpectedly. - // Therefore we need to access the canvas and clear the rectangle (Note that canvas.clear(false) does not work as expected - // as well so wee need to do it manually.) TODO: Figure out the bug clearly and file it to oCanvas library. - var w = $container.width(); - var h = $container.height(); + var nodePos = node.renderedPosition(); + var width = node.renderedOuterWidth() + getPadding(); + var height = node.renderedOuterHeight() + getPadding(); + var startPos = { + x: nodePos.x - width / 2, + y: nodePos.y - height / 2 + }; - canvas.canvas.clearRect(0, 0, w, h); + var gs = getGrappleSize(node); - }; + this.shape = new Konva.Rect({ + width: gs, + height: gs + }); + if(this.isActive) { + this.shape.fill(options.grappleColor); + } + else { + // we need to parse the inactiveGrappleStroke option that is composed of 3 parts + var parts = options.inactiveGrappleStroke.split(' '); + var color = parts[2]; + var strokeWidth = parseInt(parts[1].replace(/px/, '')); + this.shape.stroke(color); + this.shape.strokeWidth(strokeWidth); + } - var getGrappleSize = function (node) { - return Math.max(1, cy.zoom()) * options.grappleSize * Math.min(node.width()/25, node.height()/25, 1); - }; + this.updateShapePosition(startPos, width, height, gs); + canvas.add(this.shape); - var getPadding = function () { - return options.padding*Math.max(1, cy.zoom()); + if(this.isActive) { + this.bindActiveEvents(); + } + else { + this.bindInactiveEvents(); + } }; - var drawInactiveGrapple = function (x, y, t, node) { - var inactiveGrapple = canvas.display.rectangle({ - x: x, - y: y, - height: getGrappleSize(node), - width: getGrappleSize(node), - stroke: options.inactiveGrappleStroke - }); - - canvas.addChild(inactiveGrapple, false); + Grapple.prototype.bindInactiveEvents = function () { + var self = this; // keep reference to the grapple object inside events - - var eMouseEnter = function () { - canvas.mouse.cursor(options.cursors.inactive); - inactiveGrapple.bind("touchleave mouseleave", eMouseLeave); + var eMouseEnter = function (event) { + event.target.getStage().container().style.cursor = options.cursors.inactive; }; - var eMouseLeave = function () { - canvas.mouse.cursor(options.cursors.default); - inactiveGrapple.unbind("touchleave mouseleave", eMouseLeave); + var eMouseLeave = function (event) { + event.target.getStage().container().style.cursor = options.cursors.default; }; - var eMouseDown = function () { + var eMouseDown = function (event) { cy.boxSelectionEnabled(false); cy.panningEnabled(false); cy.autounselectify(true); cy.autoungrabify(true); - canvas.bind("touchend mouseup", eMouseUp); + canvas.getStage().on("contentTouchend contentMouseup", eMouseUp); + nodeToDrawGrapples = self.parent; // keep global reference of the concerned node }; - var eMouseUp = function () { + var eMouseUp = function (event) { + // stage scope cy.boxSelectionEnabled(true); cy.panningEnabled(true); cy.autounselectify(false); cy.autoungrabify(false); - setTimeout(function () { + /*setTimeout(function () { cy.$().unselect(); - node.select(); - }, 0); - canvas.unbind("touchend mouseup", eMouseUp); + nodeToDrawGrapples.select(); + }, 0);*/ + canvas.getStage().off("contentTouchend contentMouseup", eMouseUp); }; - inactiveGrapple.bind("touchstart mousedown", eMouseDown); - inactiveGrapple.bind("touchenter mouseenter", eMouseEnter); - - return inactiveGrapple; - } - - var drawActiveGrapple = function (x, y, t, node, cur) { - var grapple = canvas.display.rectangle({ - x: x, - y: y, - height: getGrappleSize(node), - width: getGrappleSize(node), - fill: options.grappleColor - }); - - canvas.addChild(grapple, false); + this.shape.on("mouseenter", eMouseEnter); + this.shape.on("mouseleave", eMouseLeave); + this.shape.on("touchstart mousedown", eMouseDown); + }; + Grapple.prototype.bindActiveEvents = function () { + var self = this; // keep reference to the grapple object inside events var startPos = {}; var tmpActiveBgOpacity; - var eMouseDown = function () { - cy.trigger("noderesize.resizestart", [t, node]); + + var eMouseDown = function (event) { + cy.trigger("noderesize.resizestart", [self.location, self.parent]); tmpActiveBgOpacity = cy.style()._private.coreStyle["active-bg-opacity"].value; cy.style() .selector("core") .style("active-bg-opacity", 0) .update(); - canvas.mouse.cursor(cur); - startPos.x = this.core.pointer.x; - startPos.y = this.core.pointer.y; + event.target.getStage().container().style.cursor = options.cursors[translateLocation[self.location]]; + var currentPointer = event.target.getStage().getPointerPosition(); + startPos.x = currentPointer.x; + startPos.y = currentPointer.y; cy.boxSelectionEnabled(false); cy.panningEnabled(false); cy.autounselectify(true); cy.autoungrabify(true); - grapple.unbind("touchleave mouseleave", eMouseLeave); - grapple.unbind("touchenter mouseenter", eMouseEnter); - canvas.bind("touchmove mousemove", eMouseMove); - canvas.bind("touchend mouseup", eMouseUp); + self.shape.off("mouseenter", eMouseEnter); + self.shape.off("mouseleave", eMouseLeave); + //canvas.bind("touchmove mousemove", eMouseMove); + //canvas.bind("touchend mouseup", eMouseUp); + canvas.getStage().on("contentTouchend contentMouseup", eMouseUp); + canvas.getStage().on("contentTouchmove contentMousemove", eMouseMove); }; - var eMouseUp = function () { + + var eMouseUp = function (event) { cy.style() .selector("core") .style("active-bg-opacity", tmpActiveBgOpacity) .update(); - canvas.mouse.cursor(options.cursors.default); + self.shape.getStage().container().style.cursor = options.cursors.default; cy.boxSelectionEnabled(true); cy.panningEnabled(true); - cy.autounselectify(false); - cy.autoungrabify(false); - cy.trigger("noderesize.resizeend", [t, node]); - setTimeout(function () { - cy.$().unselect(); - node.select(); + setTimeout(function () { // for some reason, making node unselectable before doesn't work + //cy.$().unselect(); + //node.select(); + cy.autounselectify(false); // think about those 2 + cy.autoungrabify(false); }, 0); - canvas.unbind("touchmove mousemove", eMouseMove); - canvas.unbind("touchend mouseup", eMouseUp); - grapple.bind("touchenter mouseenter", eMouseEnter); + cy.trigger("noderesize.resizeend", [self.location, self.parent]); + canvas.getStage().off("contentTouchend contentMouseup", eMouseUp); + canvas.getStage().off("contentTouchmove contentMousemove", eMouseMove); + self.shape.on("mouseenter", eMouseEnter); + self.shape.on("mouseleave", eMouseLeave); + //canvas.unbind("touchmove mousemove", eMouseMove); + //canvas.unbind("touchend mouseup", eMouseUp); + //grapple.bind("touchenter mouseenter", eMouseEnter); }; - var eMouseMove = function () { - var core = this; - var x = core.pointer.x; - var y = core.pointer.y; + + var eMouseMove = function (event) { + var currentPointer = self.shape.getStage().getPointerPosition(); + var x = currentPointer.x; + var y = currentPointer.y; var xHeight = (y - startPos.y) / cy.zoom(); var xWidth = (x - startPos.x) / cy.zoom(); + var node = self.parent; + var location = self.location; cy.batch(function () { var isAspectedMode = options.isFixedAspectRatioResizeMode(node); - if ((isAspectedMode && t.indexOf("center") >= 0) || - options.isNoResizeMode(node)) + if ((isAspectedMode && location.indexOf("center") >= 0) || + options.isNoResizeMode(node)) return; if (isAspectedMode) { @@ -542,24 +598,22 @@ var aspectedSize = Math.min(xWidth, xHeight); - var isCrossCorners = (t == "topright") || (t == "bottomleft"); + var isCrossCorners = (location == "topright" || location == "bottomleft"); if (xWidth > xHeight) xHeight = xWidth * aspectRatio * (isCrossCorners ? -1 : 1); else xWidth = xHeight / aspectRatio * (isCrossCorners ? -1 : 1); - } - var nodePos = node.position(); - if (t.startsWith("top")) { + if (location.startsWith("top")) { if (node.height() - xHeight > options.minHeight(node)) { node.position("y", nodePos.y + xHeight / 2); options.setHeight(node, node.height() - xHeight); } else if (isAspectedMode) return; - } else if (t.startsWith("bottom")) { + } else if (location.startsWith("bottom")) { if (node.height() + xHeight > options.minHeight(node)) { node.position("y", nodePos.y + xHeight / 2); options.setHeight(node, node.height() + xHeight); @@ -567,10 +621,10 @@ return; } - if (t.endsWith("left") && node.width() - xWidth > options.minWidth(node)) { + if (location.endsWith("left") && node.width() - xWidth > options.minWidth(node)) { node.position("x", nodePos.x + xWidth / 2); options.setWidth(node, node.width() - xWidth); - } else if (t.endsWith("right") && node.width() + xWidth > options.minWidth(node)) { + } else if (location.endsWith("right") && node.width() + xWidth > options.minWidth(node)) { node.position("x", nodePos.x + xWidth / 2); options.setWidth(node, node.width() + xWidth); } @@ -578,74 +632,113 @@ startPos.x = x; startPos.y = y; + self.resizeControls.update(); - cy.trigger("noderesize.resizedrag", [t, node]); + cy.trigger("noderesize.resizedrag", [location, node]); }; - var eMouseEnter = function () { - canvas.mouse.cursor(cur); - grapple.bind("touchleave mouseleave", eMouseLeave); + var translateLocation = { + "topleft": "nw", + "topcenter": "n", + "topright": "ne", + "centerright": "e", + "bottomright": "se", + "bottomcenter": "s", + "bottomleft": "sw", + "centerleft": "w" }; - - var eMouseLeave = function () { - canvas.mouse.cursor(options.cursors.default); - grapple.unbind("touchleave mouseleave", eMouseLeave); + var eMouseEnter = function (event) { + event.target.getStage().container().style.cursor = options.cursors[translateLocation[self.location]]; }; - grapple.bind("touchstart mousedown", eMouseDown); - grapple.bind("touchenter mouseenter", eMouseEnter); - - return grapple; - } + var eMouseLeave = function (event) { + event.target.getStage().container().style.cursor = options.cursors.default; + }; - var drawGrapple = function (x, y, t, node, cur) { - if (options.isNoResizeMode(node) || (options.isFixedAspectRatioResizeMode(node) && t.indexOf("center") >= 0)) { - return drawInactiveGrapple(x, y, t, node); - } - else { - return drawActiveGrapple(x, y, t, node, cur); - } + this.shape.on("mouseenter", eMouseEnter); + this.shape.on("mouseleave", eMouseLeave); + this.shape.on("touchstart mousedown", eMouseDown); }; - var drawGrapples = function (node) { - var nodePos = node.renderedPosition(); - var width = node.renderedOuterWidth() + getPadding(); - var height = node.renderedOuterHeight() + getPadding(); + Grapple.prototype.update = function() { + var nodePos = this.parent.renderedPosition(); + var width = this.parent.renderedOuterWidth() + getPadding(); + var height = this.parent.renderedOuterHeight() + getPadding(); var startPos = { x: nodePos.x - width / 2, y: nodePos.y - height / 2 }; - var gs = getGrappleSize(node); + var gs = getGrappleSize(this.parent); - if (options.boundingRectangle) { - var rect = canvas.display.dashedRectangle({ - x: startPos.x, - y: startPos.y, - width: width, - height: height, - lineColor: options.boundingRectangleLineColor, - lineWidth: options.boundingRectangleLineWidth, - lineDash: options.boundingRectangleLineDash - }); - canvas.addChild(rect); + this.shape.width(gs); + this.shape.height(gs); + this.updateShapePosition(startPos, width, height, gs); + }; + + Grapple.prototype.updateShapePosition = function (startPos, width, height, gs) { + switch(this.location) { + case "topleft": + this.shape.x(startPos.x - gs / 2); + this.shape.y(startPos.y - gs / 2); + break; + case "topcenter": + this.shape.x(startPos.x + width / 2 - gs / 2); + this.shape.y(startPos.y - gs / 2); + break; + case "topright": + this.shape.x(startPos.x + width - gs / 2); + this.shape.y(startPos.y - gs / 2); + break; + case "centerright": + this.shape.x(startPos.x + width - gs / 2); + this.shape.y(startPos.y + height / 2 - gs / 2); + break; + case "bottomright": + this.shape.x(startPos.x + width - gs / 2); + this.shape.y(startPos.y + height - gs / 2); + break; + case "bottomcenter": + this.shape.x(startPos.x + width / 2 - gs / 2); + this.shape.y(startPos.y + height - gs / 2); + break; + case "bottomleft": + this.shape.x(startPos.x - gs / 2); + this.shape.y(startPos.y + height - gs / 2); + break; + case "centerleft": + this.shape.x(startPos.x - gs / 2); + this.shape.y(startPos.y + height / 2 - gs / 2); + break; } + }; + var clearDrawing = function () { + throw new Error("clearDrawing should not be called"); + // reset the canvas + canvas.reset(); - // Clock turning - drawGrapple(startPos.x - gs / 2, startPos.y - gs / 2, "topleft", node, options.cursors.nw); - drawGrapple(startPos.x + width / 2 - gs / 2, startPos.y - gs / 2, "topcenter", node, options.cursors.n); - drawGrapple(startPos.x + width - gs / 2, startPos.y - gs / 2, "topright", node, options.cursors.ne); - drawGrapple(startPos.x + width - gs / 2, startPos.y + height / 2 - gs / 2, "centerright", node, options.cursors.e); - drawGrapple(startPos.x + width - gs / 2, startPos.y + height - gs / 2, "bottomright", node, options.cursors.se); - drawGrapple(startPos.x + width / 2 - gs / 2, startPos.y + height - gs / 2, "bottomcenter", node, options.cursors.s); - drawGrapple(startPos.x - gs / 2, startPos.y + height - gs / 2, "bottomleft", node, options.cursors.sw); - drawGrapple(startPos.x - gs / 2, startPos.y + height / 2 - gs / 2, "centerleft", node, options.cursors.w); - canvas.redraw(); + // Normally canvas.reset() should clear the drawings as well. + // It works as expected id windows is never resized however if it is resized the drawings are not cleared unexpectedly. + // Therefore we need to access the canvas and clear the rectangle (Note that canvas.clear(false) does not work as expected + // as well so wee need to do it manually.) TODO: Figure out the bug clearly and file it to oCanvas library. + var w = $container.width(); + var h = $container.height(); + + canvas.canvas.clearRect(0, 0, w, h); }; + var getGrappleSize = function (node) { + return Math.max(1, cy.zoom()) * options.grappleSize * Math.min(node.width()/25, node.height()/25, 1); + }; + + var getPadding = function () { + return options.padding*Math.max(1, cy.zoom()); + }; + var refreshGrapples = function () { + throw new Error("refreshGrapples should not be called"); clearDrawing(); // If the node to draw grapples is defined it means that there is just one node selected and @@ -799,12 +892,16 @@ }; var bindEvents = function() { + // declare old and current positions + var oldPos = {x: undefined, y: undefined}; + var currentPos = {x : 0, y : 0}; cy.on("unselect", "node", eUnselectNode = function() { + var node = this; // reinitialize old and current compound positions oldPos = {x: undefined, y: undefined}; currentPos = {x: 0, y: 0}; - numberOfSelectedNodes = numberOfSelectedNodes - 1; + /*numberOfSelectedNodes = numberOfSelectedNodes - 1; if (numberOfSelectedNodes === 1) { var selectedNodes = cy.nodes(':selected'); @@ -822,13 +919,22 @@ nodeToDrawGrapples = undefined; } - refreshGrapples(); + refreshGrapples();*/ + if(cy.nodes(':selected').size() == 1) { + controls = new ResizeControls(cy.nodes(':selected')); + } + else { + if(controls) { + controls.remove(); + controls = null; + } + } }); cy.on("select", "node", eSelectNode = function() { var node = this; - numberOfSelectedNodes = numberOfSelectedNodes + 1; + /*numberOfSelectedNodes = numberOfSelectedNodes + 1; if (numberOfSelectedNodes === 1) { nodeToDrawGrapples = node; @@ -836,7 +942,16 @@ else { nodeToDrawGrapples = undefined; } - refreshGrapples(); + refreshGrapples();*/ + if(cy.nodes(':selected').size() == 1) { + controls = new ResizeControls(node); + } + else { + if(controls) { + controls.remove(); + controls = null; + } + } }); cy.on("remove", "node", eRemoveNode = function() { @@ -854,16 +969,12 @@ eSelectNode(); } }); - - // declare old and current positions - var oldPos = {x: undefined, y: undefined}; - var currentPos = {x : 0, y : 0}; // listens for position event and refreshGrapples if necessary cy.on("position", "node", ePositionNode = function() { var node = this; // if position of selected node or compound changes refreshGrapples - if (nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id()){ + /*if (nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id()){ refreshGrapples(); } // if the position of compund changes by repositioning its children's @@ -872,18 +983,25 @@ currentPos = nodeToDrawGrapples.position(); refreshGrapples(); oldPos = {x : currentPos.x, y : currentPos.y}; - }; + };*/ + if(controls) { + if(currentPos.x != oldPos.x || currentPos.y != oldPos.y) { + currentPos = controls.parent.position(); + oldPos = {x : currentPos.x, y : currentPos.y}; + } + controls.update(); + } }); cy.on("zoom", eZoom = function() { - if ( nodeToDrawGrapples ) { - refreshGrapples(); + if ( controls ) { + controls.update(); } }); cy.on("pan", ePan = function() { - if ( nodeToDrawGrapples ) { - refreshGrapples(); + if ( controls ) { + controls.update(); } }); @@ -961,7 +1079,9 @@ options.setWidth(node, arg.css.width); options.setHeight(node, arg.css.height); - refreshGrapples(); // refresh grapplers after node resize + if (controls) { + controls.update(); // refresh grapplers after node resize + } return result; }; @@ -1010,8 +1130,8 @@ }); } - if (typeof cytoscape !== 'undefined' && typeof jQuery !== "undefined") { // expose to global cytoscape (i.e. window.cytoscape) - register(cytoscape, jQuery); + if (typeof cytoscape !== 'undefined' && typeof jQuery !== "undefined" && typeof Konva !== "undefined") { // expose to global cytoscape (i.e. window.cytoscape) + register(cytoscape, jQuery, Konva); } })(); diff --git a/demo.html b/demo.html index 4f14dc3..5c92061 100644 --- a/demo.html +++ b/demo.html @@ -8,7 +8,7 @@ - + diff --git a/package.json b/package.json index 66b8ce9..9b3184c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize/issues" }, "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize", + "dependencies": { + "konva": "^1.6.3" + }, "devDependencies": { "gulp": "^3.8.8", "gulp-jshint": "^1.8.5", diff --git a/undoable_demo.html b/undoable_demo.html index d41cc8a..687aece 100644 --- a/undoable_demo.html +++ b/undoable_demo.html @@ -8,7 +8,7 @@ - + From 6e69d56c051a71711926ac9f7949defbb5a8d13c Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Fri, 9 Jun 2017 16:50:27 +0200 Subject: [PATCH 14/37] ensure we destroy the controls before creating them on addNode --- cytoscape-node-resize.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 5573125..9814af2 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -966,6 +966,10 @@ var node = this; // If a selected node is added we should regard this event just like a select event if ( node.selected() ) { + if(controls) { + controls.remove(); + controls = null; + } eSelectNode(); } }); From 8a4ad44ff3e0885b622107e4fc44865e1ec364aa Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Fri, 9 Jun 2017 16:53:18 +0200 Subject: [PATCH 15/37] remove listener on addNode --- cytoscape-node-resize.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 9814af2..c0a6e48 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -962,7 +962,7 @@ } }); - cy.on("add", "node", eAddNode = function() { + /*cy.on("add", "node", eAddNode = function() { var node = this; // If a selected node is added we should regard this event just like a select event if ( node.selected() ) { @@ -972,7 +972,7 @@ } eSelectNode(); } - }); + });*/ // listens for position event and refreshGrapples if necessary cy.on("position", "node", ePositionNode = function() { From b0e7eca0683cddc6b41563a20d24d1be909f0db6 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Fri, 9 Jun 2017 20:18:53 +0200 Subject: [PATCH 16/37] clean things, remove commented parts --- cytoscape-node-resize.js | 135 +++++++++------------------------------ 1 file changed, 31 insertions(+), 104 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index c0a6e48..2d799d7 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -300,25 +300,14 @@ cytoscape('core', 'nodeResize', function (opts) { var cy = this; - // Nodes to draw grapples this variable is set if there is just one selected node - var nodeToDrawGrapples; + + // the controls object represents the grapples and bounding rectangle + // only one can exist at any time var controls; - // We need to keep the number of selected nodes to check if we should draw grapples. - // Calculating it each time decreases performance. - var numberOfSelectedNodes; + // Events to bind and unbind var eUnselectNode, ePositionNode, eZoom, ePan, eSelectNode, eRemoveNode, eAddNode, eFreeNode; - // Initilize nodes to draw grapples and the number of selected nodes - { - var selectedNodes = cy.nodes(':selected'); - numberOfSelectedNodes = selectedNodes.length; - - if (numberOfSelectedNodes === 1) { - nodeToDrawGrapples = selectedNodes[0]; - } - } - options = $.extend(true, options, opts); var $canvasElement = $('
'); @@ -367,6 +356,15 @@ $(window).on('resize', sizeCanvas); + + /** + * ResizeControls is the object representing the graphical controls presented to the user. + * The controls are composed of: + * - 1 BoundingRectangle object + * - 8 Grapple objects + * + * It is assumed that only one can exist at any time, and it is sotred in the global variable: controls. + */ var ResizeControls = function (node) { this.parent = node; this.boundingRectangle = new BoundingRectangle(node); @@ -505,7 +503,6 @@ cy.autounselectify(true); cy.autoungrabify(true); canvas.getStage().on("contentTouchend contentMouseup", eMouseUp); - nodeToDrawGrapples = self.parent; // keep global reference of the concerned node }; var eMouseUp = function (event) { // stage scope @@ -513,10 +510,6 @@ cy.panningEnabled(true); cy.autounselectify(false); cy.autoungrabify(false); - /*setTimeout(function () { - cy.$().unselect(); - nodeToDrawGrapples.select(); - }, 0);*/ canvas.getStage().off("contentTouchend contentMouseup", eMouseUp); }; @@ -530,6 +523,18 @@ var startPos = {}; var tmpActiveBgOpacity; + // helper object + var translateLocation = { + "topleft": "nw", + "topcenter": "n", + "topright": "ne", + "centerright": "e", + "bottomright": "se", + "bottomcenter": "s", + "bottomleft": "sw", + "centerleft": "w" + }; + var eMouseDown = function (event) { cy.trigger("noderesize.resizestart", [self.location, self.parent]); tmpActiveBgOpacity = cy.style()._private.coreStyle["active-bg-opacity"].value; @@ -547,8 +552,6 @@ cy.autoungrabify(true); self.shape.off("mouseenter", eMouseEnter); self.shape.off("mouseleave", eMouseLeave); - //canvas.bind("touchmove mousemove", eMouseMove); - //canvas.bind("touchend mouseup", eMouseUp); canvas.getStage().on("contentTouchend contentMouseup", eMouseUp); canvas.getStage().on("contentTouchmove contentMousemove", eMouseMove); }; @@ -562,8 +565,6 @@ cy.boxSelectionEnabled(true); cy.panningEnabled(true); setTimeout(function () { // for some reason, making node unselectable before doesn't work - //cy.$().unselect(); - //node.select(); cy.autounselectify(false); // think about those 2 cy.autoungrabify(false); }, 0); @@ -572,9 +573,7 @@ canvas.getStage().off("contentTouchmove contentMousemove", eMouseMove); self.shape.on("mouseenter", eMouseEnter); self.shape.on("mouseleave", eMouseLeave); - //canvas.unbind("touchmove mousemove", eMouseMove); - //canvas.unbind("touchend mouseup", eMouseUp); - //grapple.bind("touchenter mouseenter", eMouseEnter); + }; var eMouseMove = function (event) { @@ -637,16 +636,6 @@ cy.trigger("noderesize.resizedrag", [location, node]); }; - var translateLocation = { - "topleft": "nw", - "topcenter": "n", - "topright": "ne", - "centerright": "e", - "bottomright": "se", - "bottomcenter": "s", - "bottomleft": "sw", - "centerleft": "w" - }; var eMouseEnter = function (event) { event.target.getStage().container().style.cursor = options.cursors[translateLocation[self.location]]; }; @@ -713,22 +702,6 @@ } }; - var clearDrawing = function () { - throw new Error("clearDrawing should not be called"); - // reset the canvas - canvas.reset(); - - // Normally canvas.reset() should clear the drawings as well. - // It works as expected id windows is never resized however if it is resized the drawings are not cleared unexpectedly. - // Therefore we need to access the canvas and clear the rectangle (Note that canvas.clear(false) does not work as expected - // as well so wee need to do it manually.) TODO: Figure out the bug clearly and file it to oCanvas library. - var w = $container.width(); - var h = $container.height(); - - canvas.canvas.clearRect(0, 0, w, h); - - }; - var getGrappleSize = function (node) { return Math.max(1, cy.zoom()) * options.grappleSize * Math.min(node.width()/25, node.height()/25, 1); }; @@ -737,17 +710,6 @@ return options.padding*Math.max(1, cy.zoom()); }; - var refreshGrapples = function () { - throw new Error("refreshGrapples should not be called"); - clearDrawing(); - - // If the node to draw grapples is defined it means that there is just one node selected and - // we need to draw grapples for that node. - if(nodeToDrawGrapples) { - drawGrapples(nodeToDrawGrapples); - } - }; - function getTopMostNodes(nodes) { var nodesMap = {}; for (var i = 0; i < nodes.length; i++) { @@ -901,25 +863,6 @@ oldPos = {x: undefined, y: undefined}; currentPos = {x: 0, y: 0}; - /*numberOfSelectedNodes = numberOfSelectedNodes - 1; - - if (numberOfSelectedNodes === 1) { - var selectedNodes = cy.nodes(':selected'); - - // If user unselects all nodes by tapping to the core etc. then our 'numberOfSelectedNodes' - // may be misleading. Therefore we need to check if the number of nodes to draw grapples is really 1 here. - if (selectedNodes.length === 1) { - nodeToDrawGrapples = selectedNodes[0]; - } - else { - nodeToDrawGrapples = undefined; - } - } - else { - nodeToDrawGrapples = undefined; - } - - refreshGrapples();*/ if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(cy.nodes(':selected')); } @@ -934,15 +877,6 @@ cy.on("select", "node", eSelectNode = function() { var node = this; - /*numberOfSelectedNodes = numberOfSelectedNodes + 1; - - if (numberOfSelectedNodes === 1) { - nodeToDrawGrapples = node; - } - else { - nodeToDrawGrapples = undefined; - } - refreshGrapples();*/ if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(node); } @@ -962,7 +896,9 @@ } }); - /*cy.on("add", "node", eAddNode = function() { + /* + // is this useful ? adding a node never seems to select it, and it causes a bug when changing parent + cy.on("add", "node", eAddNode = function() { var node = this; // If a selected node is added we should regard this event just like a select event if ( node.selected() ) { @@ -977,18 +913,9 @@ // listens for position event and refreshGrapples if necessary cy.on("position", "node", ePositionNode = function() { var node = this; - // if position of selected node or compound changes refreshGrapples - /*if (nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id()){ - refreshGrapples(); - } - // if the position of compund changes by repositioning its children's - // Note: position event for compound is not triggered in this case - else if (nodeToDrawGrapples && (currentPos.x != oldPos.x || currentPos.y != oldPos.y)){ - currentPos = nodeToDrawGrapples.position(); - refreshGrapples(); - oldPos = {x : currentPos.x, y : currentPos.y}; - };*/ if(controls) { + // if the position of compund changes by repositioning its children's + // Note: position event for compound is not triggered in this case if(currentPos.x != oldPos.x || currentPos.y != oldPos.y) { currentPos = controls.parent.position(); oldPos = {x : currentPos.x, y : currentPos.y}; From ce6c30a713db34fed665823fec7bab00ca67682f Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Fri, 9 Jun 2017 20:49:41 +0200 Subject: [PATCH 17/37] fix the trigger on add by actually using the event argument --- cytoscape-node-resize.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 2d799d7..a06e174 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -373,7 +373,6 @@ this.grapples = []; for(var i=0; i < grappleLocations.length; i++) { var location = grappleLocations[i]; - console.log("create grapple", location); var isActive = true; if (options.isNoResizeMode(node) || (options.isFixedAspectRatioResizeMode(node) && location.indexOf("center") >= 0)) { isActive = false; @@ -425,8 +424,6 @@ // add the shape to the layer canvas.add(rect); this.shape = rect; - - console.log(startPos, canvas, node.position()); }; BoundingRectangle.prototype.update = function () { @@ -857,8 +854,7 @@ // declare old and current positions var oldPos = {x: undefined, y: undefined}; var currentPos = {x : 0, y : 0}; - cy.on("unselect", "node", eUnselectNode = function() { - var node = this; + cy.on("unselect", "node", eUnselectNode = function(e) { // reinitialize old and current compound positions oldPos = {x: undefined, y: undefined}; currentPos = {x: 0, y: 0}; @@ -874,8 +870,8 @@ } }); - cy.on("select", "node", eSelectNode = function() { - var node = this; + cy.on("select", "node", eSelectNode = function(e) { + var node = e.target; if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(node); @@ -888,31 +884,29 @@ } }); - cy.on("remove", "node", eRemoveNode = function() { - var node = this; + cy.on("remove", "node", eRemoveNode = function(e) { + var node = e.target; // If a selected node is removed we should regard this event just like an unselect event if ( node.selected() ) { - eUnselectNode(); + eUnselectNode(e); } }); - /* // is this useful ? adding a node never seems to select it, and it causes a bug when changing parent - cy.on("add", "node", eAddNode = function() { - var node = this; + cy.on("add", "node", eAddNode = function(e) { + var node = e.target; // If a selected node is added we should regard this event just like a select event if ( node.selected() ) { if(controls) { controls.remove(); controls = null; } - eSelectNode(); + eSelectNode(e); } - });*/ + }); // listens for position event and refreshGrapples if necessary - cy.on("position", "node", ePositionNode = function() { - var node = this; + cy.on("position", "node", ePositionNode = function(e) { if(controls) { // if the position of compund changes by repositioning its children's // Note: position event for compound is not triggered in this case From 90ae1b993f66546370da866c9cac22e9b6b1ccab Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Fri, 9 Jun 2017 21:17:06 +0200 Subject: [PATCH 18/37] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9e4fc0..81e27b5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Only consists of initilization & default options. * Cytoscape.js ^2.7.0 * jquery ^1.7.0 || ^2.0.0 || ^3.0.0 - * oCanvas ^2.8.0 // It is not commonjs nor AMD compatible just include it in your html file + * konva ^1.6.3 * cytoscape-undo-redo ^1.0.10 (optional) @@ -87,7 +87,7 @@ CommonJS: var cytoscape = require('cytoscape'); var nodeResize = require('cytoscape-node-resize'); -nodeResize( cytoscape, jQuery ); // register extension +nodeResize( cytoscape, jQuery, Konva ); // register extension ``` AMD: From 2026274799305923f036687c0b575583759282dc Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Sat, 10 Jun 2017 14:29:23 +0200 Subject: [PATCH 19/37] fix compound performance issue --- cytoscape-node-resize.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index a06e174..b00637c 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -605,18 +605,22 @@ if (location.startsWith("top")) { if (node.height() - xHeight > options.minHeight(node)) { + // this will trigger a position event, leading to useless redraw. + // TODO find a way to avoid that node.position("y", nodePos.y + xHeight / 2); options.setHeight(node, node.height() - xHeight); } else if (isAspectedMode) return; } else if (location.startsWith("bottom")) { if (node.height() + xHeight > options.minHeight(node)) { + // this will trigger a position event, leading to useless redraw. node.position("y", nodePos.y + xHeight / 2); options.setHeight(node, node.height() + xHeight); } else if (isAspectedMode) return; } + // other position event triggered below if (location.endsWith("left") && node.width() - xWidth > options.minWidth(node)) { node.position("x", nodePos.x + xWidth / 2); options.setWidth(node, node.width() - xWidth); @@ -908,13 +912,16 @@ // listens for position event and refreshGrapples if necessary cy.on("position", "node", ePositionNode = function(e) { if(controls) { + if(e.target.id() == controls.parent.id()) { + controls.update(); + } // if the position of compund changes by repositioning its children's // Note: position event for compound is not triggered in this case - if(currentPos.x != oldPos.x || currentPos.y != oldPos.y) { + else if(currentPos.x != oldPos.x || currentPos.y != oldPos.y) { currentPos = controls.parent.position(); + controls.update(); oldPos = {x : currentPos.x, y : currentPos.y}; } - controls.update(); } }); From 7d1295a1fc663164c68268ae4ea6f2d98184dc22 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Sat, 10 Jun 2017 14:54:55 +0200 Subject: [PATCH 20/37] avoid 1 unnecessary redraw when position x and y are changed separately --- cytoscape-node-resize.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index b00637c..b032e59 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -602,37 +602,47 @@ } var nodePos = node.position(); + var newX = nodePos.x; + var newY = nodePos.y; + var isXresized = false; + var isYresized = false; if (location.startsWith("top")) { if (node.height() - xHeight > options.minHeight(node)) { - // this will trigger a position event, leading to useless redraw. - // TODO find a way to avoid that - node.position("y", nodePos.y + xHeight / 2); + newY = nodePos.y + xHeight / 2; + isYresized = true; options.setHeight(node, node.height() - xHeight); } else if (isAspectedMode) return; } else if (location.startsWith("bottom")) { if (node.height() + xHeight > options.minHeight(node)) { - // this will trigger a position event, leading to useless redraw. - node.position("y", nodePos.y + xHeight / 2); + newY = nodePos.y + xHeight / 2; + isYresized = true; options.setHeight(node, node.height() + xHeight); } else if (isAspectedMode) return; } - // other position event triggered below if (location.endsWith("left") && node.width() - xWidth > options.minWidth(node)) { - node.position("x", nodePos.x + xWidth / 2); + newX = nodePos.x + xWidth / 2; + isXresized = true; options.setWidth(node, node.width() - xWidth); } else if (location.endsWith("right") && node.width() + xWidth > options.minWidth(node)) { - node.position("x", nodePos.x + xWidth / 2); + newX = nodePos.x + xWidth / 2; + isXresized = true; options.setWidth(node, node.width() + xWidth); } + + // this will trigger a position event, leading to useless redraw. + // TODO find a way to avoid that + if(isXresized || isYresized) { + node.position({x: newX, y: newY}); + } }); startPos.x = x; startPos.y = y; - self.resizeControls.update(); + self.resizeControls.update(); // redundant update if the position has changed just before cy.trigger("noderesize.resizedrag", [location, node]); }; From cc41dec56ebcf5b1be0b39ad2c8093af94d8b2b8 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Sat, 10 Jun 2017 15:06:07 +0200 Subject: [PATCH 21/37] put konva as peer dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b3184c..b406300 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize/issues" }, "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize", - "dependencies": { + "peerDependencies": { "konva": "^1.6.3" }, "devDependencies": { From 35672f6445a1a39795253c0db6ccc08b43125cd8 Mon Sep 17 00:00:00 2001 From: metincansiper Date: Sat, 10 Jun 2017 19:14:12 +0300 Subject: [PATCH 22/37] A quick fix in README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81e27b5..ae5cf99 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,15 @@ CommonJS: ```js var cytoscape = require('cytoscape'); var nodeResize = require('cytoscape-node-resize'); +var konva = require('konva'); -nodeResize( cytoscape, jQuery, Konva ); // register extension +nodeResize( cytoscape, jQuery, konva ); // register extension ``` AMD: ```js -require(['cytoscape', 'cytoscape-node-resize', "jquery"], function( cytoscape, nodeResize, jQuery ){ - nodeResize( cytoscape, jQuery ); // register extension +require(['cytoscape', 'cytoscape-node-resize', 'jquery', 'konva'], function( cytoscape, nodeResize, jQuery, konva ){ + nodeResize( cytoscape, jQuery, konva ); // register extension }); ``` From 19eebf410ed803a7f62e2cab990c12822c7189f6 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Sun, 11 Jun 2017 19:10:49 +0200 Subject: [PATCH 23/37] fix iVis-at-Bilkent/newt#64 --- cytoscape-node-resize.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index b032e59..04b54d5 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -394,6 +394,7 @@ this.boundingRectangle.shape.destroy(); delete this.boundingRectangle; for(var i=0; i < this.grapples.length; i++) { + this.grapples[i].unbindAllEvents(); this.grapples[i].shape.destroy(); }; delete this.grapples; @@ -676,6 +677,12 @@ this.updateShapePosition(startPos, width, height, gs); }; + Grapple.prototype.unbindAllEvents = function () { + this.shape.off('mouseenter'); + this.shape.off('mouseleave'); + this.shape.off('touchstart mousedown'); + }; + Grapple.prototype.updateShapePosition = function (startPos, width, height, gs) { switch(this.location) { case "topleft": @@ -873,29 +880,27 @@ oldPos = {x: undefined, y: undefined}; currentPos = {x: 0, y: 0}; + if(controls) { + controls.remove(); + controls = null; + } + if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(cy.nodes(':selected')); } - else { - if(controls) { - controls.remove(); - controls = null; - } - } }); cy.on("select", "node", eSelectNode = function(e) { var node = e.target; + if(controls) { + controls.remove(); + controls = null; + } + if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(node); } - else { - if(controls) { - controls.remove(); - controls = null; - } - } }); cy.on("remove", "node", eRemoveNode = function(e) { From 3bd694df92e33829fa7c04d91a5456f12b6fc76f Mon Sep 17 00:00:00 2001 From: leonarddrv Date: Mon, 12 Jun 2017 10:54:32 +0300 Subject: [PATCH 24/37] Arrow key pressing while label name is being changed bug is fixed /iVis-at-Bilkent/newt#62 --- cytoscape-node-resize.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 04b54d5..b8d94f1 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -992,17 +992,20 @@ }); cy.on("noderesize.moveend", function (e, nodes) { - var initialPos = moveparam.firstNodePosition; + if (moveparam != undefined) + { + var initialPos = moveparam.firstNodePosition; - moveparam.positionDiff = { - x: -nodes[0].position('x') + initialPos.x, - y: -nodes[0].position('y') + initialPos.y - } + moveparam.positionDiff = { + x: -nodes[0].position('x') + initialPos.x, + y: -nodes[0].position('y') + initialPos.y + } - delete moveparam.firstNodePosition; + delete moveparam.firstNodePosition; - cy.undoRedo().do("noderesize.move", moveparam); - moveparam = undefined; + cy.undoRedo().do("noderesize.move", moveparam); + moveparam = undefined; + } }); var resizeDo = function (arg) { From d1317005e30cd5ea43693285008008921a3debe8 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Sun, 11 Jun 2017 19:10:49 +0200 Subject: [PATCH 25/37] fix iVis-at-Bilkent/newt#64 --- cytoscape-node-resize.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index b032e59..04b54d5 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -394,6 +394,7 @@ this.boundingRectangle.shape.destroy(); delete this.boundingRectangle; for(var i=0; i < this.grapples.length; i++) { + this.grapples[i].unbindAllEvents(); this.grapples[i].shape.destroy(); }; delete this.grapples; @@ -676,6 +677,12 @@ this.updateShapePosition(startPos, width, height, gs); }; + Grapple.prototype.unbindAllEvents = function () { + this.shape.off('mouseenter'); + this.shape.off('mouseleave'); + this.shape.off('touchstart mousedown'); + }; + Grapple.prototype.updateShapePosition = function (startPos, width, height, gs) { switch(this.location) { case "topleft": @@ -873,29 +880,27 @@ oldPos = {x: undefined, y: undefined}; currentPos = {x: 0, y: 0}; + if(controls) { + controls.remove(); + controls = null; + } + if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(cy.nodes(':selected')); } - else { - if(controls) { - controls.remove(); - controls = null; - } - } }); cy.on("select", "node", eSelectNode = function(e) { var node = e.target; + if(controls) { + controls.remove(); + controls = null; + } + if(cy.nodes(':selected').size() == 1) { controls = new ResizeControls(node); } - else { - if(controls) { - controls.remove(); - controls = null; - } - } }); cy.on("remove", "node", eRemoveNode = function(e) { From 439d30c43e5721f114dccf59ac9b0775ea9a5064 Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Mon, 12 Jun 2017 10:22:37 +0200 Subject: [PATCH 26/37] fix iVis-at-Bilkent/newt#67 and partially #14 --- cytoscape-node-resize.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 04b54d5..9af4d11 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -885,8 +885,9 @@ controls = null; } - if(cy.nodes(':selected').size() == 1) { - controls = new ResizeControls(cy.nodes(':selected')); + var selectedNodes = cy.nodes(':selected'); + if(selectedNodes.size() == 1) { + controls = new ResizeControls(selectedNodes); } }); @@ -898,8 +899,9 @@ controls = null; } - if(cy.nodes(':selected').size() == 1) { - controls = new ResizeControls(node); + var selectedNodes = cy.nodes(':selected'); + if(selectedNodes.size() == 1) { + controls = new ResizeControls(selectedNodes); } }); @@ -916,10 +918,6 @@ var node = e.target; // If a selected node is added we should regard this event just like a select event if ( node.selected() ) { - if(controls) { - controls.remove(); - controls = null; - } eSelectNode(e); } }); @@ -927,12 +925,16 @@ // listens for position event and refreshGrapples if necessary cy.on("position", "node", ePositionNode = function(e) { if(controls) { + // It seems that parent.position() doesn't always give consistent result. + // But calling it here makes the results consistent, by updating it to the correct value, somehow. + // Maybe there is some cache on cytoscape side preventing a position update. + var trash_var = controls.parent.position(); // trash_var isn't used, this line apparently makes position() correct if(e.target.id() == controls.parent.id()) { controls.update(); } // if the position of compund changes by repositioning its children's // Note: position event for compound is not triggered in this case - else if(currentPos.x != oldPos.x || currentPos.y != oldPos.y) { + else if(e.target.isChild() && (currentPos.x != oldPos.x || currentPos.y != oldPos.y)) { currentPos = controls.parent.position(); controls.update(); oldPos = {x : currentPos.x, y : currentPos.y}; From 62107798d8612c0fecfb72c04523de0064b4935f Mon Sep 17 00:00:00 2001 From: Ludovic Roy Date: Mon, 12 Jun 2017 17:10:00 +0200 Subject: [PATCH 27/37] ensure grapples are updated for resize do action --- cytoscape-node-resize.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 464cd3c..43c867f 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -1012,6 +1012,9 @@ var resizeDo = function (arg) { if (arg.firstTime) { + if (controls) { + controls.update(); // refresh grapplers after node resize + } delete arg.firstTime; return arg; } From 862c17beb4d2ca542f339e72dea524d8575ddde1 Mon Sep 17 00:00:00 2001 From: leonarddrv Date: Wed, 14 Jun 2017 11:54:41 +0300 Subject: [PATCH 28/37] Pressing arrows while nothing selected bug is fixed /iVis-at-Bilkent/newt#62 --- cytoscape-node-resize.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 43c867f..65bed6f 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -982,14 +982,16 @@ }); cy.on("noderesize.movestart", function (e, nodes) { - - moveparam = { - firstTime : true, - firstNodePosition: { - x: nodes[0].position('x'), - y: nodes[0].position('y') - }, - nodes: nodes + if (nodes[0] != undefined) + { + moveparam = { + firstTime: true, + firstNodePosition: { + x: nodes[0].position('x'), + y: nodes[0].position('y') + }, + nodes: nodes + } } }); From c65016e5f4ce6da0648e0c1df204b60475008af1 Mon Sep 17 00:00:00 2001 From: kinimesi Date: Wed, 14 Jun 2017 14:58:21 +0300 Subject: [PATCH 29/37] Fixes iVis-at-Bilkent/cytoscape.js-node-resize/issues/14 --- cytoscape-node-resize.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 65bed6f..6024fad 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -306,7 +306,7 @@ var controls; // Events to bind and unbind - var eUnselectNode, ePositionNode, eZoom, ePan, eSelectNode, eRemoveNode, eAddNode, eFreeNode; + var eUnselectNode, ePositionNode, eZoom, ePan, eSelectNode, eRemoveNode, eAddNode, eFreeNode, eUndoRedo; options = $.extend(true, options, opts); @@ -869,6 +869,7 @@ cy.off("select", "node", eSelectNode); cy.off("remove", "node", eRemoveNode); cy.off("add", "node", eAddNode); + cy.off("afterUndo afterRedo", eUndoRedo); }; var bindEvents = function() { @@ -954,6 +955,12 @@ } }); + cy.on("afterUndo afterRedo", eUndoRedo = function() { + if ( controls ) { + controls.update(); + } + }); + document.addEventListener("keydown",keyDown, true); document.addEventListener("keyup",keyUp, true); }; From 4d34ca8a56c570b5bd395c81b85f2db1fdd2cfb7 Mon Sep 17 00:00:00 2001 From: kinimesi Date: Wed, 14 Jun 2017 19:18:57 +0300 Subject: [PATCH 30/37] Fixes #14 --- cytoscape-node-resize.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 6024fad..bef4f4e 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -958,6 +958,7 @@ cy.on("afterUndo afterRedo", eUndoRedo = function() { if ( controls ) { controls.update(); + oldPos = {x: undefined, y: undefined}; } }); From 1310d754cbd0cdb6c9a8f7a07d607c157598f206 Mon Sep 17 00:00:00 2001 From: kinimesi Date: Wed, 14 Jun 2017 21:28:09 +0300 Subject: [PATCH 31/37] Fixes #16 --- cytoscape-node-resize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index bef4f4e..7307cd3 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -935,7 +935,7 @@ } // if the position of compund changes by repositioning its children's // Note: position event for compound is not triggered in this case - else if(e.target.isChild() && (currentPos.x != oldPos.x || currentPos.y != oldPos.y)) { + else if(currentPos.x != oldPos.x || currentPos.y != oldPos.y) { currentPos = controls.parent.position(); controls.update(); oldPos = {x : currentPos.x, y : currentPos.y}; From 6ce289f53fda65e48bfa490efe544c15be0ee1a5 Mon Sep 17 00:00:00 2001 From: metincansiper Date: Tue, 20 Jun 2017 11:18:57 +0300 Subject: [PATCH 32/37] Add compound resize support --- README.md | 33 ++++- cytoscape-node-resize.js | 271 +++++++++++++++++++++++++++++++++++---- 2 files changed, 273 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ae5cf99..3b99057 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ A Cytoscape.js extension to provide grapples to resize nodes, distributed under ![Image of extension](img.png) -## API - -Only consists of initilization & default options. +## Default Options ```js cy.nodeResize({ @@ -34,6 +32,27 @@ Only consists of initilization & default options. return data ? data : 15; }, // a function returns min height of node + // Getters for some style properties the defaults returns ele.css('property-name') + // you are encouraged to override these getters + getCompoundMinWidth: function(node) { + return node.css('min-width'); + }, + getCompoundMinHeight: function(node) { + return node.css('min-height'); + }, + getCompoundMinWidthBiasRight: function(node) { + return node.css('min-width-bias-right'); + }, + getCompoundMinWidthBiasLeft: function(node) { + return node.css('min-width-bias-left'); + }, + getCompoundMinHeightBiasTop: function(node) { + return node.css('min-height-bias-top'); + }, + getCompoundMinHeightBiasBottom: function(node) { + return node.css('min-height-bias-bottom'); + }, + // These optional function will be executed to set the width/height of a node in this extension // Using node.css() is not a recommended way (http://js.cytoscape.org/#eles.style) to do this. Therefore, overriding these defaults // so that a data field or something like that will be used to set node dimentions instead of directly calling node.css() @@ -64,6 +83,14 @@ Only consists of initilization & default options. }); ``` +## API + + `var api = cy.nodeResize('get')` + To get the extension instance after initialization. + + `api.refreshGrapples()` + Refresh rendered node grapples if any. It is an expensive operation and is supposed to be called in rare cases (When it is really needed). + ## Dependencies diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index 7307cd3..ab99b20 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -269,6 +269,27 @@ return data ? data : 15; }, // a function returns min height of node + // Getters for some style properties the defaults returns ele.css('property-name') + // you are encouraged to override these getters + getCompoundMinWidth: function(node) { + return node.css('min-width'); + }, + getCompoundMinHeight: function(node) { + return node.css('min-height'); + }, + getCompoundMinWidthBiasRight: function(node) { + return node.css('min-width-bias-right'); + }, + getCompoundMinWidthBiasLeft: function(node) { + return node.css('min-width-bias-left'); + }, + getCompoundMinHeightBiasTop: function(node) { + return node.css('min-height-bias-top'); + }, + getCompoundMinHeightBiasBottom: function(node) { + return node.css('min-height-bias-bottom'); + }, + // These optional function will be executed to set the width/height of a node in this extension // Using node.css() is not a recommended way (http://js.cytoscape.org/#eles.style) to do this. Therefore, overriding these defaults // so that a data field or something like that will be used to set node dimentions instead of directly calling node.css() @@ -279,6 +300,24 @@ setHeight: function(node, height) { node.css('height', height); }, + setCompoundMinWidth: function(node, minWidth) { + node.css('min-width', minWidth); + }, + setCompoundMinHeight: function(node, minHeight) { + node.css('min-height', minHeight); + }, + setCompoundMinWidthBiasLeft: function(node, minWidthBiasLeft) { + node.css('min-width-bias-left', minWidthBiasLeft); + }, + setCompoundMinWidthBiasRight: function(node, minHeightBiasRight) { + node.css('min-width-bias-right', minHeightBiasRight); + }, + setCompoundMinHeightBiasTop: function(node, minHeightBiasTop) { + node.css('min-height-bias-top', minHeightBiasTop); + }, + setCompoundMinHeightBiasBottom: function(node, minHeightBiasBottom) { + node.css('min-height-bias-bottom', minHeightBiasBottom); + }, isFixedAspectRatioResizeMode: function (node) { return node.is(".fixedAspectRatioResizeMode") },// with only 4 active grapples (at corners) isNoResizeMode: function (node) { return node.is(".noResizeMode, :parent") }, // no active grapples @@ -297,8 +336,16 @@ w: "w-resize" } }; + + var api; // The extension api to be exposed cytoscape('core', 'nodeResize', function (opts) { + + // If options parameter is 'get' string then just return the api + if (opts === 'get') { + return api; + } + var cy = this; // the controls object represents the grapples and bounding rectangle @@ -518,8 +565,13 @@ Grapple.prototype.bindActiveEvents = function () { var self = this; // keep reference to the grapple object inside events + var node = self.parent; + var setWidthFcn, setHeightFcn; // Functions to resize the node + var getWidthFcn,getHeightFcn; // Functions to get node sizes var startPos = {}; var tmpActiveBgOpacity; + // BBox of children of a node. Of course is valid if the node is a compound. + var childrenBBox; // helper object var translateLocation = { @@ -534,6 +586,28 @@ }; var eMouseDown = function (event) { + childrenBBox = node.children().boundingBox(); + // If the node is a compound use setCompoundMinWidth() and setCompoundMinHeight() + // instead of setWidth() and setHeight() + setWidthFcn = node.isParent() ? options.setCompoundMinWidth : options.setWidth; + setHeightFcn = node.isParent() ? options.setCompoundMinHeight : options.setHeight; + + getWidthFcn = function(node) { + if (node.isParent()) { + return Math.max(parseFloat(options.getCompoundMinWidth(node)), childrenBBox.w); + } + + return node.width(); + }; + + getHeightFcn = function(node) { + if (node.isParent()) { + return Math.max(parseFloat(options.getCompoundMinHeight(node)), childrenBBox.h); + } + + return node.height(); + }; + cy.trigger("noderesize.resizestart", [self.location, self.parent]); tmpActiveBgOpacity = cy.style()._private.coreStyle["active-bg-opacity"].value; cy.style() @@ -582,7 +656,6 @@ var xHeight = (y - startPos.y) / cy.zoom(); var xWidth = (x - startPos.x) / cy.zoom(); - var node = self.parent; var location = self.location; cy.batch(function () { var isAspectedMode = options.isFixedAspectRatioResizeMode(node); @@ -591,7 +664,7 @@ return; if (isAspectedMode) { - var aspectRatio = node.height() / node.width(); + var aspectRatio = getHeightFcn(node) / getWidthFcn(node); var aspectedSize = Math.min(xWidth, xHeight); @@ -608,37 +681,126 @@ var isXresized = false; var isYresized = false; + // These are valid if the node is a compound + // Initial (before resize) sizes of compound + var initialWidth, initialHeight; + // Extra space between node width and children bbox. Causes by 'min-width' and/or 'min-height' + var extraLeft = 0, extraRight = 0, extraTop = 0, extraBottom = 0; + + if (node.isParent()) { + var totalExtraWidth = getWidthFcn(node) - childrenBBox.w; + var totalExtraHeight = getHeightFcn(node) - childrenBBox.h; + + if (totalExtraWidth > 0) { + extraLeft = totalExtraWidth * parseFloat(options.getCompoundMinWidthBiasLeft(node)) / + ( parseFloat(options.getCompoundMinWidthBiasLeft(node)) + parseFloat(options.getCompoundMinWidthBiasRight(node)) ); + extraRight = totalExtraWidth - extraLeft; + } + + if (totalExtraHeight > 0) { + extraTop = totalExtraHeight * parseFloat(options.getCompoundMinHeightBiasTop(node)) / + ( parseFloat(options.getCompoundMinHeightBiasTop(node)) + parseFloat(options.getCompoundMinHeightBiasBottom(node)) ); + extraBottom = totalExtraHeight - extraTop; + } + } + if (location.startsWith("top")) { - if (node.height() - xHeight > options.minHeight(node)) { + // Note that xHeight is supposed to be negative + // If the node is simple min height should not be exceed, else if it is compound + // then extraTop should not be negative + if (getHeightFcn(node) - xHeight > options.minHeight(node) + && ( !node.isParent() || extraTop - xHeight >= 0 ) ) { newY = nodePos.y + xHeight / 2; isYresized = true; - options.setHeight(node, node.height() - xHeight); + setHeightFcn(node, getHeightFcn(node) - xHeight); } else if (isAspectedMode) return; } else if (location.startsWith("bottom")) { - if (node.height() + xHeight > options.minHeight(node)) { + // Note that xHeight is supposed to be positive + // If the node is simple min height should not be exceed, else if it is compound + // then extraBottom should not be negative + if (getHeightFcn(node) + xHeight > options.minHeight(node) + && ( !node.isParent() || extraBottom + xHeight >= 0 ) ) { newY = nodePos.y + xHeight / 2; isYresized = true; - options.setHeight(node, node.height() + xHeight); + setHeightFcn(node, getHeightFcn(node) + xHeight); } else if (isAspectedMode) return; } - if (location.endsWith("left") && node.width() - xWidth > options.minWidth(node)) { + if (location.endsWith("left") && getWidthFcn(node) - xWidth > options.minWidth(node) + && ( !node.isParent() || extraLeft - xWidth >= 0 ) ) { + // Note that xWidth is supposed to be negative + // If the node is simple min width should not be exceed, else if it is compound + // then extraLeft should not be negative newX = nodePos.x + xWidth / 2; isXresized = true; - options.setWidth(node, node.width() - xWidth); - } else if (location.endsWith("right") && node.width() + xWidth > options.minWidth(node)) { + setWidthFcn(node, getWidthFcn(node) - xWidth); + } else if (location.endsWith("right") && getWidthFcn(node) + xWidth > options.minWidth(node) + && ( !node.isParent() || extraRight + xWidth >= 0 ) ) { + // Note that xWidth is supposed to be positive + // If the node is simple min width should not be exceed, else if it is compound + // then extraRight should not be negative newX = nodePos.x + xWidth / 2; isXresized = true; - options.setWidth(node, node.width() + xWidth); + setWidthFcn(node, getWidthFcn(node) + xWidth); } // this will trigger a position event, leading to useless redraw. // TODO find a way to avoid that - if(isXresized || isYresized) { + if(!node.isParent() && ( isXresized || isYresized )) { node.position({x: newX, y: newY}); } + + // If the node is a compound we need to handle left/right/top/bottom biases conditionally + if ( node.isParent() ) { + var totalExtraWidth = getWidthFcn(node) - childrenBBox.w; + var totalExtraHeight = getHeightFcn(node) - childrenBBox.h; + + if (isXresized && totalExtraWidth > 0) { + // If the location ends with right the left extra space should be fixed + // else if it ends with left the right extra space should be fixed + if (location.endsWith('right')) { + extraRight = totalExtraWidth - extraLeft; + } + else if (location.endsWith('left')) { + extraLeft = totalExtraWidth - extraRight; + } + + var biasLeft = extraLeft / (extraLeft + extraRight) * 100; + var biasRight = 100 - biasLeft; + + if (biasLeft < 0 || biasRight < 0) { +// console.log('negative horizontal'); + return; + } + + options.setCompoundMinWidthBiasLeft(node, biasLeft + '%'); + options.setCompoundMinWidthBiasRight(node, biasRight + '%'); + } + + if (isYresized && totalExtraHeight > 0) { + // If the location starts with top the bottom extra space should be fixed + // else if it starst with bottom the top extra space should be fixed + if (location.startsWith('top')) { + extraTop = totalExtraHeight - extraBottom; + } + else if (location.startsWith('bottom')) { + extraBottom = totalExtraHeight - extraTop; + } + + var biasTop = extraTop / (extraTop + extraBottom) * 100; + var biasBottom = 100 - biasTop; + + if (biasTop < 0 || biasBottom < 0) { +// console.log('negative vertical'); + return; + } + + options.setCompoundMinHeightBiasTop(node, biasTop + '%'); + options.setCompoundMinHeightBiasBottom(node, biasBottom + '%'); + } + } }); startPos.x = x; @@ -971,18 +1133,32 @@ var param; var moveparam; - + + // On resize start fill param object to use it on undo/redo cy.on("noderesize.resizestart", function (e, type, node) { param = { node: node, css: { - width: node.width(), - height: node.height() - }, - position: $.extend({}, node.position()) + } }; + + // Some parts of param object are dependant on whether the node is a compound or simple node + if (node.isParent()) { + param.css.minWidth = parseFloat(options.getCompoundMinWidth(node)); + param.css.minHeight = parseFloat(options.getCompoundMinHeight(node)); + param.css.biasLeft = options.getCompoundMinWidthBiasLeft(node); + param.css.biasRight = options.getCompoundMinWidthBiasRight(node); + param.css.biasTop = options.getCompoundMinHeightBiasTop(node); + param.css.biasBottom = options.getCompoundMinHeightBiasBottom(node); + } + else { + param.css.width = node.width(); + param.css.height = node.height(); + param.position = $.extend({}, node.position()); + } }); - + + // On resize end do the action using param object cy.on("noderesize.resizeend", function (e, type, node) { param.firstTime = true; cy.undoRedo().do("resize", param); @@ -1021,6 +1197,8 @@ }); var resizeDo = function (arg) { + // If this is the first time it means that resize is already performed through user interaction. + // In this case just removing the first time parameter is enough. if (arg.firstTime) { if (controls) { controls.update(); // refresh grapplers after node resize @@ -1030,19 +1208,47 @@ } var node = arg.node; - + + // Result object is to be returned for undo/redo cases var result = { node: node, css: { - width: node.width(), - height: node.height() - }, - position: $.extend({}, node.position()) + } }; - - node.position(arg.position); - options.setWidth(node, arg.css.width); - options.setHeight(node, arg.css.height); + + // Some parts of result object is dependent on whether the node is simple or compound + if (node.isParent()) { + result.css.minWidth = parseFloat(options.getCompoundMinWidth(node)); + result.css.minHeight = parseFloat(options.getCompoundMinHeight(node)); + result.css.biasLeft = options.getCompoundMinWidthBiasLeft(node); + result.css.biasRight = options.getCompoundMinWidthBiasRight(node); + result.css.biasTop = options.getCompoundMinHeightBiasTop(node); + result.css.biasBottom = options.getCompoundMinHeightBiasBottom(node); + } + else { + result.css.width = node.width(); + result.css.height = node.height(); + result.position = $.extend({}, node.position()); + } + + // Perform actual undo/redo part using args object + cy.startBatch(); + + if (node.isParent()) { + options.setCompoundMinWidth(node, arg.css.minWidth); + options.setCompoundMinHeight(node, arg.css.minHeight); + options.setCompoundMinWidthBiasLeft(node, arg.css.biasLeft); + options.setCompoundMinWidthBiasRight(node, arg.css.biasRight); + options.setCompoundMinHeightBiasTop(node, arg.css.biasTop); + options.setCompoundMinHeightBiasBottom(node, arg.css.biasBottom); + } + else { + node.position(arg.position); + options.setWidth(node, arg.css.width); + options.setHeight(node, arg.css.height); + } + + cy.endBatch(); if (controls) { controls.update(); // refresh grapplers after node resize @@ -1079,8 +1285,17 @@ cy.undoRedo().action("noderesize.move", moveDo, moveDo); } - - return this; // chainability + api = {} + api.refreshGrapples = function() { + if (controls) { + // We need to remove old controls and create a new one rather then just updating controls + // We need this because the parent may change status and become resizable or not-resizable + var parent = controls.parent; + controls.remove(); + controls = new ResizeControls(parent); + } + } + return api; // Return the api }); }; From b418a0bd335cfe23075361cd344db1e1611e4a38 Mon Sep 17 00:00:00 2001 From: hasanbalci Date: Wed, 9 Aug 2017 04:34:26 +0300 Subject: [PATCH 33/37] Expose removeGrapples feature to remove grapples while node is selected iVis-at-Bilkent/newt/issues/150 --- README.md | 3 +++ cytoscape-node-resize.js | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index 3b99057..867e3d2 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ A Cytoscape.js extension to provide grapples to resize nodes, distributed under `api.refreshGrapples()` Refresh rendered node grapples if any. It is an expensive operation and is supposed to be called in rare cases (When it is really needed). + `api.removeGrapples()` + Remove grapples while node is selected. This is useful when a node is selected but no need to show grapples. + ## Dependencies diff --git a/cytoscape-node-resize.js b/cytoscape-node-resize.js index ab99b20..1996e4b 100644 --- a/cytoscape-node-resize.js +++ b/cytoscape-node-resize.js @@ -1295,6 +1295,13 @@ controls = new ResizeControls(parent); } } + // Simply remove grapples even if node is selected + api.removeGrapples = function() { + if (controls) { + controls.remove(); + controls = null; + } + } return api; // Return the api }); From fbf0f1d06a421df4592367942780df7277a2d685 Mon Sep 17 00:00:00 2001 From: hasanbalci Date: Wed, 9 Aug 2017 04:49:17 +0300 Subject: [PATCH 34/37] Updated 'cytoscape.min.js' link in demo files --- demo.html | 2 +- undoable_demo.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo.html b/demo.html index 5c92061..428a908 100644 --- a/demo.html +++ b/demo.html @@ -9,7 +9,7 @@ - + diff --git a/undoable_demo.html b/undoable_demo.html index 687aece..9875d6e 100644 --- a/undoable_demo.html +++ b/undoable_demo.html @@ -9,7 +9,7 @@ - + From 1bf62c2aacf106be9981086b55dddf05012bae1d Mon Sep 17 00:00:00 2001 From: hasanbalci Date: Mon, 21 Aug 2017 13:36:19 +0300 Subject: [PATCH 35/37] Updated package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b406300..b791de3 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize", "peerDependencies": { + "cytoscape": "^3.0.0", "konva": "^1.6.3" }, "devDependencies": { From 4bb928a68f853d39f216212a98ea6e35956f0d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Balc=C4=B1?= Date: Mon, 21 Aug 2017 13:41:45 +0300 Subject: [PATCH 36/37] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 867e3d2..ed5aee5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ A Cytoscape.js extension to provide grapples to resize nodes, distributed under ![Image of extension](img.png) +## Demo + +Click [here](https://rawgit.com/iVis-at-Bilkent/cytoscape.js-node-resize/master/demo.html) for demo + ## Default Options ```js From ea72387f9fab637cf3a65f862eaa4b7fcfb383de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Balc=C4=B1?= Date: Mon, 21 Aug 2017 13:45:36 +0300 Subject: [PATCH 37/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed5aee5..face2a1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Cytoscape.js extension to provide grapples to resize nodes, distributed under ## Demo -Click [here](https://rawgit.com/iVis-at-Bilkent/cytoscape.js-node-resize/master/demo.html) for demo +Click [here](https://rawgit.com/iVis-at-Bilkent/cytoscape.js-node-resize/master/demo.html) (simple) or [here](https://rawgit.com/iVis-at-Bilkent/cytoscape.js-node-resize/master/undoable_demo.html) (undoable) for demos ## Default Options