cytoscape-expand-collapse
Version:
This extension provides an interface to expand-collapse nodes.
351 lines (287 loc) • 12 kB
JavaScript
var debounce = require('./debounce');
var debounce2 = require('./debounce2');
module.exports = function (params, cy, api) {
var elementUtilities;
var fn = params;
const CUE_POS_UPDATE_DELAY = 100;
var nodeWithRenderedCue;
const getData = function () {
var scratch = cy.scratch('_cyExpandCollapse');
return scratch && scratch.cueUtilities;
};
const setData = function (data) {
var scratch = cy.scratch('_cyExpandCollapse');
if (scratch == null) {
scratch = {};
}
scratch.cueUtilities = data;
cy.scratch('_cyExpandCollapse', scratch);
};
var functions = {
init: function () {
var $canvas = document.createElement('canvas');
$canvas.classList.add("expand-collapse-canvas");
var $container = cy.container();
var ctx = $canvas.getContext('2d');
$container.append($canvas);
elementUtilities = require('./elementUtilities')(cy);
var offset = function (elt) {
var rect = elt.getBoundingClientRect();
return {
top: rect.top + document.documentElement.scrollTop,
left: rect.left + document.documentElement.scrollLeft
}
}
var _sizeCanvas = debounce(function () {
$canvas.height = cy.container().offsetHeight;
$canvas.width = cy.container().offsetWidth;
$canvas.style.position = 'absolute';
$canvas.style.top = 0;
$canvas.style.left = 0;
$canvas.style.zIndex = options().zIndex;
setTimeout(function () {
var canvasBb = offset($canvas);
var containerBb = offset($container);
$canvas.style.top = -(canvasBb.top - containerBb.top);
$canvas.style.left = -(canvasBb.left - containerBb.left);
// refresh the cues on canvas resize
if (cy) {
clearDraws(true);
}
}, 0);
}, 250);
function sizeCanvas() {
_sizeCanvas();
}
sizeCanvas();
var data = {};
// if there are events field in data unbind them here
// to prevent binding the same event multiple times
// if (!data.hasEventFields) {
// functions['unbind'].apply( $container );
// }
function options() {
return cy.scratch('_cyExpandCollapse').options;
}
function clearDraws() {
var w = cy.width();
var h = cy.height();
ctx.clearRect(0, 0, w, h);
nodeWithRenderedCue = null;
}
function drawExpandCollapseCue(node) {
var children = node.children();
var collapsedChildren = node.data('collapsedChildren');
var hasChildren = children != null && children != undefined && children.length > 0;
// If this is a simple node with no collapsed children return directly
if (!hasChildren && !collapsedChildren) {
return;
}
var isCollapsed = node.hasClass('cy-expand-collapse-collapsed-node');
//Draw expand-collapse rectangles
var rectSize = options().expandCollapseCueSize;
var lineSize = options().expandCollapseCueLineSize;
var cueCenter;
if (options().expandCollapseCuePosition === 'top-left') {
var offset = 1;
var size = cy.zoom() < 1 ? rectSize / (2 * cy.zoom()) : rectSize / 2;
var nodeBorderWid = parseFloat(node.css('border-width'));
var x = node.position('x') - node.width() / 2 - parseFloat(node.css('padding-left'))
+ nodeBorderWid + size + offset;
var y = node.position('y') - node.height() / 2 - parseFloat(node.css('padding-top'))
+ nodeBorderWid + size + offset;
cueCenter = { x: x, y: y };
} else {
var option = options().expandCollapseCuePosition;
cueCenter = typeof option === 'function' ? option.call(this, node) : option;
}
var expandcollapseCenter = elementUtilities.convertToRenderedPosition(cueCenter);
// convert to rendered sizes
rectSize = Math.max(rectSize, rectSize * cy.zoom());
lineSize = Math.max(lineSize, lineSize * cy.zoom());
var diff = (rectSize - lineSize) / 2;
var expandcollapseCenterX = expandcollapseCenter.x;
var expandcollapseCenterY = expandcollapseCenter.y;
var expandcollapseStartX = expandcollapseCenterX - rectSize / 2;
var expandcollapseStartY = expandcollapseCenterY - rectSize / 2;
var expandcollapseRectSize = rectSize;
// Draw expand/collapse cue if specified use an image else render it in the default way
if (isCollapsed && options().expandCueImage) {
drawImg(options().expandCueImage, expandcollapseStartX, expandcollapseStartY, rectSize, rectSize);
}
else if (!isCollapsed && options().collapseCueImage) {
drawImg(options().collapseCueImage, expandcollapseStartX, expandcollapseStartY, rectSize, rectSize);
}
else {
var oldFillStyle = ctx.fillStyle;
var oldWidth = ctx.lineWidth;
var oldStrokeStyle = ctx.strokeStyle;
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
ctx.ellipse(expandcollapseCenterX, expandcollapseCenterY, rectSize / 2, rectSize / 2, 0, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "white";
ctx.lineWidth = Math.max(2.6, 2.6 * cy.zoom());
ctx.moveTo(expandcollapseStartX + diff, expandcollapseStartY + rectSize / 2);
ctx.lineTo(expandcollapseStartX + lineSize + diff, expandcollapseStartY + rectSize / 2);
if (isCollapsed) {
ctx.moveTo(expandcollapseStartX + rectSize / 2, expandcollapseStartY + diff);
ctx.lineTo(expandcollapseStartX + rectSize / 2, expandcollapseStartY + lineSize + diff);
}
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = oldStrokeStyle;
ctx.fillStyle = oldFillStyle;
ctx.lineWidth = oldWidth;
}
node._private.data.expandcollapseRenderedStartX = expandcollapseStartX;
node._private.data.expandcollapseRenderedStartY = expandcollapseStartY;
node._private.data.expandcollapseRenderedCueSize = expandcollapseRectSize;
nodeWithRenderedCue = node;
}
function drawImg(imgSrc, x, y, w, h) {
var img = new Image(w, h);
img.src = imgSrc;
img.onload = () => {
ctx.drawImage(img, x, y, w, h);
};
}
cy.on('resize', data.eCyResize = function () {
sizeCanvas();
});
cy.on('expandcollapse.clearvisualcue', function () {
if (nodeWithRenderedCue) {
clearDraws();
}
});
var oldMousePos = null, currMousePos = null;
cy.on('mousedown', data.eMouseDown = function (e) {
oldMousePos = e.renderedPosition || e.cyRenderedPosition
});
cy.on('mouseup', data.eMouseUp = function (e) {
currMousePos = e.renderedPosition || e.cyRenderedPosition
});
cy.on('remove', 'node', data.eRemove = function (evt) {
const node = evt.target;
if (node == nodeWithRenderedCue) {
clearDraws();
}
});
var ur;
cy.on('select unselect', data.eSelect = function () {
if (nodeWithRenderedCue) {
clearDraws();
}
var selectedNodes = cy.nodes(':selected');
if (selectedNodes.length !== 1) {
return;
}
var selectedNode = selectedNodes[0];
if (selectedNode.isParent() || selectedNode.hasClass('cy-expand-collapse-collapsed-node')) {
drawExpandCollapseCue(selectedNode);
}
});
cy.on('tap', data.eTap = function (event) {
var node = nodeWithRenderedCue;
if (!node) {
return;
}
var expandcollapseRenderedStartX = node.data('expandcollapseRenderedStartX');
var expandcollapseRenderedStartY = node.data('expandcollapseRenderedStartY');
var expandcollapseRenderedRectSize = node.data('expandcollapseRenderedCueSize');
var expandcollapseRenderedEndX = expandcollapseRenderedStartX + expandcollapseRenderedRectSize;
var expandcollapseRenderedEndY = expandcollapseRenderedStartY + expandcollapseRenderedRectSize;
var cyRenderedPos = event.renderedPosition || event.cyRenderedPosition;
var cyRenderedPosX = cyRenderedPos.x;
var cyRenderedPosY = cyRenderedPos.y;
var opts = options();
var factor = (opts.expandCollapseCueSensitivity - 1) / 2;
if ((Math.abs(oldMousePos.x - currMousePos.x) < 5 && Math.abs(oldMousePos.y - currMousePos.y) < 5)
&& cyRenderedPosX >= expandcollapseRenderedStartX - expandcollapseRenderedRectSize * factor
&& cyRenderedPosX <= expandcollapseRenderedEndX + expandcollapseRenderedRectSize * factor
&& cyRenderedPosY >= expandcollapseRenderedStartY - expandcollapseRenderedRectSize * factor
&& cyRenderedPosY <= expandcollapseRenderedEndY + expandcollapseRenderedRectSize * factor) {
if (opts.undoable && !ur) {
ur = cy.undoRedo({ defaultActions: false });
}
if (api.isCollapsible(node)) {
clearDraws();
if (opts.undoable) {
ur.do("collapse", {
nodes: node,
options: opts
});
}
else {
api.collapse(node, opts);
}
}
else if (api.isExpandable(node)) {
clearDraws();
if (opts.undoable) {
ur.do("expand", { nodes: node, options: opts });
}
else {
api.expand(node, opts);
}
}
if (node.selectable()) {
node.unselectify();
cy.scratch('_cyExpandCollapse').selectableChanged = true;
}
}
});
cy.on('afterUndo afterRedo', data.eUndoRedo = data.eSelect);
cy.on('position', 'node', data.ePosition = debounce2(data.eSelect, CUE_POS_UPDATE_DELAY, clearDraws));
cy.on('pan zoom', data.ePosition);
// write options to data
data.hasEventFields = true;
setData(data);
},
unbind: function () {
// var $container = this;
var data = getData();
if (!data.hasEventFields) {
console.log('events to unbind does not exist');
return;
}
cy.trigger('expandcollapse.clearvisualcue');
cy.off('mousedown', 'node', data.eMouseDown)
.off('mouseup', 'node', data.eMouseUp)
.off('remove', 'node', data.eRemove)
.off('tap', 'node', data.eTap)
.off('add', 'node', data.eAdd)
.off('position', 'node', data.ePosition)
.off('pan zoom', data.ePosition)
.off('select unselect', data.eSelect)
.off('free', 'node', data.eFree)
.off('resize', data.eCyResize)
.off('afterUndo afterRedo', data.eUndoRedo);
},
rebind: function () {
var data = getData();
if (!data.hasEventFields) {
console.log('events to rebind does not exist');
return;
}
cy.on('mousedown', 'node', data.eMouseDown)
.on('mouseup', 'node', data.eMouseUp)
.on('remove', 'node', data.eRemove)
.on('tap', 'node', data.eTap)
.on('add', 'node', data.eAdd)
.on('position', 'node', data.ePosition)
.on('pan zoom', data.ePosition)
.on('select unselect', data.eSelect)
.on('free', 'node', data.eFree)
.on('resize', data.eCyResize)
.on('afterUndo afterRedo', data.eUndoRedo);
}
};
if (functions[fn]) {
return functions[fn].apply(cy.container(), Array.prototype.slice.call(arguments, 1));
} else if (typeof fn == 'object' || !fn) {
return functions.init.apply(cy.container(), arguments);
}
throw new Error('No such function `' + fn + '` for cytoscape.js-expand-collapse');
};