position.x
&& nodes[i].boundingBox().y1 < position.y
&& nodes[i].boundingBox().y2 > position.y) {
// Check if the node is excluded
if (filter(nodes[i])) {
return nodes[i];
}
}
}
return null;
},
/**
* Enable the save button
*/
showCleanWorkspaceStatus: function() {
$(GLPIImpact.selectors.save).removeClass('dirty');
$(GLPIImpact.selectors.save).removeClass('clean'); // Needed for animations if the workspace is not dirty
$(GLPIImpact.selectors.save).addClass('clean');
$(GLPIImpact.selectors.save).find('i').removeClass("fas fa-exclamation-triangle");
$(GLPIImpact.selectors.save).find('i').addClass("fas fa-check");
},
/**
* Enable the save button
*/
showDirtyWorkspaceStatus: function() {
$(GLPIImpact.selectors.save).removeClass('clean');
$(GLPIImpact.selectors.save).addClass('dirty');
$(GLPIImpact.selectors.save).find('i').removeClass("fas fa-check");
$(GLPIImpact.selectors.save).find('i').addClass("fas fa-exclamation-triangle");
},
/**
* Enable the save button
*/
showDefaultWorkspaceStatus: function() {
$(GLPIImpact.selectors.save).removeClass('clean');
$(GLPIImpact.selectors.save).removeClass('dirty');
},
/**
* Build the ongoing dialog content according to the list of ITILObjects
*
* @param {Object} ITILObjects requests, incidents, changes, problems
*
* @returns {string}
*/
buildOngoingDialogContent: function(ITILObjects) {
return this.listElements(__("Requests"), ITILObjects.requests, "ticket")
+ this.listElements(__("Incidents"), ITILObjects.incidents, "ticket")
+ this.listElements(__("Changes"), ITILObjects.changes , "change")
+ this.listElements(__("Problems"), ITILObjects.problems, "problem");
},
/**
* Build an html list
*
* @param {string} title requests, incidents, changes, problems
* @param {string} elements requests, incidents, changes, problems
* @param {string} url key used to generate the URL
*
* @returns {string}
*/
listElements: function(title, elements, url) {
var html = "";
if (elements.length > 0) {
html += "" + title + "
";
html += "";
elements.forEach(function(element) {
var link = CFG_GLPI.root_doc + "/front/" + url + ".form.php?id=" + element.id;
html += '- ' + element.name
+ '
';
});
html += "
";
}
return html;
},
/**
* Add a new compound from the selected nodes
*/
addCompoundFromSelection: _.debounce(function(){
// Check that there is enough selected nodes
if (GLPIImpact.eventData.boxSelected.length < 2) {
alert(__("You need to select at least 2 assets to make a group"));
} else {
// Create the compound
var newCompound = GLPIImpact.cy.add({
group: 'nodes',
data: {color: '#dadada'},
});
// Log event data (for undo)
GLPIImpact.eventData.newCompound = {
data: {id: newCompound.data('id')},
children: [],
};
// Set parent for coumpound member
GLPIImpact.eventData.boxSelected.forEach(function(ele) {
ele.move({'parent': newCompound.data('id')});
GLPIImpact.eventData.newCompound.children.push(ele.data('id'));
});
// Show edit dialog
$(GLPIImpact.selectors.editCompoundDialog).dialog(
GLPIImpact.getEditCompoundDialog(newCompound)
);
// Back to default mode
GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
}
// Clear the selection
GLPIImpact.eventData.boxSelected = [];
GLPIImpact.cy.filter(":selected").unselect();
}, 100, false),
/**
* Remove an element from the graph
*
* @param {object} ele
*/
deleteFromGraph: function(ele) {
if (ele.data('id') == GLPIImpact.startNode) {
alert("Can't remove starting node");
return;
}
// Log for undo/redo
var deleted = {
edges: [],
nodes: [],
compounds: []
};
if (ele.isEdge()) {
// Case 1: removing an edge
deleted.edges.push(_.clone(ele.data()));
ele.remove();
} else if (ele.isParent()) {
// Case 2: removing a compound
// Set undo/redo data
deleted.compounds.push({
compoundData : _.clone(ele.data()),
compoundChildren: ele.children().map(function(node) {
return node.data('id');
}),
});
// Remove only the parent
ele.children().move({parent: null});
ele.remove();
} else {
// Case 3: removing a node
// Remove parent if last child of a compound
if (!ele.isOrphan() && ele.parent().children().length <= 2) {
var parent = ele.parent();
// Set undo/redo data
deleted.compounds.push({
compoundData : _.clone(parent.data()),
compoundChildren: parent.children().map(function(node) {
return node.data('id');
}),
});
parent.children().move({parent: null});
parent.remove();
}
// Set undo/redo data
deleted.nodes.push({
nodeData: _.clone(ele.data()),
nodePosition: _.clone(ele.position()),
});
deleted.edges = deleted.edges.concat(ele.connectedEdges(function(edge) {
// Check for duplicates
var exist = false;
deleted.edges.forEach(function(deletedEdge) {
if (deletedEdge.id == edge.data('id')) {
exist = true;
}
});
// In case of multiple deletion, check in the buffer too
if (GLPIImpact.eventData.multipleDeletion != null) {
GLPIImpact.eventData.multipleDeletion.edges.forEach(
function(deletedEdge) {
if (deletedEdge.id == edge.data('id')) {
exist = true;
}
}
);
}
return !exist;
}).map(function(ele){
return ele.data();
}));
// Remove all edges connected to this node from graph and delta
ele.remove();
}
// Update flags
GLPIImpact.updateFlags();
// Multiple deletion, set the data in eventData buffer so it can be added
// as a simple undo/redo entry later
if (this.eventData.multipleDeletion != null) {
this.eventData.multipleDeletion.edges = this.eventData.multipleDeletion.edges.concat(deleted.edges);
this.eventData.multipleDeletion.nodes = this.eventData.multipleDeletion.nodes.concat(deleted.nodes);
this.eventData.multipleDeletion.compounds = this.eventData.multipleDeletion.compounds.concat(deleted.compounds);
} else {
this.addToUndo(this.ACTION_DELETE, deleted);
}
},
/**
* Toggle fullscreen mode
*/
toggleFullscreen: function() {
this.fullscreen = !this.fullscreen;
$(this.selectors.toggleFullscreen).toggleClass('active');
$(this.impactContainer).toggleClass('fullscreen');
$(this.selectors.side).toggleClass('fullscreen');
if (this.fullscreen) {
$(this.impactContainer).children("canvas:eq(0)").css({
height: "100vh"
});
$('html, body').css('overflow', 'hidden');
} else {
$(this.impactContainer).children("canvas:eq(0)").css({
height: "unset"
});
$('html, body').css('overflow', 'unset');
}
GLPIImpact.cy.resize();
},
/**
* Check if a given position match the hitbox of a badge
*
* @param {Object} renderedPosition {x, y}
* @param {Boolean} trigger should we trigger the link if there
* is a match ?
* @param {Boolean} blank
* @returns {Boolean}
*/
checkBadgeHitboxes: function (renderedPosition, trigger, blank) {
var hit = false;
var margin = 5 * GLPIImpact.cy.zoom();
GLPIImpact.badgesHitboxes.forEach(function(badgeHitboxDetails) {
if (hit) {
return;
}
var position = badgeHitboxDetails.position;
var bb = {
x1: position.x - margin,
x2: position.x + margin,
y1: position.y - margin,
y2: position.y + margin,
};
if (bb.x1 < renderedPosition.x && bb.x2 > renderedPosition.x
&& bb.y1 < renderedPosition.y && bb.y2 > renderedPosition.y) {
hit = true;
if (trigger) {
var target = badgeHitboxDetails.target + "?is_deleted=0&as_map=0&search=Search&itemtype=Ticket";
// Add items_id criteria
target += "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" + badgeHitboxDetails.id;
// Add itemtype criteria
target += "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" + badgeHitboxDetails.itemtype;
// Add type criteria (incident)
target += "&criteria[2][link]=AND&criteria[2][field]=14&criteria[2][searchtype]=equals&criteria[2][value]=1";
// Add status criteria (not solved)
target += "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
if (blank) {
window.open(target);
} else {
window.location.href = target;
}
}
}
});
return hit;
},
/**
* Handle global click events
*
* @param {JQuery.Event} event
*/
onClick: function (event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
break;
}
GLPIImpact.checkBadgeHitboxes(event.renderedPosition, true, GLPIImpact.eventData.ctrlDown);
},
/**
* Handle click on edge
*
* @param {JQuery.Event} event
*/
edgeOnClick: function (event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
// Remove the edge from the graph
GLPIImpact.deleteFromGraph(event.target);
break;
}
},
/**
* Handle click on node
*
* @param {JQuery.Event} event
*/
nodeOnClick: function (event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
if (GLPIImpact.eventData.lastClicktimestamp != null) {
// Trigger homemade double click event
if (event.timeStamp - GLPIImpact.eventData.lastClicktimestamp < 500
&& event.target == GLPIImpact.eventData.lastClickTarget) {
event.target.trigger('doubleClick', event);
}
}
GLPIImpact.eventData.lastClicktimestamp = event.timeStamp;
GLPIImpact.eventData.lastClickTarget = event.target;
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
GLPIImpact.deleteFromGraph(event.target);
break;
}
},
/**
* Handle end of box selection event
*
* @param {JQuery.Event} event
*/
onBox: function (event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
break;
case GLPIImpact.EDITION_ADD_COMPOUND:
var ele = event.target;
// Add node to selected list if he is not part of a compound already
if (ele.isNode() && ele.isOrphan() && !ele.isParent()) {
GLPIImpact.eventData.boxSelected.push(ele);
}
GLPIImpact.addCompoundFromSelection();
break;
}
},
/**
* Handle any graph modification
*
* @param {*} event
*/
onChange: function() {
GLPIImpact.showDirtyWorkspaceStatus();
// Remove hightligh for recently inserted graph
GLPIImpact.cy.$("[highlight][!start_node]").data("highlight", 0);
},
/**
* Double click handler
* @param {JQuery.Event} event
*/
onDoubleClick: function(event) {
if (event.target.isParent()) {
// Open edit dialog on compound nodes
$(GLPIImpact.selectors.editCompoundDialog).dialog(
GLPIImpact.getEditCompoundDialog(event.target)
);
} else if (event.target.isNode()) {
// Go to on nodes
window.open(event.target.data('link'));
}
},
/**
* Handle "grab" event
*
* @param {Jquery.event} event
*/
onGrabOn: function(event) {
// Store original position (shallow copy)
GLPIImpact.eventData.grabNodePosition = {
x: event.target.position().x,
y: event.target.position().y,
};
// Store original parent (shallow copy)
var parent = null;
if (event.target.parent() !== undefined) {
parent = event.target.parent().data('id');
}
GLPIImpact.eventData.grabNodeParent = parent;
},
/**
* Handle "free" event
* @param {Jquery.Event} event
*/
onFreeOn: function(event) {
var parent = null;
if (event.target.parent() !== undefined) {
parent = event.target.parent().data('id');
}
var newParent = null;
if (parent !== GLPIImpact.eventData.grabNodeParent) {
newParent = parent;
}
// If there was a real position change
if (GLPIImpact.eventData.grabNodePosition.x !== event.target.position().x
|| GLPIImpact.eventData.grabNodePosition.y !== event.target.position().y) {
GLPIImpact.addToUndo(GLPIImpact.ACTION_MOVE, {
node: event.target.data('id'),
oldPosition: GLPIImpact.eventData.grabNodePosition,
newPosition: {
x: event.target.position().x,
y: event.target.position().y,
},
newParent: newParent,
});
}
},
/**
* Remove handler
* @param {JQuery.Event} event
*/
onRemove: function(event) {
if (event.target.isNode() && !event.target.isParent()) {
var itemtype = event.target.data('id')
.split(GLPIImpact.NODE_ID_SEPERATOR)[0];
// If a node was deleted and its itemtype is the same as the one
// selected in the add node panel, refresh the search
if (itemtype == GLPIImpact.selectedItemtype) {
$(GLPIImpact.selectors.sideSearchResults).html("");
GLPIImpact.searchAssets(
GLPIImpact.selectedItemtype,
JSON.stringify(GLPIImpact.getUsedAssets()),
$(GLPIImpact.selectors.sideFilterAssets).val(),
0
);
}
}
},
/**
* Handler for key down events
*
* @param {JQuery.Event} event
*/
onKeyDown: function(event) {
// Ignore key events if typing inside input
if (event.target.nodeName == "INPUT") {
return;
}
switch (event.which) {
// Shift
case 16:
if (event.ctrlKey) {
// Enter add compound edge mode
if (GLPIImpact.editionMode != GLPIImpact.EDITION_ADD_COMPOUND) {
if (GLPIImpact.eventData.previousEditionMode === undefined) {
GLPIImpact.eventData.previousEditionMode = GLPIImpact.editionMode;
}
GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_COMPOUND);
}
} else {
// Enter edit edge mode
if (GLPIImpact.editionMode != GLPIImpact.EDITION_ADD_EDGE) {
if (GLPIImpact.eventData.previousEditionMode === undefined) {
GLPIImpact.eventData.previousEditionMode = GLPIImpact.editionMode;
}
GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_EDGE);
}
}
break;
// Ctrl
case 17:
GLPIImpact.eventData.ctrlDown = true;
break;
// ESC
case 27:
// Exit specific edition mode
if (GLPIImpact.editionMode != GLPIImpact.EDITION_DEFAULT) {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
}
break;
// Delete
case 46:
if (GLPIImpact.readonly) {
break;
}
// Prepare multiple deletion buffer (for undo/redo)
GLPIImpact.eventData.multipleDeletion = {
edges : [],
nodes : [],
compounds: [],
};
// Delete selected element(s)
GLPIImpact.cy.filter(":selected").forEach(function(ele) {
GLPIImpact.deleteFromGraph(ele);
});
// Set undo/redo data
GLPIImpact.addToUndo(
GLPIImpact.ACTION_DELETE,
GLPIImpact.eventData.multipleDeletion
);
// Reset multiple deletion buffer (for undo/redo)
GLPIImpact.eventData.multipleDeletion = null;
break;
// CTRL + Y
case 89:
if (!event.ctrlKey) {
break;
}
GLPIImpact.redo();
break;
// CTRL + Z / CTRL + SHIFT + Z
case 90:
if (!event.ctrlKey) {
break;
}
if (event.shiftKey) {
GLPIImpact.redo();
} else {
GLPIImpact.undo();
}
break;
}
},
/**
* Handler for key down events
*
* @param {JQuery.Event} event
*/
onKeyUp: function(event) {
switch (event.which) {
// Shift
case 16:
// Return to previous edition mode if needed
if (GLPIImpact.eventData.previousEditionMode !== undefined
&& (GLPIImpact.editionMode == GLPIImpact.EDITION_ADD_EDGE
|| GLPIImpact.editionMode == GLPIImpact.EDITION_ADD_COMPOUND)
) {
GLPIImpact.setEditionMode(GLPIImpact.eventData.previousEditionMode);
GLPIImpact.eventData.previousEditionMode = undefined;
}
break;
// Ctrl
case 17:
// Return to previous edition mode if needed
if (GLPIImpact.editionMode == GLPIImpact.EDITION_ADD_COMPOUND
&& GLPIImpact.eventData.previousEditionMode !== undefined) {
GLPIImpact.setEditionMode(GLPIImpact.eventData.previousEditionMode);
GLPIImpact.eventData.previousEditionMode = undefined;
}
GLPIImpact.eventData.ctrlDown = false;
break;
}
},
/**
* Handle mousedown events on nodes
*
* @param {JQuery.Event} event
*/
nodeOnMousedown: function (event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
$(GLPIImpact.impactContainer).css('cursor', "grabbing");
// If we are not on a compound node or a node already inside one
if (event.target.isOrphan() && !event.target.isParent()) {
GLPIImpact.eventData.grabNodeStart = event.target;
}
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
if (!event.target.isParent()) {
GLPIImpact.eventData.addEdgeStart = this.data('id');
}
break;
case GLPIImpact.EDITION_DELETE:
break;
case GLPIImpact.EDITION_ADD_COMPOUND:
break;
}
},
/**
* Handle mouseup events
*
* @param {JQuery.Event} event
*/
onMouseUp: function(event) {
if (event.target.data('id') != undefined && event.target.isNode()) {
// Handler for nodes
GLPIImpact.nodeOnMouseup();
}
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
// Exit if no start node
if (GLPIImpact.eventData.addEdgeStart == null) {
return;
}
// Reset addEdgeStart
var startEdge = GLPIImpact.eventData.addEdgeStart; // Keep a copy to use later
GLPIImpact.eventData.addEdgeStart = null;
// Remove current tmp collection
event.cy.remove(GLPIImpact.eventData.tmpEles);
var edgeID = GLPIImpact.eventData.tmpEles.data('id');
GLPIImpact.eventData.tmpEles = null;
// Option 1: Edge between a node and the fake tmp_node -> ignore
if (edgeID == 'tmp_node') {
return;
}
var edgeDetails = edgeID.split(GLPIImpact.EDGE_ID_SEPERATOR);
// Option 2: Edge between two nodes that already exist -> ignore
if (event.cy.filter('edge[id="' + edgeID + '"]').length > 0) {
return;
}
// Option 3: Both end of the edge are actually the same node -> ignore
if (startEdge == edgeDetails[1]) {
return;
}
// Option 4: Edge between two nodes that does not exist yet -> create it!
var data = {
id: edgeID,
source: startEdge,
target: edgeDetails[1]
};
event.cy.add({
group: 'edges',
data: data,
});
GLPIImpact.addToUndo(GLPIImpact.ACTION_ADD_EDGE, _.clone(data));
// Update dependencies flags according to the new link
GLPIImpact.updateFlags();
break;
case GLPIImpact.EDITION_DELETE:
break;
}
},
/**
* Handle mouseup events on nodes
*
* @param {JQuery.Event} event
*/
nodeOnMouseup: function () {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
$(GLPIImpact.impactContainer).css('cursor', "grab");
// Reset eventData for node grabbing
GLPIImpact.eventData.grabNodeStart = null;
GLPIImpact.eventData.boundingBox = null;
break;
case GLPIImpact.EDITION_ADD_NODE:
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
break;
}
},
/**
* Handle mousemove events on nodes
*
* @param {JQuery.Event} event
*/
onMousemove: _.throttle(function(event) {
var node;
// Check for badges hitboxes
if (GLPIImpact.checkBadgeHitboxes(event.renderedPosition, false, false)
&& !GLPIImpact.eventData.showPointerForBadge) {
// Entering a badge hitbox
GLPIImpact.eventData.showPointerForBadge = true;
// Store previous cursor and show pointer
GLPIImpact.eventData.previousCursor = $(GLPIImpact.impactContainer).css('cursor');
$(GLPIImpact.impactContainer).css('cursor', "pointer");
} else if (GLPIImpact.eventData.showPointerForBadge
&& !GLPIImpact.checkBadgeHitboxes(event.renderedPosition, false, false)) {
// Exiiting a badge hitbox
GLPIImpact.eventData.showPointerForBadge = false;
// Reset to previous cursor
$(GLPIImpact.impactContainer).css(
'cursor',
GLPIImpact.eventData.previousCursor
);
}
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
case GLPIImpact.EDITION_ADD_NODE:
// No action if we are not grabbing a node
if (GLPIImpact.eventData.grabNodeStart == null) {
return;
}
// Look for a compound at the cursor position
node = GLPIImpact.getNodeAt(event.position, function(node) {
return node.isParent();
});
if (node) {
// If we have a bounding box defined, the grabbed node is already
// being placed into a compound, we need to check if it was moved
// outside this original bouding box to know if the user is
// trying to move it away from the compound
if (GLPIImpact.eventData.boundingBox != null) {
// If the user tried to move out of the compound
if (GLPIImpact.eventData.boundingBox.x1 > event.position.x
|| GLPIImpact.eventData.boundingBox.x2 < event.position.x
|| GLPIImpact.eventData.boundingBox.y1 > event.position.y
|| GLPIImpact.eventData.boundingBox.y2 < event.position.y) {
// Remove it from the compound
GLPIImpact.eventData.grabNodeStart.move({parent: null});
GLPIImpact.eventData.boundingBox = null;
}
} else {
// If we found a compound, add the grabbed node inside
GLPIImpact.eventData.grabNodeStart.move({parent: node.data('id')});
// Store the original bouding box of the compound
GLPIImpact.eventData.boundingBox = node.boundingBox();
}
} else {
// Else; reset it's parent so it can be removed from any temporary
// compound while the user is stil grabbing
GLPIImpact.eventData.grabNodeStart.move({parent: null});
}
break;
case GLPIImpact.EDITION_ADD_EDGE:
// No action if we are not placing an edge
if (GLPIImpact.eventData.addEdgeStart == null) {
return;
}
// Remove current tmp collection
if (GLPIImpact.eventData.tmpEles != null) {
event.cy.remove(GLPIImpact.eventData.tmpEles);
}
node = GLPIImpact.getNodeAt(event.position, function(node) {
var nodeID = node.data('id');
// Can't link to itself
if (nodeID == GLPIImpact.eventData.addEdgeStart) {
return false;
}
// Can't link to parent
if (node.isParent()) {
return false;
}
// The created edge shouldn't already exist
var edgeID = GLPIImpact.makeID(GLPIImpact.EDGE, GLPIImpact.eventData.addEdgeStart, nodeID);
if (GLPIImpact.cy.filter('edge[id="' + edgeID + '"]').length > 0) {
return false;
}
// The node must be visible
if (!GLPIImpact.cy.getElementById(nodeID).visible()) {
return false;
}
return true;
});
if (node != null) {
node = node.data('id');
// Add temporary edge to node hovered by the user
GLPIImpact.eventData.tmpEles = event.cy.add([
{
group: 'edges',
data: {
id: GLPIImpact.makeID(GLPIImpact.EDGE, GLPIImpact.eventData.addEdgeStart, node),
source: GLPIImpact.eventData.addEdgeStart,
target: node,
}
}
]);
} else {
// Add temporary edge to a new invisible node at mouse position
GLPIImpact.eventData.tmpEles = event.cy.add([
{
group: 'nodes',
data: {
id: 'tmp_node',
},
position: {
x: event.position.x,
y: event.position.y
}
},
{
group: 'edges',
data: {
id: GLPIImpact.makeID(
GLPIImpact.EDGE,
GLPIImpact.eventData.addEdgeStart,
"tmp_node"
),
source: GLPIImpact.eventData.addEdgeStart,
target: 'tmp_node',
}
}
]);
}
break;
case GLPIImpact.EDITION_DELETE:
break;
}
}, 25),
/**
* Handle global mouseover events
*
* @param {JQuery.Event} event
*/
onMouseover: function(event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
// No valid target, no action needed
if (event.target.data('id') == undefined) {
break;
}
if (event.target.isNode()) {
if (!GLPIImpact.eventData.showPointerForBadge) {
// Don't alter the cursor if hovering a badge
$(GLPIImpact.impactContainer).css('cursor', "grab");
}
} else if (event.target.isEdge()) {
// If mouseover on edge, show default cursor and disable panning
GLPIImpact.cy.panningEnabled(false);
if (!GLPIImpact.eventData.showPointerForBadge) {
// Don't alter the cursor if hovering a badge
$(GLPIImpact.impactContainer).css('cursor', "default");
}
}
break;
case GLPIImpact.EDITION_ADD_NODE:
if (event.target.data('id') == undefined) {
break;
}
if (event.target.isNode()) {
// If mouseover on node, show grab cursor
$(GLPIImpact.impactContainer).css('cursor', "grab");
} else if (event.target.isEdge()) {
// If mouseover on edge, show default cursor and disable panning
$(GLPIImpact.impactContainer).css('cursor', "default");
GLPIImpact.cy.panningEnabled(false);
}
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
if (event.target.data('id') == undefined) {
break;
}
$(GLPIImpact.impactContainer).css('cursor', "default");
var id = event.target.data('id');
// Remove red overlay
event.cy.filter().data('todelete', 0);
event.cy.filter().unselect();
// Store here if one default node
if (event.target.data('id') == GLPIImpact.startNode) {
$(GLPIImpact.impactContainer).css('cursor', "not-allowed");
break;
}
// Add red overlay
event.target.data('todelete', 1);
event.target.select();
if (event.target.isNode()){
var sourceFilter = "edge[source='" + id + "']";
var targetFilter = "edge[target='" + id + "']";
event.cy.filter(sourceFilter + ", " + targetFilter)
.data('todelete', 1)
.select();
}
break;
}
},
/**
* Handle global mouseout events
*
* @param {JQuery.Event} event
*/
onMouseout: function(event) {
switch (GLPIImpact.editionMode) {
case GLPIImpact.EDITION_DEFAULT:
if (!GLPIImpact.eventData.showPointerForBadge) {
// Don't alter the cursor if hovering a badge
$(GLPIImpact.impactContainer).css('cursor', "move");
}
// Re-enable panning in case the mouse was over an edge
GLPIImpact.cy.panningEnabled(true);
break;
case GLPIImpact.EDITION_ADD_NODE:
if (!GLPIImpact.eventData.showPointerForBadge) {
// Don't alter the cursor if hovering a badge
$(GLPIImpact.impactContainer).css('cursor', "move");
}
// Re-enable panning in case the mouse was over an edge
GLPIImpact.cy.panningEnabled(true);
break;
case GLPIImpact.EDITION_ADD_EDGE:
break;
case GLPIImpact.EDITION_DELETE:
// Remove red overlay
event.cy.filter().data('todelete', 0);
event.cy.filter().unselect();
if (!GLPIImpact.eventData.showPointerForBadge) {
// Don't alter the cursor if hovering a badge
$(GLPIImpact.impactContainer).css('cursor', "move");
}
break;
}
},
/**
* Handle "goTo" menu event
*
* @param {JQuery.Event} event
*/
menuOnGoTo: function(event) {
window.open(event.target.data('link'));
},
/**
* Handle "showOngoing" menu event
*
* @param {JQuery.Event} event
*/
menuOnShowOngoing: function(event) {
$(GLPIImpact.selectors.ongoingDialog).html(
GLPIImpact.buildOngoingDialogContent(event.target.data('ITILObjects'))
);
$(GLPIImpact.selectors.ongoingDialog).dialog(GLPIImpact.getOngoingDialog());
},
/**
* Handle "EditCompound" menu event
*
* @param {JQuery.Event} event
*/
menuOnEditCompound: function (event) {
$(GLPIImpact.selectors.editCompoundDialog).dialog(
GLPIImpact.getEditCompoundDialog(event.target)
);
},
/**
* Handler for "removeFromCompound" action
*
* @param {JQuery.Event} event
*/
menuOnRemoveFromCompound: function(event) {
var parent = GLPIImpact.cy.getElementById(
event.target.data('parent')
);
// Undo log
GLPIImpact.addToUndo(GLPIImpact.ACTION_REMOVE_FROM_COMPOUND, {
nodeData : _.clone(event.target.data()),
compoundData: _.clone(parent.data()),
children : parent.children().map(function(node) {
return node.data('id');
}),
});
// Remove node from compound
event.target.move({parent: null});
// Destroy compound if only one or zero member left
if (parent.children().length < 2) {
parent.children().move({parent: null});
GLPIImpact.cy.remove(parent);
}
},
/**
* Handler for "delete" menu action
*
* @param {JQuery.Event} event
*/
menuOnDelete: function(event){
GLPIImpact.deleteFromGraph(event.target);
},
/**
* Ask the backend for available assets to insert into the graph
*
* @param {String} itemtype
* @param {Array} used
* @param {String} filter
* @param {Number} page
*/
searchAssets: function(itemtype, used, filter, page) {
var hidden = GLPIImpact.cy
.nodes(GLPIImpact.getHiddenSelector())
.filter(function(node) {
return !node.isParent();
})
.map(function(node) {
return node.data('id');
});
$(GLPIImpact.selectors.sideSearchSpinner).show();
$(GLPIImpact.selectors.sideSearchNoResults).hide();
$.ajax({
type: "GET",
url: $(GLPIImpact.selectors.form).prop('action'),
data: {
'action' : 'search',
'itemtype': itemtype,
'used' : used,
'filter' : filter,
'page' : page,
},
success: function(data){
$.each(data.items, function(index, value) {
var graph_id = itemtype + GLPIImpact.NODE_ID_SEPERATOR + value['id'];
var isHidden = hidden.indexOf(graph_id) !== -1;
var cssClass = "";
if (isHidden) {
cssClass = "impact-res-disabled";
}
var str = '';
str += '
';
str += value["name"];
if (isHidden) {
str += '';
}
str += "
";
$(GLPIImpact.selectors.sideSearchResults).append(str);
});
// All data was loaded, hide "More..."
if (data.total <= ((page + 1) * 20)) {
$(GLPIImpact.selectors.sideSearchMore).hide();
} else {
$(GLPIImpact.selectors.sideSearchMore).show();
}
// No results
if (data.total == 0 && page == 0) {
$(GLPIImpact.selectors.sideSearchNoResults).show();
}
$(GLPIImpact.selectors.sideSearchSpinner).hide();
},
error: function(){
alert("error");
},
});
},
/**
* Get the list of assets already on the graph
*/
getUsedAssets: function() {
// Get used ids for this itemtype
var used = [];
GLPIImpact.cy.nodes().not(GLPIImpact.getHiddenSelector()).forEach(function(node) {
if (node.isParent()) {
return;
}
var nodeId = node.data('id')
.split(GLPIImpact.NODE_ID_SEPERATOR);
if (nodeId[0] == GLPIImpact.selectedItemtype) {
used.push(parseInt(nodeId[1]));
}
});
return used;
},
/**
* Taken from cytoscape source, get the real position of the click event on
* the cytoscape canvas
*
* @param {Number} clientX
* @param {Number} clientY
* @param {Boolean} rendered
* @returns {Object}
*/
projectIntoViewport: function (clientX, clientY, rendered) {
var cy = this.cy;
var offsets = this.findContainerClientCoords();
var offsetLeft = offsets[0];
var offsetTop = offsets[1];
var scale = offsets[4];
var pan = cy.pan();
var zoom = cy.zoom();
if (rendered) {
return {
x: clientX - offsetLeft,
y: clientY - offsetTop
};
} else {
return {
x: ((clientX - offsetLeft) / scale - pan.x) / zoom,
y: ((clientY - offsetTop) / scale - pan.y) / zoom
};
}
},
/**
* Used for projectIntoViewport
*
* @returns {Array}
*/
findContainerClientCoords: function () {
var container = this.impactContainer[0];
var rect = container.getBoundingClientRect();
var style = window.getComputedStyle(container);
var styleValue = function styleValue(name) {
return parseFloat(style.getPropertyValue(name));
};
var padding = {
left : styleValue('padding-left'),
right : styleValue('padding-right'),
top : styleValue('padding-top'),
bottom: styleValue('padding-bottom')
};
var border = {
left : styleValue('border-left-width'),
right : styleValue('border-right-width'),
top : styleValue('border-top-width'),
bottom: styleValue('border-bottom-width')
};
var clientWidth = container.clientWidth;
var clientHeight = container.clientHeight;
var paddingHor = padding.left + padding.right;
var paddingVer = padding.top + padding.bottom;
var borderHor = border.left + border.right;
var scale = rect.width / (clientWidth + borderHor);
var unscaledW = clientWidth - paddingHor;
var unscaledH = clientHeight - paddingVer;
var left = rect.left + padding.left + border.left;
var top = rect.top + padding.top + border.top;
return [left, top, unscaledW, unscaledH, scale];
},
/**
* Set event handler for toolbar events
*/
initToolbar: function() {
// Save the graph
$(GLPIImpact.selectors.save).click(function() {
GLPIImpact.showCleanWorkspaceStatus();
// Send data as JSON on submit
$.ajax({
type: "POST",
url: $(GLPIImpact.selectors.form).prop('action'),
data: {
'impacts': JSON.stringify(GLPIImpact.computeDelta())
},
success: function(){
GLPIImpact.initialState = GLPIImpact.getCurrentState();
$(document).trigger('impactUpdated');
},
error: function(){
GLPIImpact.showDirtyWorkspaceStatus();
},
});
});
// Add a new node on the graph
$(GLPIImpact.selectors.addNode).click(function() {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_NODE);
});
// Add a new edge on the graph
$(GLPIImpact.selectors.addEdge).click(function() {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_EDGE);
});
// Add a new compound on the graph
$(GLPIImpact.selectors.addCompound).click(function() {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_COMPOUND);
});
// Enter delete mode
$(GLPIImpact.selectors.deleteElement).click(function() {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_DELETE);
});
// Export graph
$(GLPIImpact.selectors.export).click(function() {
GLPIImpact.download(
'png',
false
);
});
// Show settings
$(this.selectors.impactSettings).click(function() {
if ($(this).find('i.fa-chevron-right').length) {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
} else {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_SETTINGS);
}
});
$(GLPIImpact.selectors.undo).click(function() {
GLPIImpact.undo();
});
// Redo button
$(GLPIImpact.selectors.redo).click(function() {
GLPIImpact.redo();
});
// Toggle expanded toolbar
$(this.selectors.sideToggle).click(function() {
if ($(this).find('i.fa-chevron-right').length) {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
} else {
GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_NODE);
}
});
// Toggle impact visibility
$(GLPIImpact.selectors.toggleImpact).click(function() {
GLPIImpact.toggleVisibility(GLPIImpact.FORWARD);
GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_IMPACT_VISIBILITY, {});
});
// Toggle depends visibility
$(GLPIImpact.selectors.toggleDepends).click(function() {
GLPIImpact.toggleVisibility(GLPIImpact.BACKWARD);
GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_DEPENDS_VISIBILITY, {});
});
// Depth selector
$(GLPIImpact.selectors.maxDepth).on('input', function() {
var previous = GLPIImpact.maxDepth;
GLPIImpact.setDepth($(GLPIImpact.selectors.maxDepth).val());
GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_MAX_DEPTH, {
oldDepth: previous,
newDepth: GLPIImpact.maxDepth,
});
});
$(GLPIImpact.selectors.toggleFullscreen).click(function() {
GLPIImpact.toggleFullscreen();
});
// Filter available itemtypes
$(GLPIImpact.selectors.sideSearchFilterItemtype).on('input', function() {
var value = $(GLPIImpact.selectors.sideSearchFilterItemtype).val().toLowerCase();
$(GLPIImpact.selectors.sideFilterItem + ' img').each(function() {
var itemtype = $(this).attr('title').toLowerCase();
if (value == "" || itemtype.indexOf(value) != -1) {
$(this).parent().show();
} else {
$(this).parent().hide();
}
});
});
// Exit type selection and enter asset search
$(GLPIImpact.selectors.sideFilterItem).click(function() {
var img = $(this).find('img').eq(0);
GLPIImpact.selectedItemtype = $(img).attr('data-itemtype');
$(GLPIImpact.selectors.sideSearch).show();
$(GLPIImpact.selectors.sideSearch + " img").attr('title', $(img).attr('title'));
$(GLPIImpact.selectors.sideSearch + " img").attr('src', $(img).attr('src'));
$(GLPIImpact.selectors.sideSearch + " > h4 > span").html($(img).attr('title'));
$(GLPIImpact.selectors.sideSearchSelectItemtype).hide();
// Empty search
GLPIImpact.searchAssets(
GLPIImpact.selectedItemtype,
JSON.stringify(GLPIImpact.getUsedAssets()),
$(GLPIImpact.selectors.sideFilterAssets).val(),
0
);
});
// Exit asset search and return to type selection
$(GLPIImpact.selectors.sideSearch + ' > h4 > i').click(function() {
$(GLPIImpact.selectors.sideSearch).hide();
$(GLPIImpact.selectors.sideSearchSelectItemtype).show();
$(GLPIImpact.selectors.sideSearchResults).html("");
});
$(GLPIImpact.selectors.sideFilterAssets).on('input', function() {
// Reset results
$(GLPIImpact.selectors.sideSearchResults).html("");
$(GLPIImpact.selectors.sideSearchMore).hide();
$(GLPIImpact.selectors.sideSearchSpinner).show();
$(GLPIImpact.selectors.sideSearchNoResults).hide();
searchAssetsDebounced(
GLPIImpact.selectedItemtype,
JSON.stringify(GLPIImpact.getUsedAssets()),
$(GLPIImpact.selectors.sideFilterAssets).val(),
0
);
});
// Load more results on "More..." click
$(GLPIImpact.selectors.sideSearchMore).on('click', function() {
GLPIImpact.searchAssets(
GLPIImpact.selectedItemtype,
JSON.stringify(GLPIImpact.getUsedAssets()),
$(GLPIImpact.selectors.sideFilterAssets).val(),
++GLPIImpact.addAssetPage
);
});
// Watch for color changes (depends)
$(GLPIImpact.selectors.dependsColor).change(function(){
var previous = GLPIImpact.edgeColors[GLPIImpact.BACKWARD];
GLPIImpact.setEdgeColors({
backward: $(GLPIImpact.selectors.dependsColor).val(),
});
GLPIImpact.updateStyle();
GLPIImpact.cy.trigger("change");
GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_DEPENDS_COLOR, {
oldColor: previous,
newColor: GLPIImpact.edgeColors[GLPIImpact.BACKWARD]
});
});
// Watch for color changes (impact)
$(GLPIImpact.selectors.impactColor).change(function(){
var previous = GLPIImpact.edgeColors[GLPIImpact.FORWARD];
GLPIImpact.setEdgeColors({
forward: $(GLPIImpact.selectors.impactColor).val(),
});
GLPIImpact.updateStyle();
GLPIImpact.cy.trigger("change");
GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_IMPACT_COLOR, {
oldColor: previous,
newColor: GLPIImpact.edgeColors[GLPIImpact.FORWARD]
});
});
// Watch for color changes (impact and depends)
$(GLPIImpact.selectors.impactAndDependsColor).change(function(){
var previous = GLPIImpact.edgeColors[GLPIImpact.BOTH];
GLPIImpact.setEdgeColors({
both: $(GLPIImpact.selectors.impactAndDependsColor).val(),
});
GLPIImpact.updateStyle();
GLPIImpact.cy.trigger("change");
GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_IMPACT_AND_DEPENDS_COLOR, {
oldColor: previous,
newColor: GLPIImpact.edgeColors[GLPIImpact.BOTH]
});
});
// Handle drag & drop on add node search result
$(document).on('mousedown', GLPIImpact.selectors.sideSearchResults + ' p', function(e) {
// Only on left click and not for disabled item
if (e.which !== 1
|| $(e.target).hasClass('impact-res-disabled')
|| $(e.target).parent().hasClass('impact-res-disabled')) {
return;
}
// Tmp data to be shared with mousedown event
GLPIImpact.eventData.addNodeStart = {
id : $(this).attr("data-id"),
type: $(this).attr("data-type"),
};
// Show preview icon at cursor location
$(GLPIImpact.selectors.dropPreview).css({
left: e.clientX - 24,
top: e.clientY - 24,
});
$(GLPIImpact.selectors.dropPreview).attr('src', $(this).find('img').attr('src'));
$(GLPIImpact.selectors.dropPreview).show();
$("*").css({cursor: "grabbing"});
});
// Handle drag & drop on add node search result
$(document).on('mouseup', function(e) {
// Middle click on badge, open link in new tab
if (event.which == 2) {
GLPIImpact.checkBadgeHitboxes(
GLPIImpact.projectIntoViewport(e.clientX, e.clientY, true),
true,
true
);
}
if (GLPIImpact.eventData.addNodeStart === undefined) {
return;
}
if (e.target.nodeName == "CANVAS") {
// Add node at event position
GLPIImpact.addNode(
GLPIImpact.eventData.addNodeStart.id,
GLPIImpact.eventData.addNodeStart.type,
GLPIImpact.projectIntoViewport(e.clientX, e.clientY, false)
);
}
$(GLPIImpact.selectors.dropPreview).hide();
// Clear tmp event data
GLPIImpact.eventData.addNodeStart = undefined;
$("*").css('cursor', "");
});
$(document).on('mousemove', function(e) {
if (GLPIImpact.eventData.addNodeStart === undefined) {
return;
}
// Show preview icon at cursor location
$(GLPIImpact.selectors.dropPreview).css({
left: e.clientX - 24,
top: e.clientY - 24,
});
});
},
/**
* Init and render the canvas overlay used to show the badges
*/
initCanvasOverlay: function() {
var layer = GLPIImpact.cy.cyCanvas();
var canvas = layer.getCanvas();
var ctx = canvas.getContext('2d');
GLPIImpact.cy.on("render cyCanvas.resize", function() {
layer.resetTransform(ctx);
layer.clear(ctx);
GLPIImpact.badgesHitboxes = [];
GLPIImpact.cy.filter("node:childless:visible").forEach(function(node) {
// Stop here if the node has no badge defined
if (!node.data('badge')) {
return;
}
// Set badge color, adjust contract as needed (target ratio is > 1.8)
var rgb = hexToRgb(node.data('badge').color);
while (contrast([255, 255, 255], [rgb.r, rgb.g, rgb.b]) < 1.8) {
rgb.r *= 0.95;
rgb.g *= 0.95;
rgb.b *= 0.95;
}
// Set badge position (bottom right corner of the node)
var bbox = node.renderedBoundingBox({
includeLabels : false,
includeOverlays: false,
includeNodes : true,
});
var pos = {
x: bbox.x2 + GLPIImpact.cy.zoom(),
y: bbox.y2 + GLPIImpact.cy.zoom(),
};
// Register badge position so it can be clicked
GLPIImpact.badgesHitboxes.push({
position: pos,
target : node.data('badge').target,
itemtype: node.data('id').split(GLPIImpact.NODE_ID_SEPERATOR)[0],
id : node.data('id').split(GLPIImpact.NODE_ID_SEPERATOR)[1],
});
// Draw the badge
ctx.beginPath();
ctx.arc(pos.x, pos.y, 4 * GLPIImpact.cy.zoom(), 0, 2 * Math.PI, false);
ctx.fillStyle = "rgb(" + rgb.r + ", " + rgb.g + ", " + rgb.b + ")";
ctx.fill();
// Check if text should be light or dark by calculating the
// grayscale of the background color
var greyscale = (
Math.round(rgb.r * 299)
+ Math.round(rgb.g * 587)
+ Math.round(rgb.b * 114)
) / 1000;
ctx.fillStyle = (greyscale >= 138) ? '#4e4e4e' : 'white';
// Print number
ctx.font = 6 * GLPIImpact.cy.zoom() + "px sans-serif";
ctx.fillText(
node.data('badge').count,
pos.x - (1.95 * GLPIImpact.cy.zoom()),
pos.y + (2.23 * GLPIImpact.cy.zoom())
);
});
});
}
};
var searchAssetsDebounced = _.debounce(GLPIImpact.searchAssets, 400, false);