dynamic display of link map

This commit is contained in:
zadam 2019-06-09 19:18:14 +02:00
parent 412b0377e9
commit 4329675c60
3 changed files with 388 additions and 123 deletions

View File

@ -2104,14 +2104,9 @@
sel = k.getSelection(), sel = k.getSelection(),
dPos = this.params.getPosition(dragEl); dPos = this.params.getPosition(dragEl);
if (sel.length > 0) { for (var i = 0; i < sel.length; i++) {
for (var i = 0; i < sel.length; i++) { var p = this.params.getPosition(sel[i].el);
var p = this.params.getPosition(sel[i].el); positions.push([ sel[i].el, { left: p[0], top: p[1] }, sel[i] ]);
positions.push([ sel[i].el, { left: p[0], top: p[1] }, sel[i] ]);
}
}
else {
positions.push([ dragEl, {left:dPos[0], top:dPos[1]}, this ]);
} }
_dispatch("stop", { _dispatch("stop", {
@ -3540,6 +3535,231 @@
}).call(typeof window !== 'undefined' ? window : this); }).call(typeof window !== 'undefined' ? window : this);
;(function() {
var DEFAULT_OPTIONS = {
deriveAnchor:function(edge, index, ep, conn) {
return {
top:["TopRight", "TopLeft"],
bottom:["BottomRight", "BottomLeft"]
}[edge][index];
}
};
var root = this;
var ListManager = function(jsPlumbInstance) {
this.count = 0;
this.instance = jsPlumbInstance;
this.lists = {};
this.instance.addList = function(el, options) {
return this.listManager.addList(el, options);
};
this.instance.removeList = function(el) {
this.listManager.removeList(el);
};
this.instance.bind("manageElement", function(p) {
//look for [jtk-scrollable-list] elements and attach scroll listeners if necessary
var scrollableLists = this.instance.getSelector(p.el, "[jtk-scrollable-list]");
for (var i = 0; i < scrollableLists.length; i++) {
this.addList(scrollableLists[i]);
}
}.bind(this));
this.instance.bind("unmanageElement", function(p) {
this.removeList(p.el);
});
this.instance.bind("connection", function(c, evt) {
if (evt == null) {
// not added by mouse. look for an ancestor of the source and/or target element that is a scrollable list, and run
// its scroll method.
this._maybeUpdateParentList(c.source);
this._maybeUpdateParentList(c.target);
}
}.bind(this));
};
root.jsPlumbListManager = ListManager;
ListManager.prototype = {
addList : function(el, options) {
var dp = this.instance.extend({}, DEFAULT_OPTIONS);
options = this.instance.extend(dp, options || {});
var id = [this.instance.getInstanceIndex(), this.count++].join("_");
this.lists[id] = new List(this.instance, el, options, id);
},
removeList:function(el) {
var list = this.lists[el._jsPlumbList];
if (list) {
list.destroy();
delete this.lists[el._jsPlumbList];
}
},
_maybeUpdateParentList:function (el) {
var parent = el.parentNode, container = this.instance.getContainer();
while(parent != null && parent !== container) {
if (parent._jsPlumbList != null && this.lists[parent._jsPlumbList] != null) {
parent._jsPlumbScrollHandler();
return
}
parent = parent.parentNode;
}
}
};
var List = function(instance, el, options, id) {
el["_jsPlumbList"] = id;
//
// Derive an anchor to use for the current situation. In contrast to the way we derive an endpoint, here we use `anchor` from the options, if present, as
// our first choice, and then `deriveAnchor` as our next choice. There is a default `deriveAnchor` implementation that uses TopRight/TopLeft for top and
// BottomRight/BottomLeft for bottom.
//
// edge - "top" or "bottom"
// index - 0 when endpoint is connection source, 1 when endpoint is connection target
// ep - the endpoint that is being proxied
// conn - the connection that is being proxied
//
function deriveAnchor(edge, index, ep, conn) {
return options.anchor ? options.anchor : options.deriveAnchor(edge, index, ep, conn);
}
//
// Derive an endpoint to use for the current situation. We'll use a `deriveEndpoint` function passed in to the options as our first choice,
// followed by `endpoint` (an endpoint spec) from the options, and failing either of those we just use the `type` of the endpoint that is being proxied.
//
// edge - "top" or "bottom"
// index - 0 when endpoint is connection source, 1 when endpoint is connection target
// endpoint - the endpoint that is being proxied
// connection - the connection that is being proxied
//
function deriveEndpoint(edge, index, ep, conn) {
return options.deriveEndpoint ? options.deriveEndpoint(edge, index, ep, conn) : options.endpoint ? options.endpoint : ep.type;
}
//
// look for a parent of the given scrollable list that is draggable, and then update the child offsets for it. this should not
// be necessary in the delegated drag stuff from the upcoming 3.0.0 release.
//
function _maybeUpdateDraggable(el) {
var parent = el.parentNode, container = instance.getContainer();
while(parent != null && parent !== container) {
if (instance.hasClass(parent, "jtk-managed")) {
instance.recalculateOffsets(parent);
return
}
parent = parent.parentNode;
}
}
var scrollHandler = function(e) {
var children = instance.getSelector(el, ".jtk-managed");
var elId = instance.getId(el);
for (var i = 0; i < children.length; i++) {
if (children[i].offsetTop < el.scrollTop) {
if (!children[i]._jsPlumbProxies) {
children[i]._jsPlumbProxies = children[i]._jsPlumbProxies || [];
instance.select({source: children[i]}).each(function (c) {
instance.proxyConnection(c, 0, el, elId, function () {
return deriveEndpoint("top", 0, c.endpoints[0], c);
}, function () {
return deriveAnchor("top", 0, c.endpoints[0], c);
});
children[i]._jsPlumbProxies.push([c, 0]);
});
instance.select({target: children[i]}).each(function (c) {
instance.proxyConnection(c, 1, el, elId, function () {
return deriveEndpoint("top", 1, c.endpoints[1], c);
}, function () {
return deriveAnchor("top", 1, c.endpoints[1], c);
});
children[i]._jsPlumbProxies.push([c, 1]);
});
}
}
//
else if (children[i].offsetTop > el.scrollTop + el.offsetHeight) {
if (!children[i]._jsPlumbProxies) {
children[i]._jsPlumbProxies = children[i]._jsPlumbProxies || [];
instance.select({source: children[i]}).each(function (c) {
instance.proxyConnection(c, 0, el, elId, function () {
return deriveEndpoint("bottom", 0, c.endpoints[0], c);
}, function () {
return deriveAnchor("bottom", 0, c.endpoints[0], c);
});
children[i]._jsPlumbProxies.push([c, 0]);
});
instance.select({target: children[i]}).each(function (c) {
instance.proxyConnection(c, 1, el, elId, function () {
return deriveEndpoint("bottom", 1, c.endpoints[1], c);
}, function () {
return deriveAnchor("bottom", 1, c.endpoints[1], c);
});
children[i]._jsPlumbProxies.push([c, 1]);
});
}
} else if (children[i]._jsPlumbProxies) {
for (var j = 0; j < children[i]._jsPlumbProxies.length; j++) {
instance.unproxyConnection(children[i]._jsPlumbProxies[j][0], children[i]._jsPlumbProxies[j][1], elId);
}
delete children[i]._jsPlumbProxies;
}
instance.revalidate(children[i]);
}
_maybeUpdateDraggable(el);
};
instance.setAttribute(el, "jtk-scrollable-list", "true");
el._jsPlumbScrollHandler = scrollHandler;
instance.on(el, "scroll", scrollHandler);
scrollHandler(); // run it once; there may be connections already.
this.destroy = function() {
instance.off(el, "scroll", scrollHandler);
delete el._jsPlumbScrollHandler;
var children = instance.getSelector(el, ".jtk-managed");
var elId = instance.getId(el);
for (var i = 0; i < children.length; i++) {
if (children[i]._jsPlumbProxies) {
for (var j = 0; j < children[i]._jsPlumbProxies.length; j++) {
instance.unproxyConnection(children[i]._jsPlumbProxies[j][0], children[i]._jsPlumbProxies[j][1], elId);
}
delete children[i]._jsPlumbProxies;
}
}
};
};
}).call(typeof window !== 'undefined' ? window : this);
/* /*
* This file contains the core code. * This file contains the core code.
* *
@ -4006,7 +4226,7 @@
var jsPlumbInstance = root.jsPlumbInstance = function (_defaults) { var jsPlumbInstance = root.jsPlumbInstance = function (_defaults) {
this.version = "2.9.3"; this.version = "2.10.0";
this.Defaults = { this.Defaults = {
Anchor: "Bottom", Anchor: "Bottom",
@ -5470,6 +5690,7 @@
managedElements[id].info = _updateOffset({ elId: id, timestamp: _suspendedAt }); managedElements[id].info = _updateOffset({ elId: id, timestamp: _suspendedAt });
_currentInstance.addClass(element, "jtk-managed"); _currentInstance.addClass(element, "jtk-managed");
if (!_transient) { if (!_transient) {
_currentInstance.fire("manageElement", { id:id, info:managedElements[id].info, el:element }); _currentInstance.fire("manageElement", { id:id, info:managedElements[id].info, el:element });
} }
@ -5480,9 +5701,10 @@
var _unmanage = _currentInstance.unmanage = function(id) { var _unmanage = _currentInstance.unmanage = function(id) {
if (managedElements[id]) { if (managedElements[id]) {
_currentInstance.removeClass(managedElements[id].el, "jtk-managed"); var el = managedElements[id].el;
_currentInstance.removeClass(el, "jtk-managed");
delete managedElements[id]; delete managedElements[id];
_currentInstance.fire("unmanageElement", id); _currentInstance.fire("unmanageElement", {id:id, el:el});
} }
}; };
@ -6619,6 +6841,8 @@
this.getFloatingConnectionFor = function(id) { this.getFloatingConnectionFor = function(id) {
return floatingConnections[id]; return floatingConnections[id];
}; };
this.listManager = new root.jsPlumbListManager(this);
}; };
_ju.extend(root.jsPlumbInstance, _ju.EventGenerator, { _ju.extend(root.jsPlumbInstance, _ju.EventGenerator, {
@ -6709,6 +6933,86 @@
floatingConnections: {}, floatingConnections: {},
getFloatingAnchorIndex: function (jpc) { getFloatingAnchorIndex: function (jpc) {
return jpc.endpoints[0].isFloating() ? 0 : jpc.endpoints[1].isFloating() ? 1 : -1; return jpc.endpoints[0].isFloating() ? 0 : jpc.endpoints[1].isFloating() ? 1 : -1;
},
proxyConnection :function(connection, index, proxyEl, proxyElId, endpointGenerator, anchorGenerator) {
var proxyEp,
originalElementId = connection.endpoints[index].elementId,
originalEndpoint = connection.endpoints[index];
connection.proxies = connection.proxies || [];
if(connection.proxies[index]) {
proxyEp = connection.proxies[index].ep;
}else {
proxyEp = this.addEndpoint(proxyEl, {
endpoint:endpointGenerator(connection, index),
anchor:anchorGenerator(connection, index),
parameters:{
isProxyEndpoint:true
}
});
}
proxyEp.setDeleteOnEmpty(true);
// for this index, stash proxy info: the new EP, the original EP.
connection.proxies[index] = { ep:proxyEp, originalEp: originalEndpoint };
// and advise the anchor manager
if (index === 0) {
// TODO why are there two differently named methods? Why is there not one method that says "some end of this
// connection changed (you give the index), and here's the new element and element id."
this.anchorManager.sourceChanged(originalElementId, proxyElId, connection, proxyEl);
}
else {
this.anchorManager.updateOtherEndpoint(connection.endpoints[0].elementId, originalElementId, proxyElId, connection);
connection.target = proxyEl;
connection.targetId = proxyElId;
}
// detach the original EP from the connection.
originalEndpoint.detachFromConnection(connection, null, true);
// set the proxy as the new ep
proxyEp.connections = [ connection ];
connection.endpoints[index] = proxyEp;
originalEndpoint.setVisible(false);
connection.setVisible(true);
this.revalidate(proxyEl);
},
unproxyConnection : function(connection, index, proxyElId) {
// if connection cleaned up, no proxies, or none for this end of the connection, abort.
if (connection._jsPlumb == null || connection.proxies == null || connection.proxies[index] == null) {
return;
}
var originalElement = connection.proxies[index].originalEp.element,
originalElementId = connection.proxies[index].originalEp.elementId;
connection.endpoints[index] = connection.proxies[index].originalEp;
// and advise the anchor manager
if (index === 0) {
// TODO why are there two differently named methods? Why is there not one method that says "some end of this
// connection changed (you give the index), and here's the new element and element id."
this.anchorManager.sourceChanged(proxyElId, originalElementId, connection, originalElement);
}
else {
this.anchorManager.updateOtherEndpoint(connection.endpoints[0].elementId, proxyElId, originalElementId, connection);
connection.target = originalElement;
connection.targetId = originalElementId;
}
// detach the proxy EP from the connection (which will cause it to be removed as we no longer need it)
connection.proxies[index].ep.detachFromConnection(connection, null);
connection.proxies[index].originalEp.addConnection(connection);
if(connection.isVisible()) {
connection.proxies[index].originalEp.setVisible(true);
}
// cleanup
delete connection.proxies[index];
} }
}); });
@ -10183,15 +10487,15 @@
}, },
_path = function (segments) { _path = function (segments) {
var anchorsPerFace = anchorCount / segments.length, a = [], var anchorsPerFace = anchorCount / segments.length, a = [],
_computeFace = function (x1, y1, x2, y2, fractionalLength) { _computeFace = function (x1, y1, x2, y2, fractionalLength, ox, oy) {
anchorsPerFace = anchorCount * fractionalLength; anchorsPerFace = anchorCount * fractionalLength;
var dx = (x2 - x1) / anchorsPerFace, dy = (y2 - y1) / anchorsPerFace; var dx = (x2 - x1) / anchorsPerFace, dy = (y2 - y1) / anchorsPerFace;
for (var i = 0; i < anchorsPerFace; i++) { for (var i = 0; i < anchorsPerFace; i++) {
a.push([ a.push([
x1 + (dx * i), x1 + (dx * i),
y1 + (dy * i), y1 + (dy * i),
0, ox == null ? 0 : ox,
0 oy == null ? 0 : oy
]); ]);
} }
}; };
@ -10205,16 +10509,16 @@
_shape = function (faces) { _shape = function (faces) {
var s = []; var s = [];
for (var i = 0; i < faces.length; i++) { for (var i = 0; i < faces.length; i++) {
s.push([faces[i][0], faces[i][1], faces[i][2], faces[i][3], 1 / faces.length]); s.push([faces[i][0], faces[i][1], faces[i][2], faces[i][3], 1 / faces.length, faces[i][4], faces[i][5]]);
} }
return _path(s); return _path(s);
}, },
_rectangle = function () { _rectangle = function () {
return _shape([ return _shape([
[ 0, 0, 1, 0 ], [ 0, 0, 1, 0, 0, -1 ],
[ 1, 0, 1, 1 ], [ 1, 0, 1, 1, 1, 0 ],
[ 1, 1, 0, 1 ], [ 1, 1, 0, 1, 0, 1 ],
[ 0, 1, 0, 0 ] [ 0, 1, 0, 0, -1, 0 ]
]); ]);
}; };
@ -12115,11 +12419,11 @@
c.setVisible(false); c.setVisible(false);
if (c.endpoints[oidx].element._jsPlumbGroup === group) { if (c.endpoints[oidx].element._jsPlumbGroup === group) {
c.endpoints[oidx].setVisible(false); c.endpoints[oidx].setVisible(false);
self.expandConnection(c, oidx, group); _expandConnection(c, oidx, group);
} }
else { else {
c.endpoints[index].setVisible(false); c.endpoints[index].setVisible(false);
self.collapseConnection(c, index, group); _collapseConnection(c, index, group);
} }
}); });
}; };
@ -12203,54 +12507,16 @@
} }
} }
var _collapseConnection = this.collapseConnection = function(c, index, group) { var _collapseConnection = function(c, index, group) {
var proxyEp, groupEl = group.getEl(), groupElId = _jsPlumb.getId(groupEl),
originalElementId = c.endpoints[index].elementId;
var otherEl = c.endpoints[index === 0 ? 1 : 0].element; var otherEl = c.endpoints[index === 0 ? 1 : 0].element;
if (otherEl[GROUP] && (!otherEl[GROUP].shouldProxy() && otherEl[GROUP].collapsed)) { if (otherEl[GROUP] && (!otherEl[GROUP].shouldProxy() && otherEl[GROUP].collapsed)) {
return; return;
} }
c.proxies = c.proxies || []; var groupEl = group.getEl(), groupElId = _jsPlumb.getId(groupEl);
if(c.proxies[index]) {
proxyEp = c.proxies[index].ep;
}else {
proxyEp = _jsPlumb.addEndpoint(groupEl, {
endpoint:group.getEndpoint(c, index),
anchor:group.getAnchor(c, index),
parameters:{
isProxyEndpoint:true
}
});
}
proxyEp.setDeleteOnEmpty(true);
// for this index, stash proxy info: the new EP, the original EP. _jsPlumb.proxyConnection(c, index, groupEl, groupElId, function(c, index) { return group.getEndpoint(c, index); }, function(c, index) { return group.getAnchor(c, index); });
c.proxies[index] = { ep:proxyEp, originalEp: c.endpoints[index] };
// and advise the anchor manager
if (index === 0) {
// TODO why are there two differently named methods? Why is there not one method that says "some end of this
// connection changed (you give the index), and here's the new element and element id."
_jsPlumb.anchorManager.sourceChanged(originalElementId, groupElId, c, groupEl);
}
else {
_jsPlumb.anchorManager.updateOtherEndpoint(c.endpoints[0].elementId, originalElementId, groupElId, c);
c.target = groupEl;
c.targetId = groupElId;
}
// detach the original EP from the connection.
c.proxies[index].originalEp.detachFromConnection(c, null, true);
// set the proxy as the new ep
proxyEp.connections = [ c ];
c.endpoints[index] = proxyEp;
c.setVisible(true);
}; };
this.collapseGroup = function(group) { this.collapseGroup = function(group) {
@ -12287,37 +12553,8 @@
_jsPlumb.fire(EVT_COLLAPSE, { group:group }); _jsPlumb.fire(EVT_COLLAPSE, { group:group });
}; };
var _expandConnection = this.expandConnection = function(c, index, group) { var _expandConnection = function(c, index, group) {
_jsPlumb.unproxyConnection(c, index, _jsPlumb.getId(group.getEl()));
// if no proxies or none for this end of the connection, abort.
if (c.proxies == null || c.proxies[index] == null) {
return;
}
var groupElId = _jsPlumb.getId(group.getEl()),
originalElement = c.proxies[index].originalEp.element,
originalElementId = c.proxies[index].originalEp.elementId;
c.endpoints[index] = c.proxies[index].originalEp;
// and advise the anchor manager
if (index === 0) {
// TODO why are there two differently named methods? Why is there not one method that says "some end of this
// connection changed (you give the index), and here's the new element and element id."
_jsPlumb.anchorManager.sourceChanged(groupElId, originalElementId, c, originalElement);
}
else {
_jsPlumb.anchorManager.updateOtherEndpoint(c.endpoints[0].elementId, groupElId, originalElementId, c);
c.target = originalElement;
c.targetId = originalElementId;
}
// detach the proxy EP from the connection (which will cause it to be removed as we no longer need it)
c.proxies[index].ep.detachFromConnection(c, null);
c.proxies[index].originalEp.addConnection(c);
// cleanup
delete c.proxies[index];
}; };
this.expandGroup = function(group, doNotFireEvent) { this.expandGroup = function(group, doNotFireEvent) {

View File

@ -101,34 +101,58 @@ async function loadNotesAndRelations() {
0.5 // Damping 0.5 // Damping
); );
function getNoteBox(noteId) {
const noteBoxId = noteIdToId(noteId);
const $existingNoteBox = $("#" + noteBoxId);
if ($existingNoteBox.length > 0) {
return $existingNoteBox;
}
const note = notes.find(n => n.noteId === noteId);
const $noteBox = $("<div>")
.addClass("note-box")
.prop("id", noteBoxId)
.append($("<span>").addClass("title").append(note.title));
jsPlumbInstance.getContainer().appendChild($noteBox[0]);
return $noteBox;
}
const renderer = new Springy.Renderer( const renderer = new Springy.Renderer(
layout, layout,
() => {}, () => {}, //cleanup(),
(edge, p1, p2) => {
const connectionId = edge.source.id + '-' + edge.target.id;
if ($("#" + connectionId).length > 0) {
return;
}
getNoteBox(edge.source.id);
getNoteBox(edge.target.id);
const connection = jsPlumbInstance.connect({
source: noteIdToId(edge.source.id),
target: noteIdToId(edge.target.id),
type: 'relation' // FIXME
});
connection.canvas.id = connectionId;
},
(node, p) => {
const $noteBox = getNoteBox(node.id);
$noteBox
.css("left", (300 + p.x * 100) + "px")
.css("top", (300 + p.y * 100) + "px");
},
() => {}, () => {},
() => {}, () => {},
() => { () => {
layout.eachNode((node, point) => { jsPlumbInstance.repaintEverything();
console.log(node, point.p);
const note = notes.find(n => n.noteId === node.id);
const $noteBox = $("<div>")
.addClass("note-box")
.prop("id", noteIdToId(node.id))
.append($("<span>").addClass("title").append(note.title))
.css("left", (300 + point.p.x * 100) + "px")
.css("top", (300 + point.p.y * 100) + "px");
jsPlumbInstance.getContainer().appendChild($noteBox[0]);
});
for (const link of links) {
const connection = jsPlumbInstance.connect({
source: noteIdToId(link.noteId),
target: noteIdToId(link.targetNoteId),
type: link.type
});
}
} }
); );
@ -152,14 +176,18 @@ function initPanZoom() {
}); });
} }
function cleanup() {
// delete all endpoints and connections
// this is done at this point (after async operations) to reduce flicker to the minimum
jsPlumbInstance.deleteEveryEndpoint();
// without this we still end up with note boxes remaining in the canvas
$linkMapContainer.empty();
}
function initJsPlumbInstance() { function initJsPlumbInstance() {
if (jsPlumbInstance) { if (jsPlumbInstance) {
// delete all endpoints and connections cleanup();
// this is done at this point (after async operations) to reduce flicker to the minimum
jsPlumbInstance.deleteEveryEndpoint();
// without this we still end up with note boxes remaining in the canvas
$linkMapContainer.empty();
return; return;
} }

View File

@ -8,7 +8,7 @@
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body" style="outline: none;">
<div id="link-map-container"></div> <div id="link-map-container"></div>
</div> </div>
</div> </div>