6 * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
9 * This file contains the jsPlumb connector editors. It is not deployed wth the released versions of jsPlumb; you need to
10 * include it as an extra script.
12 * Copyright (c) 2010 - 2013 Simon Porritt (simon.porritt@gmail.com)
15 * http://github.com/sporritt/jsplumb
16 * http://code.google.com/p/jsplumb
18 * Dual licensed under the MIT and GPL2 licenses.
22 var AbstractEditor = function(params) {
26 var isTouchDevice = "ontouchstart" in document.documentElement,
27 downEvent = isTouchDevice ? "touchstart" : "mousedown",
28 upEvent = isTouchDevice ? "touchend" : "mouseup",
29 moveEvent = isTouchDevice ? "touchmove" : "mousemove";
31 // TODO: this is for a Straight segment.it would be better to have these all available somewjere, keyed
33 var findClosestPointOnPath = function(seg, x, y, i, bounds) {
34 var m = seg[0] == seg[2] ? Infinity : 0,
36 out = { s:seg, m:m, i:i, x:-1, y:-1, d:Infinity };
39 // a horizontal line. if x is in the range of this line then distance is delta y. otherwise we consider it to be
41 if ( (seg[0] <= x && x <= seg[2]) || (seg[2] <= x && x <= seg[0])) {
44 out.d = Math.abs(y - seg[1]);
47 else if (m == Infinity || m == -Infinity) {
48 // a vertical line. if y is in the range of this line then distance is delta x. otherwise we consider it to be
50 if ((seg[1] <= y && y <= seg[3]) || (seg[3] <= y && y <= seg[1])){
53 out.d = Math.abs(x - seg[0]);
57 // closest point lies on normal from given point to this line.
58 var b = seg[1] - (m * seg[0]),
61 // y1 = m.x1 + b and y1 = m2.x1 + b2
62 // so: m.x1 + b = m2.x1 + b2
63 // x1(m - m2) = b2 - b
64 // x1 = (b2 - b) / (m - m2)
65 _x1 = (b2 -b) / (m - m2),
67 d = jsPlumbGeom.lineLength([ x, y ], [ _x1, _y1 ]),
68 fractionInSegment = jsPlumbGeom.lineLength([ _x1, _y1 ], [ seg[0], seg[1] ]);
73 out.l = fractionInSegment / length;
79 * @namespace jsPlumb.ConnectorEditors
80 * @desc These are editors for the various connector types. They are not included in the
81 * main jsPlumb release. To use them you have to build a custom version of jsPlumb - see
82 * the Gruntfile for information on how to do that.
84 * Currently there is only an editor for the Flowchart connector.
86 jsPlumb.ConnectorEditors = {
88 * @name jsPlumb.ConnectorEditors.FlowchartConnectorEditor
90 * @classdesc Lets you drag the segments of a flowchart connection around. If you subsequently
91 * drag an element, your edits are lost.
93 "Flowchart":function(params) {
94 AbstractEditor.apply(this, arguments);
96 var jpcl = jsPlumb.CurrentLibrary,
97 clickConsumer = function(conn) {
98 conn._jsPlumb.afterEditClick = function() {
99 console.log("after edit click");
100 conn.unbind("click", conn._jsPlumb.afterEditClick);
101 conn._jsPlumb.afterEditClick = null;
104 conn.bind("click", conn._jsPlumb.afterEditClick, true);
106 documentMouseUp = function(e) {
108 // an attempt at consuming the click that occurs after this mouseup
109 // it's not reliable though, as we dont always get a click fired, for some
112 // clickConsumer(params.connection);
114 jpcl.removeClass(document.body, params.connection._jsPlumb.instance.dragSelectClass);
115 params.connection._jsPlumb.instance.setConnectionBeingDragged(false);
118 jpcl.unbind(document, upEvent, documentMouseUp);
119 jpcl.unbind(document, moveEvent, documentMouseMove);
121 currentSegments = null;
122 selectedSegment = null;
123 segmentCoords = null;
124 params.connection.setHover(false);
125 params.connector.setSuspendEvents(false);
126 params.connection.endpoints[0].setSuspendEvents(false);
127 params.connection.endpoints[1].setSuspendEvents(false);
128 params.connection.editCompleted();
129 params.connector.justEdited = editing;
133 currentSegments = null,
134 selectedSegment = null,
135 segmentCoords = null,
137 anchorsMoveable = params.params.anchorsMoveable,
138 sgn = function(p1, p2) {
140 return p1[1] < p2[1] ? 1 : -1;
142 return p1[0] < p2[0] ? 1 : -1;
144 // collapses currentSegments by joining subsequent segments that are in the
145 // same axis. we do this because it doesn't matter about stubs any longer once a user
146 // is editing a connector. so it is best to reduce the number of segments to the
148 _collapseSegments = function() {
149 var _last = null, _lastAxis = null, s = [];
150 for (var i = 0; i < currentSegments.length; i++) {
151 var seg = currentSegments[i], axis = seg[4], axisIndex = (axis == "v" ? 3 : 2);
152 if (_last != null && _lastAxis === axis) {
153 _last[axisIndex] = seg[axisIndex];
163 // attempt to shift anchor
164 _shiftAnchor = function(endpoint, horizontal, value) {
165 var elementSize = jpcl.getSize(endpoint.element),
166 sizeValue = elementSize[horizontal ? 1 : 0],
167 ee = jpcl.getElementObject(endpoint.element),
168 off = jpcl.getOffset(ee),
169 cc = jpcl.getElementObject(params.connector.canvas.parentNode),
170 co = jpcl.getOffset(cc),
171 offValue = off[horizontal ? "top" : "left"] - co[horizontal ? "top" : "left"],
172 ap = endpoint.anchor.getCurrentLocation({element:endpoint}),
173 desiredLoc = horizontal ? params.connector.y + value : params.connector.x + value;
175 if (anchorsMoveable) {
177 if (offValue < desiredLoc && desiredLoc < offValue + sizeValue) {
178 // if still on the element, okay to move.
179 var udl = [ ap[0], ap[1] ];
180 ap[horizontal ? 1 : 0] = desiredLoc;
181 endpoint.anchor.setUserDefinedLocation(ap);
185 // otherwise, clamp to element edge
186 var edgeVal = desiredLoc < offValue ? offValue : offValue + sizeValue;
187 return edgeVal - (horizontal ? params.connector.y: params.connector.x);
191 // otherwise, return the current anchor point.
192 return ap[horizontal ? 1 : 0] - params.connector[horizontal ? "y" : "x"];
195 _updateSegmentOrientation = function(seg) {
196 if (seg[0] != seg[2]) seg[5] = (seg[0] < seg[2]) ? 1 : -1;
197 if (seg[1] != seg[3]) seg[6] = (seg[1] < seg[3]) ? 1 : -1;
199 documentMouseMove = function(e) {
200 if (selectedSegment != null) {
201 // suspend events on first move.
203 params.connection.setHover(true);
204 params.connector.setSuspendEvents(true);
205 params.connection.endpoints[0].setSuspendEvents(true);
206 params.connection.endpoints[1].setSuspendEvents(true);
209 var m = selectedSegment.m, s = selectedSegment.s,
210 x = (e.pageX || e.page.x), y = (e.pageY || e.page.y),
211 dx = m == 0 ? 0 : x - downAt[0], dy = m == 0 ? y - downAt[1] : 0,
212 newX1 = segmentCoords[0] + dx,
213 newY1 = segmentCoords[1] + dy,
214 newX2 = segmentCoords[2] + dx,
215 newY2 = segmentCoords[3] + dy,
216 horizontal = s[4] == "h";
218 // so here we know the new x,y values we would like to set for the start
219 // and end of this segment. but we may not be able to set these values: if this
220 // is the first segment, for example, then we are constrained by how far the anchor
221 // can move (before it slides off its element). same thing goes if this is the last
222 // segment. if this is not the first or last segment then there are other considerations.
223 // we know, from having run collapse segments, that there will never be two
224 // consecutive segments that are not at right angles to each other, so what we need to
225 // know is whether we can adjust the endpoint of the previous segment to the values we
226 // want, and the same question for the start values of the next segment. the answer to
227 // that is whether or not the segment in question would be rendered too small by such
228 // a change. if that is the case (and the same goes for anchors) then we want to know
229 // what an agreeable value is, and we use that.
231 if (selectedSegment.i == 0) {
233 var anchorLoc = _shiftAnchor(params.connection.endpoints[0], horizontal, horizontal ? newY1 : newX1);
235 newY1 = newY2 = anchorLoc;
237 newX1 = newX2 = anchorLoc;
239 currentSegments[1][0] = newX2;
240 currentSegments[1][1] = newY2;
241 _updateSegmentOrientation(currentSegments[1]);
243 else if (selectedSegment.i == currentSegments.length - 1) {
244 var anchorLoc = _shiftAnchor(params.connection.endpoints[1], horizontal, horizontal ? newY1 : newX1);
246 newY1 = newY2 = anchorLoc;
248 newX1 = newX2 = anchorLoc;
250 currentSegments[currentSegments.length - 2][2] = newX1;
251 currentSegments[currentSegments.length - 2][3] = newY1;
252 _updateSegmentOrientation(currentSegments[currentSegments.length - 2]);
256 currentSegments[selectedSegment.i - 1][2] = newX1;
257 currentSegments[selectedSegment.i + 1][0] = newX2;
260 currentSegments[selectedSegment.i - 1][3] = newY1;
261 currentSegments[selectedSegment.i + 1][1] = newY2;
263 _updateSegmentOrientation(currentSegments[selectedSegment.i + 1]);
264 _updateSegmentOrientation(currentSegments[selectedSegment.i - 1]);
272 params.connector.setSegments(currentSegments);
273 params.connection.repaint();
274 params.connection.endpoints[0].repaint();
275 params.connection.endpoints[1].repaint();
276 params.connector.setEdited(true);
282 //bind to mousedown and mouseup, for editing
283 params.connector.bind(downEvent, function(c, e) {
284 var x = (e.pageX || e.page.x),
285 y = (e.pageY || e.page.y),
286 oe = jpcl.getElementObject(params.connection.getConnector().canvas),
287 o = jpcl.getOffset(oe),
290 // TODO this is really the way we want to go: get the segment from the connector.
291 // for now it's just here to remind me what to change.
292 var __seg = params.connector.findSegmentForPoint(x-o.left, y-o.top);
295 currentSegments = params.connector.getOriginalSegments();
297 for (var i = 0; i < currentSegments.length; i++) {
298 var _s = findClosestPointOnPath(currentSegments[i], (x - o.left) , (y - o.top), i, params.connector.bounds);
300 //var _s = currentSegments[i].findClosestPointOnPath(x - o.left, y - o.top);
303 selectedSegment = _s;
304 segmentCoords = [ _s.s[0], _s.s[1], _s.s[2], _s.s[3] ]; // copy the coords at mousedown
311 if (selectedSegment != null) {
312 jpcl.bind(document, upEvent, documentMouseUp);
313 jpcl.bind(document, moveEvent, documentMouseMove);
314 jpcl.addClass(document.body, params.connection._jsPlumb.instance.dragSelectClass);
315 params.connection._jsPlumb.instance.setConnectionBeingDragged(true);
316 params.connection.editStarted();
323 jsPlumb.Connectors.AbstractConnector.prototype.shouldFireEvent = function(type, value, originalEvent) {
324 var out = !this.justEdited;
325 if (type == "click") {
326 this.justEdited = false;
331 // ------------------ augment the Connection prototype with the editing stuff --------------------------
333 var EDIT_STARTED = "editStarted", EDIT_COMPLETED = "editCompleted", EDIT_CANCELED = "editCanceled";
335 jsPlumb.Connection.prototype.setEditable = function(e) {
336 if (this.connector && this.connector.isEditable())
337 this._jsPlumb.editable = e;
339 return this._jsPlumb.editable;
342 jsPlumb.Connection.prototype.isEditable = function() { return this._jsPlumb.editable; };
344 jsPlumb.Connection.prototype.editStarted = function() {
345 this.setSuspendEvents(true);
346 this.fire(EDIT_STARTED, {
347 path:this.connector.getPath()
349 this._jsPlumb.instance.setHoverSuspended(true);
352 jsPlumb.Connection.prototype.editCompleted = function() {
353 this.fire(EDIT_COMPLETED, {
354 path:this.connector.getPath()
356 this.setSuspendEvents(false);
357 this._jsPlumb.instance.setHoverSuspended(false);
358 this.setHover(false);
361 jsPlumb.Connection.prototype.editCanceled = function() {
362 this.fire(EDIT_CANCELED, {
363 path:this.connector.getPath()
365 this._jsPlumb.instance.setHoverSuspended(false);
366 this.setHover(false);