+ 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
- .attr('height', $container.height())
- .attr('width', $container.width())
- .css({
- 'position': 'absolute',
- 'top': 0,
- 'left': 0,
- 'z-index': '999'
- })
- ;
+ $canvasElement
+ .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();
-
- $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();
- }
+ var canvasBb = $canvasElement.offset();
+ var containerBb = $container.offset();
+
+ $canvasElement
+ .css({
+ 'top': -(canvasBb.top - containerBb.top),
+ 'left': -(canvasBb.left - containerBb.left)
+ })
+ ;
+ canvas.getStage().setWidth($container.width());
+ canvas.getStage().setHeight($container.height());
}, 0);
- }, 250 );
+ }, 250 );
- sizeCanvas();
+ sizeCanvas();
$(window).on('resize', sizeCanvas);
-
-
- oCanvas.registerDisplayObject("dashedRectangle", function (settings, core) {
-
- return oCanvas.extend({
- core: core,
-
- shapeType: "rectangular",
-
- 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;
-
- canvas.beginPath();
- if (this.lineWidth > 0) {
- canvas.strokeStyle = this.lineColor;
- canvas.lineWidth = this.lineWidth;
- canvas.setLineDash(this.lineDash);
- canvas.strokeRect(x, y, width, height);
- }
-
- canvas.closePath();
+ /**
+ * 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);
+ var grappleLocations = ["topleft", "topcenter", "topright", "centerright", "bottomright",
+ "bottomcenter", "bottomleft", "centerleft"];
+ this.grapples = [];
+ for(var i=0; i < grappleLocations.length; i++) {
+ var location = grappleLocations[i];
+ var isActive = true;
+ if (options.isNoResizeMode(node) || (options.isFixedAspectRatioResizeMode(node) && location.indexOf("center") >= 0)) {
+ isActive = false;
}
- }, settings);
- });
-
- 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
- // 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();
+ this.grapples.push(new Grapple(node, this, location, isActive))
+ };
+ canvas.draw();
+ };
- canvas.canvas.clearRect(0, 0, w, h);
+ ResizeControls.prototype.update = function () {
+ this.boundingRectangle.update();
+ for(var i=0; i < this.grapples.length; i++) {
+ this.grapples[i].update();
+ };
+ canvas.draw();
+ };
+ ResizeControls.prototype.remove = function () {
+ 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;
+ canvas.draw();
};
- var getGrappleSize = function (node) {
- return Math.max(1, cy.zoom()) * options.grappleSize * Math.min(node.width()/25, node.height()/25, 1);
+ 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;
};
- var getPadding = function () {
- return options.padding*Math.max(1, cy.zoom());
+ 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 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 Grapple = function (node, resizeControls, location, isActive) {
+ this.parent = node;
+ this.location = location;
+ this.isActive = isActive;
+ this.resizeControls = resizeControls;
- canvas.addChild(inactiveGrapple);
+ 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
+ };
+ var gs = getGrappleSize(node);
- var eMouseEnter = function () {
- canvas.mouse.cursor(options.cursors.inactive);
- inactiveGrapple.bind("touchleave mouseleave", eMouseLeave);
- };
+ 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 eMouseLeave = function () {
- canvas.mouse.cursor(options.cursors.default);
- inactiveGrapple.unbind("touchleave mouseleave", eMouseLeave);
- };
+ this.updateShapePosition(startPos, width, height, gs);
+ canvas.add(this.shape);
- 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);
- };
+ if(this.isActive) {
+ this.bindActiveEvents();
+ }
+ else {
+ this.bindInactiveEvents();
+ }
+ };
- inactiveGrapple.bind("touchstart mousedown", eMouseDown);
- inactiveGrapple.bind("touchenter mouseenter", eMouseEnter);
+ Grapple.prototype.bindInactiveEvents = function () {
+ var self = this; // keep reference to the grapple object inside events
- return inactiveGrapple;
- }
- var grapple = canvas.display.rectangle({
- x: x,
- y: y,
- height: getGrappleSize(node),
- width: getGrappleSize(node),
- fill: options.grappleColor
- });
+ var eMouseEnter = function (event) {
+ event.target.getStage().container().style.cursor = options.cursors.inactive;
+ };
+
+ var eMouseLeave = function (event) {
+ event.target.getStage().container().style.cursor = options.cursors.default;
+ };
+
+ var eMouseDown = function (event) {
+ cy.boxSelectionEnabled(false);
+ cy.panningEnabled(false);
+ cy.autounselectify(true);
+ cy.autoungrabify(true);
+ canvas.getStage().on("contentTouchend contentMouseup", eMouseUp);
+ };
+ var eMouseUp = function (event) {
+ // stage scope
+ cy.boxSelectionEnabled(true);
+ cy.panningEnabled(true);
+ cy.autounselectify(false);
+ cy.autoungrabify(false);
+ canvas.getStage().off("contentTouchend contentMouseup", eMouseUp);
+ };
- canvas.addChild(grapple);
+ 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 node = self.parent;
+ var setWidthFcn, setHeightFcn; // Functions to resize the node
+ var getWidthFcn,getHeightFcn; // Functions to get node sizes
var startPos = {};
var tmpActiveBgOpacity;
- var eMouseDown = function () {
- cy.trigger("noderesize.resizestart", [t, node]);
+ // BBox of children of a node. Of course is valid if the node is a compound.
+ var childrenBBox;
+
+ // helper object
+ var translateLocation = {
+ "topleft": "nw",
+ "topcenter": "n",
+ "topright": "ne",
+ "centerright": "e",
+ "bottomright": "se",
+ "bottomcenter": "s",
+ "bottomleft": "sw",
+ "centerleft": "w"
+ };
+
+ 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()
.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.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.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);
+
};
- 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 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) {
- var aspectRatio = node.height() / node.width();
+ var aspectRatio = getHeightFcn(node) / getWidthFcn(node);
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();
+ var newX = nodePos.x;
+ var newY = nodePos.y;
+ 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 (t.startsWith("top")) {
- if (node.height() - xHeight > options.minHeight(node)) {
- node.position("y", nodePos.y + xHeight / 2);
- node.css("height", node.height() - xHeight);
+ if (location.startsWith("top")) {
+ // 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;
+ setHeightFcn(node, getHeightFcn(node) - 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);
+ } else if (location.startsWith("bottom")) {
+ // 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;
+ setHeightFcn(node, getHeightFcn(node) + 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);
- } else if (t.endsWith("right") && node.width() + xWidth > options.minWidth(node)) {
- node.position("x", nodePos.x + xWidth / 2);
- node.css("width", node.width() + xWidth);
+ 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;
+ 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;
+ setWidthFcn(node, getWidthFcn(node) + xWidth);
+ }
+
+ // this will trigger a position event, leading to useless redraw.
+ // TODO find a way to avoid that
+ 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;
startPos.y = y;
-
- cy.trigger("noderesize.resizedrag", [t, node]);
- };
+ self.resizeControls.update(); // redundant update if the position has changed just before
- var eMouseEnter = function () {
- canvas.mouse.cursor(cur);
- grapple.bind("touchleave mouseleave", eMouseLeave);
+ cy.trigger("noderesize.resizedrag", [location, node]);
};
- 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);
-
+ var eMouseLeave = function (event) {
+ event.target.getStage().container().style.cursor = options.cursors.default;
+ };
- return grapple;
+ 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.unbindAllEvents = function () {
+ this.shape.off('mouseenter');
+ this.shape.off('mouseleave');
+ this.shape.off('touchstart mousedown');
+ };
- // 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);
+ 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 getGrappleSize = function (node) {
+ return Math.max(1, cy.zoom()) * options.grappleSize * Math.min(node.width()/25, node.height()/25, 1);
};
- 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);
- }
+ var getPadding = function () {
+ return options.padding*Math.max(1, cy.zoom());
};
+ function getTopMostNodes(nodes) {
+ var nodesMap = {};
+ for (var i = 0; i < nodes.length; i++) {
+ nodesMap[nodes[i].id()] = true;
+ }
+ 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()]){
+ 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(node, i) {
+ if(typeof node === "number") {
+ node = i;
+ }
+ var oldX = node.position("x");
+ var oldY = node.position("y");
+ return {
+ x: oldX + positionDiff.x,
+ y: oldY + positionDiff.y
+ };
+ });
+ }
+
+ 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;
+ }
+
+ cy.trigger("noderesize.moveend", [selectedNodesToMove]);
+ selectedNodesToMove = undefined;
+ nodesMoving = false;
+ }
+
var unBindEvents = function() {
cy.off("unselect", "node", eUnselectNode);
cy.off("position", "node", ePositionNode);
@@ -644,148 +1031,278 @@
cy.off("select", "node", eSelectNode);
cy.off("remove", "node", eRemoveNode);
cy.off("add", "node", eAddNode);
+ cy.off("afterUndo afterRedo", eUndoRedo);
};
var bindEvents = function() {
- cy.on("unselect", "node", eUnselectNode = function() {
- 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;
+ // declare old and current positions
+ var oldPos = {x: undefined, y: undefined};
+ var currentPos = {x : 0, y : 0};
+ cy.on("unselect", "node", eUnselectNode = function(e) {
+ // reinitialize old and current compound positions
+ oldPos = {x: undefined, y: undefined};
+ currentPos = {x: 0, y: 0};
+
+ if(controls) {
+ controls.remove();
+ controls = null;
}
- refreshGrapples();
+ var selectedNodes = cy.nodes(':selected');
+ if(selectedNodes.size() == 1) {
+ controls = new ResizeControls(selectedNodes);
+ }
});
-
- cy.on("select", "node", eSelectNode = function() {
- var node = this;
- numberOfSelectedNodes = numberOfSelectedNodes + 1;
+ cy.on("select", "node", eSelectNode = function(e) {
+ var node = e.target;
- if (numberOfSelectedNodes === 1) {
- nodeToDrawGrapples = node;
+ if(controls) {
+ controls.remove();
+ controls = null;
}
- else {
- nodeToDrawGrapples = undefined;
+
+ var selectedNodes = cy.nodes(':selected');
+ if(selectedNodes.size() == 1) {
+ controls = new ResizeControls(selectedNodes);
}
- refreshGrapples();
});
-
- 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);
}
});
-
- cy.on("add", "node", eAddNode = function() {
- var node = this;
+
+ // 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(e) {
+ var node = e.target;
// If a selected node is added we should regard this event just like a select event
if ( node.selected() ) {
- eSelectNode();
+ eSelectNode(e);
}
});
-
- cy.on("position", "node", ePositionNode = function() {
- var node = this;
- if ( nodeToDrawGrapples && nodeToDrawGrapples.id() === node.id() ) {
- refreshGrapples();
+
+ // 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) {
+ currentPos = controls.parent.position();
+ controls.update();
+ oldPos = {x : currentPos.x, y : currentPos.y};
+ }
}
});
-
- /*
- * 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();
- }
- });
-
+
cy.on("zoom", eZoom = function() {
- if ( nodeToDrawGrapples ) {
- refreshGrapples();
+ if ( controls ) {
+ controls.update();
}
});
-
+
cy.on("pan", ePan = function() {
- if ( nodeToDrawGrapples ) {
- refreshGrapples();
- }
+ if ( controls ) {
+ controls.update();
+ }
+ });
+
+ cy.on("afterUndo afterRedo", eUndoRedo = function() {
+ if ( controls ) {
+ controls.update();
+ oldPos = {x: undefined, y: undefined};
+ }
});
- //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;
+
+ // 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);
param = undefined;
});
+ cy.on("noderesize.movestart", function (e, nodes) {
+ if (nodes[0] != undefined)
+ {
+ moveparam = {
+ firstTime: true,
+ firstNodePosition: {
+ x: nodes[0].position('x'),
+ y: nodes[0].position('y')
+ },
+ nodes: nodes
+ }
+ }
+ });
+
+ cy.on("noderesize.moveend", function (e, nodes) {
+ if (moveparam != undefined)
+ {
+ 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 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
+ }
delete arg.firstTime;
return arg;
}
-
+
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)
- .css("width", arg.css.width)
- .css("height", 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();
- refreshGrapples(); // refresh grapplers after node resize
+ 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
+ }
+
+ 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);
}
-
- 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);
+ }
+ }
+ // Simply remove grapples even if node is selected
+ api.removeGrapples = function() {
+ if (controls) {
+ controls.remove();
+ controls = null;
+ }
+ }
+ return api; // Return the api
});
};
@@ -800,8 +1317,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);
}
-})();
\ No newline at end of file
+})();
diff --git a/demo.html b/demo.html
index 4f14dc3..428a908 100644
--- a/demo.html
+++ b/demo.html
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/package.json b/package.json
index 66b8ce9..b791de3 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,10 @@
"url": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize/issues"
},
"homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-node-resize",
+ "peerDependencies": {
+ "cytoscape": "^3.0.0",
+ "konva": "^1.6.3"
+ },
"devDependencies": {
"gulp": "^3.8.8",
"gulp-jshint": "^1.8.5",
diff --git a/undoable_demo.html b/undoable_demo.html
new file mode 100644
index 0000000..9875d6e
--- /dev/null
+++ b/undoable_demo.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
cytoscape-node-resize.js demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
cytoscape-node-resize demo
+
+
+
+
+
+