%PDF- %PDF-
Direktori : /var/www/projetos/suporte.iigd.com.br.old/js/ |
Current File : /var/www/projetos/suporte.iigd.com.br.old/js/impact.js |
/** * --------------------------------------------------------------------- * * GLPI - Gestionnaire Libre de Parc Informatique * * http://glpi-project.org * * @copyright 2015-2022 Teclib' and contributors. * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- * * LICENSE * * This file is part of GLPI. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * --------------------------------------------------------------------- */ // Load cytoscape var cytoscape = window.cytoscape; // Needed for JS lint validation /* global _ */ /* global hexToRgb */ /* global contrast */ var GLPIImpact = { // Constants to represent nodes and edges NODE: 1, EDGE: 2, // Constants for graph direction (bitmask) DEFAULT : 0, // 0b00 FORWARD : 1, // 0b01 BACKWARD: 2, // 0b10 BOTH : 3, // 0b11 // Constants for graph edition mode EDITION_DEFAULT : 1, EDITION_ADD_NODE : 2, EDITION_ADD_EDGE : 3, EDITION_DELETE : 4, EDITION_ADD_COMPOUND: 5, EDITION_SETTINGS : 6, // Constants for ID separator NODE_ID_SEPERATOR: "::", EDGE_ID_SEPERATOR: "->", // Constants for delta action DELTA_ACTION_ADD : 1, DELTA_ACTION_UPDATE: 2, DELTA_ACTION_DELETE: 3, // Constants for action stack ACTION_MOVE : 1, ACTION_ADD_NODE : 2, ACTION_ADD_EDGE : 3, ACTION_ADD_COMPOUND : 4, ACTION_ADD_GRAPH : 5, ACTION_EDIT_COMPOUND : 6, ACTION_REMOVE_FROM_COMPOUND : 7, ACTION_DELETE : 8, ACTION_EDIT_MAX_DEPTH : 9, ACTION_EDIT_IMPACT_VISIBILITY : 10, ACTION_EDIT_DEPENDS_VISIBILITY : 11, ACTION_EDIT_DEPENDS_COLOR : 12, ACTION_EDIT_IMPACT_COLOR : 13, ACTION_EDIT_IMPACT_AND_DEPENDS_COLOR: 14, // Constans for depth DEFAULT_DEPTH: 5, MAX_DEPTH: 10, NO_DEPTH_LIMIT: 10000, // Store the initial state of the graph initialState: null, // Store the visibility settings of the different direction of the graph directionVisibility: {}, // Store defaults colors for edge defaultColors: {}, // Store color for egdes edgeColors: {}, // Cytoscape instance cy: null, // The impact network container impactContainer: null, // The graph edition mode editionMode: null, // Start node of the graph (id) startNode: null, // Maximum depth of the graph (default 5) maxDepth: this.DEFAULT_DEPTH, // Is the graph readonly ? readonly: true, // Fullscreen fullscreen: false, // Used in add assets sidebar selectedItemtype: "", addAssetPage: 0, // Action stack for undo/redo undoStack: [], redoStack: [], // Buffer used when generating positions for unset nodes no_positions: [], // Register badges hitbox so they can be clicked badgesHitboxes: [], // Store selectors selectors: { // Dialogs ongoingDialog : "#ongoing_dialog", ongoingDialogBody : "#ongoing_dialog .modal-body", editCompoundDialog : "#edit_compound_dialog", editCompoundDialogSave : '#edit_compound_save', editCompoundDialogCancel: '#edit_compound_cancel', // Inputs compoundName : "input[name=compound_name]", compoundColor : "input[name=compound_color]", dependsColor : "input[name=depends_color]", impactColor : "input[name=impact_color]", impactAndDependsColor: "input[name=impact_and_depends_color]", toggleImpact : "#toggle_impact", toggleDepends : "#toggle_depends", maxDepth : "#max_depth", maxDepthView : "#max_depth_view", // Toolbar helpText : "#help_text", save : "#save_impact", addNode : "#add_node", addEdge : "#add_edge", addCompound : "#add_compound", deleteElement : "#delete_element", export : "#export_graph", expandToolbar : "#expand_toolbar", toggleFullscreen: "#toggle_fullscreen", impactSettings : "#impact_settings", sideToggle : ".impact-side-toggle", sideToggleIcon : ".impact-side-toggle i", undo : "#impact_undo", redo : "#impact_redo", // Sidebar content side : ".impact-side", sidePanel : ".impact-side-panel", sideAddNode : ".impact-side-add-node", sideSettings : ".impact-side-settings", sideSearch : ".impact-side-search", sideSearchSpinner : ".impact-side-search-spinner", sideSearchNoResults : ".impact-side-search-no-results", sideSearchMore : ".impact-side-search-more", sideSearchResults : ".impact-side-search-results", sideSearchSelectItemtype: ".impact-side-select-itemtype", sideSearchFilterItemtype: "#impact-side-filter-itemtypes", sideFilterAssets : "#impact-side-filter-assets", sideFilterItem : ".impact-side-filter-itemtypes-item", // Others form : "form[name=form_impact_network]", dropPreview: ".impact-drop-preview", }, // Data that needs to be stored/shared between events eventData: { addEdgeStart : null, // Store starting node of a new edge tmpEles : null, // Temporary collection used when adding an edge lastClicktimestamp : null, // Store last click timestamp lastClickTarget : null, // Store last click target boxSelected : [], grabNodeStart : null, boundingBox : null, showPointerForBadge: false, previousCursor : "default", ctrlDown : false, editCompound : null, // Compound being edited }, /** * Add given action to undo stack and reset redo stack * @param {Number} action_code const ACTION_XXXX * @param {Object} data data specific to the action */ addToUndo : function(action_code, data) { // Add new item to undo list this.undoStack.push({ code: action_code, data: data }); $(this.selectors.undo).removeClass("impact-disabled"); // Clear redo list this.redoStack = []; $(this.selectors.redo).addClass("impact-disabled"); }, /** * Undo last action */ undo: function() { // Empty stack, stop here if (this.undoStack.length === 0) { return; } var action = this.undoStack.pop(); var data = action.data; // Add action to redo stack this.redoStack.push(action); $(this.selectors.redo).removeClass("impact-disabled"); switch (action.code) { // Set node to old position // Available data: node, oldPosition, newPosition and newParent case this.ACTION_MOVE: this.cy.filter("node" + this.makeIDSelector(data.node)) .position({ x: data.oldPosition.x, y: data.oldPosition.y, }); if (data.newParent !== null) { this.cy.filter("node" + this.makeIDSelector(data.node)) .move({parent: null}); } break; // Remove node // Available data: toAdd case this.ACTION_ADD_NODE: this.cy.getElementById(data.toAdd.data.id).remove(); break; // Delete edge // Available data; id, data case this.ACTION_ADD_EDGE: this.cy.remove("edge" + this.makeIDSelector(data.id)); this.updateFlags(); break; // Delete compound // Available data: data, children case this.ACTION_ADD_COMPOUND: data.children.forEach(function(id) { GLPIImpact.cy.filter("node" + GLPIImpact.makeIDSelector(id)) .move({parent: null}); }); this.cy.remove("node" + this.makeIDSelector(data.data.id)); this.updateFlags(); break; // Remove the newly added graph // Available data: edges, nodes, compounds case this.ACTION_ADD_GRAPH: // Delete edges data.edges.forEach(function(edge) { GLPIImpact.cy.getElementById(edge.id).remove(); }); // Delete compounds data.compounds.forEach(function(compound) { compound.compoundChildren.forEach(function(nodeId) { GLPIImpact.cy.getElementById(nodeId).move({ parent: null }); }); GLPIImpact.cy.getElementById(compound.compoundData.id).remove(); }); // Delete nodes data.nodes.forEach(function(node) { GLPIImpact.cy.getElementById(node.nodeData.id).remove(); }); this.updateFlags(); break; // Revert edit // Available data: id, label, color, oldLabel, oldColor case this.ACTION_EDIT_COMPOUND: this.cy.filter("node" + this.makeIDSelector(data.id)).data({ label: data.oldLabel, color: data.oldColor, }); GLPIImpact.cy.trigger("change"); break; // Re-add node to the compound (and recreate it needed) // Available data: nodeData, compoundData, children case this.ACTION_REMOVE_FROM_COMPOUND: if (data.children.length <= 2) { // Recreate the compound and re-add every nodes this.cy.add({ group: "nodes", data: data.compoundData, }); data.children.forEach(function(childId) { GLPIImpact.cy.getElementById(childId) .move({parent: data.compoundData.id}); }); } else { // Add the node that was removed this.cy.getElementById(data.nodeData.id) .move({parent: data.compoundData.id}); } break; // Re-add given nodes, edges and compounds // Available data: nodes, edges, compounds case this.ACTION_DELETE: // Add nodes data.nodes.forEach(function(node) { var newNode = GLPIImpact.cy.add({ group: "nodes", data: node.nodeData, }); newNode.position(node.nodePosition); }); // Add compound data.compounds.forEach(function(compound) { GLPIImpact.cy.add({ group: "nodes", data: compound.compoundData, }); compound.compoundChildren.forEach(function(nodeId) { GLPIImpact.cy.getElementById(nodeId).move({ parent: compound.compoundData.id }); }); }); // Add edges data.edges.forEach(function(edge) { GLPIImpact.cy.add({ group: "edges", data: edge, }); }); this.updateFlags(); break; // Toggle impact visibility case this.ACTION_EDIT_IMPACT_VISIBILITY: this.toggleVisibility(this.FORWARD); $(GLPIImpact.selectors.toggleImpact).prop( 'checked', !$(GLPIImpact.selectors.toggleImpact).prop('checked') ); break; // Toggle depends visibility case this.ACTION_EDIT_DEPENDS_VISIBILITY: this.toggleVisibility(this.BACKWARD); $(GLPIImpact.selectors.toggleDepends).prop( 'checked', !$(GLPIImpact.selectors.toggleDepends).prop('checked') ); break; // Set previous value for "depends" color // Available data: oldColor, newColor case this.ACTION_EDIT_DEPENDS_COLOR: this.setEdgeColors({ backward: data.oldColor, }); $(GLPIImpact.selectors.dependsColor).val( GLPIImpact.edgeColors[GLPIImpact.BACKWARD] ); this.updateStyle(); this.cy.trigger("change"); break; // Set previous value for "impact" color // Available data: oldColor, newColor case this.ACTION_EDIT_IMPACT_COLOR: this.setEdgeColors({ forward: data.oldColor, }); $(GLPIImpact.selectors.impactColor).val( GLPIImpact.edgeColors[GLPIImpact.FORWARD] ); this.updateStyle(); this.cy.trigger("change"); break; // Set previous value for "impact and depends" color // Available data: oldColor, newColor case this.ACTION_EDIT_IMPACT_AND_DEPENDS_COLOR: this.setEdgeColors({ both: data.oldColor, }); $(GLPIImpact.selectors.impactAndDependsColor).val( GLPIImpact.edgeColors[GLPIImpact.BOTH] ); this.updateStyle(); this.cy.trigger("change"); break; // Set previous value for max depth // Available data: oldDepth, newDepth case this.ACTION_EDIT_MAX_DEPTH: this.setDepth(data.oldDepth); $(GLPIImpact.selectors.maxDepth).val(data.oldDepth); break; } if (this.undoStack.length === 0) { $(this.selectors.undo).addClass("impact-disabled"); } }, /** * Redo last undoed action */ redo: function() { // Empty stack, stop here if (this.redoStack.length === 0) { return; } var action = this.redoStack.pop(); var data = action.data; // Add action to undo stack this.undoStack.push(action); $(this.selectors.undo).removeClass("impact-disabled"); switch (action.code) { // Set node to new position // Available data: node, oldPosition, newPosition and newParent case this.ACTION_MOVE: this.cy.filter("node" + this.makeIDSelector(data.node)) .position({ x: data.newPosition.x, y: data.newPosition.y, }); if (data.newParent !== null) { this.cy.filter("node" + this.makeIDSelector(data.node)) .move({parent: data.newParent}); } break; // Add the node again // Available data: toAdd case this.ACTION_ADD_NODE: this.cy.add(data.toAdd); break; // Add edge // Available data; id, data case this.ACTION_ADD_EDGE: this.cy.add({ group: "edges", data: data, }); this.updateFlags(); break; // Add compound and update its children // Available data: data, children case this.ACTION_ADD_COMPOUND: this.cy.add({ group: "nodes", data: data.data, }); data.children.forEach(function(id) { GLPIImpact.cy.filter("node" + GLPIImpact.makeIDSelector(id)) .move({parent: data.data.id}); }); this.updateFlags(); break; // Insert again the graph // Available data: edges, nodes, compounds case this.ACTION_ADD_GRAPH: // Add nodes data.nodes.forEach(function(node) { var newNode = GLPIImpact.cy.add({ group: "nodes", data: node.nodeData, }); newNode.position(node.nodePosition); }); // Add compound data.compounds.forEach(function(compound) { GLPIImpact.cy.add({ group: "nodes", data: compound.compoundData, }); compound.compoundChildren.forEach(function(nodeId) { GLPIImpact.cy.getElementById(nodeId).move({ parent: compound.compoundData.id }); }); }); // Add edges data.edges.forEach(function(edge) { GLPIImpact.cy.add({ group: "edges", data: edge, }); }); this.updateFlags(); break; // Reapply edit // Available data : id, label, color, previousLabel, previousColor case this.ACTION_EDIT_COMPOUND: this.cy.filter("node" + this.makeIDSelector(data.id)).data({ label: data.label, color: data.color, }); GLPIImpact.cy.trigger("change"); break; // Remove node from the compound (and delete if needed) // Available data: nodeData, compoundData, children case this.ACTION_REMOVE_FROM_COMPOUND: if (data.children.length <= 2) { // Remove every nodes and delete the compound data.children.forEach(function(childId) { GLPIImpact.cy.getElementById(childId) .move({parent: null}); }); this.cy.getElementById(data.compoundData.id).remove(); } else { // Remove only he node that was re-added this.cy.getElementById(data.nodeData.id) .move({parent: null}); } break; // Re-delete given nodes, edges and compounds // Available data: nodes, edges, compounds case this.ACTION_DELETE: // Delete edges data.edges.forEach(function(edge) { GLPIImpact.cy.getElementById(edge.id).remove(); }); // Delete compounds data.compounds.forEach(function(compound) { compound.compoundChildren.forEach(function(nodeId) { GLPIImpact.cy.getElementById(nodeId).move({ parent: null }); }); GLPIImpact.cy.getElementById(compound.compoundData.id).remove(); }); // Delete nodes data.nodes.forEach(function(node) { GLPIImpact.cy.getElementById(node.id).remove(); }); this.updateFlags(); break; // Toggle impact visibility case this.ACTION_EDIT_IMPACT_VISIBILITY: this.toggleVisibility(this.FORWARD); $(GLPIImpact.selectors.toggleImpact).prop( 'checked', !$(GLPIImpact.selectors.toggleImpact).prop('checked') ); break; // Toggle depends visibility case this.ACTION_EDIT_DEPENDS_VISIBILITY: this.toggleVisibility(this.BACKWARD); $(GLPIImpact.selectors.toggleDepends).prop( 'checked', !$(GLPIImpact.selectors.toggleDepends).prop('checked') ); break; // Set new value for "depends" color // Available data: oldColor, newColor case this.ACTION_EDIT_DEPENDS_COLOR: this.setEdgeColors({ backward: data.newColor, }); $(GLPIImpact.selectors.dependsColor).val( GLPIImpact.edgeColors[GLPIImpact.BACKWARD] ); this.updateStyle(); this.cy.trigger("change"); break; // Set new value for "impact" color // Available data: oldColor, newColor case this.ACTION_EDIT_IMPACT_COLOR: this.setEdgeColors({ forward: data.newColor, }); $(GLPIImpact.selectors.forwardColor).val( "set", GLPIImpact.edgeColors[GLPIImpact.FORWARD] ); this.updateStyle(); this.cy.trigger("change"); break; // Set new value for "impact and depends" color // Available data: oldColor, newColor case this.ACTION_EDIT_IMPACT_AND_DEPENDS_COLOR: this.setEdgeColors({ both: data.newColor, }); $(GLPIImpact.selectors.impactAndDependsColor).val( GLPIImpact.edgeColors[GLPIImpact.BOTH] ); this.updateStyle(); this.cy.trigger("change"); break; // Set new value for max depth // Available data: oldDepth, newDepth case this.ACTION_EDIT_MAX_DEPTH: this.setDepth(data.newDepth); $(GLPIImpact.selectors.maxDepth).val(data.newDepth); break; } if (this.redoStack.length === 0) { $(this.selectors.redo).addClass("impact-disabled"); } }, /** * Selector for nodes to hide according to depth and flag settings */ getHiddenSelector: function() { var depthSelector = '[depth > ' + this.maxDepth + '][depth !> ' + Number.MAX_SAFE_INTEGER + ']'; var flagSelector; // We have to compute the flags ourselves as bit comparison operators are // not supported by cytoscape selectors var forward = this.directionVisibility[this.FORWARD]; var backward = this.directionVisibility[this.BACKWARD]; if (forward && backward) { // Hide nothing flagSelector = "[flag = -1]"; } else if (forward && !backward) { // Hide backward flagSelector = "[flag = " + this.BACKWARD + "]"; } else if (!forward && backward) { // Hide forward flagSelector = "[flag = " + this.FORWARD + "]"; } else { // Hide all but start node and not connected nodes flagSelector = '[flag != 0]'; } return flagSelector + ', ' + depthSelector; }, /** * Get network style * * @returns {Array} */ getNetworkStyle: function() { let body_text_color = $(document.body).css("--tblr-body-color"); // If body color is somehow invalid, default to black if (!body_text_color || body_text_color === "") { body_text_color = "#000000"; } return [ { selector: 'core', style: { 'selection-box-opacity' : '0.2', 'selection-box-border-width': '0', 'selection-box-color' : '#24acdf' } }, { selector: 'node', style: { color: body_text_color } }, { selector: 'node:parent', style: { 'padding' : '30px', 'shape' : 'roundrectangle', 'border-width' : '1px', 'background-opacity': '0.5', 'font-size' : '1.1em', 'background-color' : '#d2d2d2', 'text-margin-y' : '20px', 'text-opacity' : 0.7, } }, { selector: 'node:parent[label]', style: { 'label': 'data(label)', } }, { selector: 'node:parent[color]', style: { 'border-color' : 'data(color)', 'background-color' : 'data(color)', } }, { selector: 'node[image]', style: { 'label' : 'data(label)', 'shape' : 'rectangle', 'background-color' : '#666', 'background-image' : 'data(image)', 'background-fit' : 'contain', 'background-opacity': '0', 'font-size' : '1em', 'text-opacity' : 0.7, 'overlay-opacity' : 0.01, 'overlay-color' : "white", } }, { selector: 'node[highlight=1]', style: { 'font-weight': 'bold', } }, { selector: ':selected', style: { 'overlay-opacity': 0.2, 'overlay-color' : "gray", } }, { selector: '[todelete=1]:selected', style: { 'overlay-opacity': 0.2, 'overlay-color': 'red', } }, { selector: GLPIImpact.getHiddenSelector(), style: { 'display': 'none', } }, { selector: '[id="tmp_node"]', style: { // Use opacity instead of display none here as this will make // the edges connected to this node still visible 'opacity': 0, } }, { selector: 'edge', style: { 'width' : 1, 'line-color' : this.edgeColors[0], 'target-arrow-color' : this.edgeColors[0], 'target-arrow-shape' : 'triangle', 'arrow-scale' : 0.7, 'curve-style' : 'bezier', 'source-endpoint' : 'outside-to-node-or-label', 'target-endpoint' : 'outside-to-node-or-label', 'source-distance-from-node': '2px', 'target-distance-from-node': '2px', } }, { selector: 'edge[target="tmp_node"]', style: { // We want the arrow to go exactly where the cursor of the user // is on the graph, no padding. 'source-endpoint' : 'inside-to-node', 'target-endpoint' : 'inside-to-node', 'source-distance-from-node': '0px', 'target-distance-from-node': '0px', } }, { selector: '[flag=' + GLPIImpact.FORWARD + ']', style: { 'line-color' : this.edgeColors[GLPIImpact.FORWARD], 'target-arrow-color': this.edgeColors[GLPIImpact.FORWARD], } }, { selector: '[flag=' + GLPIImpact.BACKWARD + ']', style: { 'line-color' : this.edgeColors[GLPIImpact.BACKWARD], 'target-arrow-color': this.edgeColors[GLPIImpact.BACKWARD], } }, { selector: '[flag=' + GLPIImpact.BOTH + ']', style: { 'line-color' : this.edgeColors[GLPIImpact.BOTH], 'target-arrow-color': this.edgeColors[GLPIImpact.BOTH], } } ]; }, /** * Get network layout * * @returns {Object} */ getPresetLayout: function (positions) { this.no_positions = []; return { name: 'preset', positions: function(node) { var x = 0; var y = 0; if (!node.isParent() && positions[node.data('id')] !== undefined) { x = parseFloat(positions[node.data('id')].x); y = parseFloat(positions[node.data('id')].y); } return { x: x, y: y, }; } }; }, /** * Generate postion for nodes that are not saved in the current context * * Firstly, order the positionless nodes in a way that the one that depends * on others positionless nodes are placed after their respective * dependencies * * Secondly, try to place each nodes on the graph: * 1) take a random non positionless neighbor of our node * 2) Find the closest node to this neighbor, save the distance (if this * neighbor has no neighbor of its own use a set value for the distance) * 3) Try to place the node at the left or the right of the neighbor ( * depending on the edge direction, we want the graph to flow from left * to right) at the saved position. * 4) If the position is not avaible, try at various angles bewteen -75° * and 75° * 5) If the position is still not available, increase the distance and * try again until a valid position is found */ generateMissingPositions: function() { // Safety check, should not happen if (this.cy.filter("node:childless").length == this.no_positions.length) { // Set a random node as valid this.no_positions.pop(); } // Keep tracks of the id of all the no yet placed nodes var not_placed = []; this.no_positions.forEach(function(node){ not_placed.push(node.data('id')); }); // First we need to order no_positions in a way that the ones that depend // on the positions of other nodes with no position are used last var clean_order = []; var np_valid = []; while (this.no_positions.length !== 0) { this.no_positions.forEach(function(node, index) { // Check that any neibhor is either valid (no in not placed) or has // just been validated (in np_valid) var valid = false; node.neighborhood().forEach(function(ele) { if (valid) { return; } // We don't need edges if (!ele.isNode()) { return; } if (not_placed.indexOf(ele.data('id')) === -1 || np_valid.indexOf(ele.data('id')) !== -1) { valid = true; } }); if (valid) { // Add to the list of validated nodes, set order and remove it // from buffer np_valid.push(node.data('id')); clean_order.push(node); // not_placed.splice(index, 1); GLPIImpact.no_positions.splice(index, 1); } }); } this.no_positions = clean_order; // Generate positions for nodes which lake them this.no_positions.forEach(function(node){ // Find random neighbor with a valid position var neighbor = null; node.neighborhood().forEach(function(ele) { // We already found a valid neighor, skip until the end if (neighbor !== null) { return; } if (!ele.isNode()) { return; } // Ignore our starting node if (ele.data('id') == node.data('id')) { return; } // Ignore node with no positions not yet placed if (not_placed.indexOf(ele.data('id')) !== -1) { return; } // Valid neighor, let's pick it neighbor = ele; }); // Should not happen if no_positions is correctly sorted if (neighbor === null) { return; } // We now need to find the closest node to the neighor var closest = null; var distance = Number.MAX_SAFE_INTEGER; neighbor.neighborhood().forEach(function(ele){ if (!ele.isNode()) { return; } var ele_distance = GLPIImpact.getDistance(neighbor.position(), ele.position()); if (ele_distance < distance) { distance = ele_distance; closest = ele; } }); // If our neighbor node has no neighors himself, use a set distance if (closest === null) { distance = 100; } // Find the edge between our node and the chosen neighbor var edge = node.edgesTo(neighbor)[0]; if (edge == undefined) { edge = neighbor.edgesTo(node)[0]; } // Set direction factor according to the edge direction (are we the // source or the target of this edge ?). This factor will be used to // know if the node must be placed before or after the neighbor var direction_factor; if (edge.data('target') == node.data('id')) { direction_factor = 1; } else { direction_factor = -1; } // Keep trying to place the node until we succeed$ var success = false; while(!success) { var angle = 0; var angle_mirror = false; // Try all possible angles bewteen -75° and 75° while (angle !== -75) { // Calculate the position var position = { x: direction_factor * (distance * Math.cos(angle * (Math.PI / 180))) + (neighbor.position().x), y: distance * Math.sin(angle * (Math.PI / 180)) + neighbor.position().y, }; // Check if position is available var available = true; GLPIImpact.cy.filter().forEach(function(ele){ var bdb = ele.boundingBox(); // var bdb = ele.renderedBoundingBox(); if ((bdb.x1 - 20) < position.x && (bdb.x2 + 20) > position.x && (bdb.y1 - 20) < position.y && (bdb.y2 + 20) > position.y) { available = false; } }); // Success, set the node position and go to the next one if (available) { node.position(position); var np_index = not_placed.indexOf(node.data('id')); not_placed.splice(np_index, 1); success = true; break; } if (!angle_mirror && angle !== 0) { // We tried X°, lets try the "mirror angle" -X°] angle = angle * -1; angle_mirror = true; } else { // Add 15° and return to positive number if (angle < 0) { angle = 0 - angle; angle_mirror = false; } angle += 15; } } // Increase distance and try again distance += 30; } }); // Reset buffer this.no_positions = []; }, /** * Get network layout * * @returns {Object} */ getDagreLayout: function () { return { name: 'dagre', rankDir: 'LR', fit: false }; }, /** * Get the current state of the graph * * @returns {Object} */ getCurrentState: function() { var data = {edges: {}, compounds: {}, items: {}}; // Load edges GLPIImpact.cy.edges().forEach(function(edge) { data.edges[edge.data('id')] = { source: edge.data('source'), target: edge.data('target'), }; }); // Load compounds GLPIImpact.cy.filter("node:parent").forEach(function(compound) { data.compounds[compound.data('id')] = { name: compound.data('label'), color: compound.data('color'), }; }); // Load items GLPIImpact.cy.filter("node:childless").forEach(function(node) { data.items[node.data('id')] = { impactitem_id: node.data('impactitem_id'), parent : node.data('parent'), position : node.position() }; }); return data; }, /** * Delta computation for edges * * @returns {Object} */ computeEdgeDelta: function(currentEdges) { var edgesDelta = {}; // First iterate on the edges we had in the initial state Object.keys(GLPIImpact.initialState.edges).forEach(function(edgeID) { var edge = GLPIImpact.initialState.edges[edgeID]; if (Object.prototype.hasOwnProperty.call(currentEdges, edgeID)) { // If the edge is still here in the current state, nothing happened // Remove it from the currentEdges data so we can skip it later delete currentEdges[edgeID]; } else { // If the edge is missing in the current state, it has been deleted var source = edge.source.split(GLPIImpact.NODE_ID_SEPERATOR); var target = edge.target.split(GLPIImpact.NODE_ID_SEPERATOR); edgesDelta[edgeID] = { action : GLPIImpact.DELTA_ACTION_DELETE, itemtype_source : source[0], items_id_source : source[1], itemtype_impacted: target[0], items_id_impacted: target[1] }; } }); // Now iterate on the edges we have in the current state // Since we removed the edges that were not modified in the previous step, // the remaining edges can only be new ones Object.keys(currentEdges).forEach(function (edgeID) { var edge = currentEdges[edgeID]; var source = edge.source.split(GLPIImpact.NODE_ID_SEPERATOR); var target = edge.target.split(GLPIImpact.NODE_ID_SEPERATOR); edgesDelta[edgeID] = { action : GLPIImpact.DELTA_ACTION_ADD, itemtype_source : source[0], items_id_source : source[1], itemtype_impacted: target[0], items_id_impacted: target[1] }; }); return edgesDelta; }, /** * Delta computation for compounds * * @returns {Object} */ computeCompoundsDelta: function(currentCompounds) { var compoundsDelta = {}; // First iterate on the compounds we had in the initial state Object.keys(GLPIImpact.initialState.compounds).forEach(function(compoundID) { var compound = GLPIImpact.initialState.compounds[compoundID]; if (Object.prototype.hasOwnProperty.call(currentCompounds, compoundID)) { // If the compound is still here in the current state var currentCompound = currentCompounds[compoundID]; // Check for updates ... if (compound.name != currentCompound.name || compound.color != currentCompound.color) { compoundsDelta[compoundID] = { action: GLPIImpact.DELTA_ACTION_UPDATE, name : currentCompound.name, color : currentCompound.color }; } // Remove it from the currentCompounds data delete currentCompounds[compoundID]; } else { // If the compound is missing in the current state, it's been deleted compoundsDelta[compoundID] = { action : GLPIImpact.DELTA_ACTION_DELETE, }; } }); // Now iterate on the compounds we have in the current state Object.keys(currentCompounds).forEach(function (compoundID) { compoundsDelta[compoundID] = { action: GLPIImpact.DELTA_ACTION_ADD, name : currentCompounds[compoundID].name, color : currentCompounds[compoundID].color }; }); return compoundsDelta; }, /** * Delta computation for parents * * @returns {Object} */ computeContext: function(currentNodes) { var positions = {}; Object.keys(currentNodes).forEach(function (nodeID) { var node = currentNodes[nodeID]; positions[nodeID] = { x: node.position.x, y: node.position.y }; }); return { node_id : this.startNode, positions : JSON.stringify(positions), zoom : GLPIImpact.cy.zoom(), pan_x : GLPIImpact.cy.pan().x, pan_y : GLPIImpact.cy.pan().y, impact_color : GLPIImpact.edgeColors[GLPIImpact.FORWARD], depends_color : GLPIImpact.edgeColors[GLPIImpact.BACKWARD], impact_and_depends_color: GLPIImpact.edgeColors[GLPIImpact.BOTH], show_depends : GLPIImpact.directionVisibility[GLPIImpact.BACKWARD], show_impact : GLPIImpact.directionVisibility[GLPIImpact.FORWARD], max_depth : GLPIImpact.maxDepth, }; }, /** * Delta computation for parents * * @returns {Object} */ computeItemsDelta: function(currentNodes) { var itemsDelta = {}; // Now iterate on the parents we have in the current state Object.keys(currentNodes).forEach(function (nodeID) { var node = currentNodes[nodeID]; itemsDelta[node.impactitem_id] = { action : GLPIImpact.DELTA_ACTION_UPDATE, parent_id: node.parent, }; // Set parent to 0 if null if (node.parent == undefined) { node.parent = 0; } // Store parent itemsDelta[node.impactitem_id] = { action : GLPIImpact.DELTA_ACTION_UPDATE, parent_id : node.parent, }; }); return itemsDelta; }, /** * Compute the delta betwteen the initial state and the current state * * @returns {Object} */ computeDelta: function () { // Store the delta for edges, compounds and parent var result = {}; // Get the current state of the graph var currentState = this.getCurrentState(); // Compute each deltas result.edges = this.computeEdgeDelta(currentState.edges); result.compounds = this.computeCompoundsDelta(currentState.compounds); result.items = this.computeItemsDelta(currentState.items); result.context = this.computeContext(currentState.items); return result; }, /** * Get the context menu items * * @returns {Array} */ getContextMenuItems: function(){ return [ { id : 'goTo', content : '<i class="fas fa-link"></i>' + __("Go to"), tooltipText : __("Open this element in a new tab"), selector : 'node[link]', onClickFunction: this.menuOnGoTo }, { id : 'showOngoing', content : '<i class="fas fa-list"></i>' + __("Show ongoing tickets"), tooltipText : __("Show ongoing tickets for this item"), selector : 'node[hasITILObjects=1]', onClickFunction: this.menuOnShowOngoing }, { id : 'editCompound', content : '<i class="fas fa-edit"></i>' + __("Group properties..."), tooltipText : __("Set name and/or color for this group"), selector : 'node:parent', onClickFunction: this.menuOnEditCompound, show : !this.readonly, }, { id : 'removeFromCompound', content : '<i class="fas fa-external-link-alt"></i>' + __("Remove from group"), tooltipText : __("Remove this asset from the group"), selector : 'node:child', onClickFunction: this.menuOnRemoveFromCompound, show : !this.readonly, }, { id : 'delete', content : '<i class="fas fa-trash"></i>' + __("Delete"), tooltipText : __("Delete element"), selector : 'node, edge', onClickFunction: this.menuOnDelete, show : !this.readonly, }, ]; }, addNode: function(itemID, itemType, position) { // Build a new graph from the selected node and insert it var node = { itemtype: itemType, items_id: itemID }; var nodeID = GLPIImpact.makeID(GLPIImpact.NODE, node.itemtype, node.items_id); // Check if the node is already on the graph if (GLPIImpact.cy.filter('node[id="' + nodeID + '"]').length > 0) { alert(__('This asset already exists.')); return; } // Build the new subgraph $.when(GLPIImpact.buildGraphFromNode(node)) .done( function (graph, params) { // Insert the new graph data into the current graph GLPIImpact.insertGraph(graph, params, { id: nodeID, x: position.x, y: position.y }); GLPIImpact.updateFlags(); } ).fail( function () { // Ajax failed alert(__("Unexpected error.")); } ); }, /** * Show the add node dialog */ showOngoingDialog: function(ITILObjects) { $(GLPIImpact.selectors.ongoingDialogBody).html( GLPIImpact.buildOngoingDialogContent(ITILObjects) ); $(GLPIImpact.selectors.ongoingDialog).modal('show'); }, /** * Set up event handlers for the edit compound dialog */ prepareEditCompoundDialog: function() { $(this.selectors.editCompoundDialogSave).on('click', function() { var compound = GLPIImpact.eventData.editCompound.target; // Save compound name compound.data( 'label', $(GLPIImpact.selectors.compoundName).val() ); // Save compound color compound.data( 'color', $(GLPIImpact.selectors.compoundColor).val() ); // Close dialog $(GLPIImpact.selectors.editCompoundDialog).modal('hide'); GLPIImpact.cy.trigger("change"); // Log for undo if (GLPIImpact.eventData.newCompound == null) { var previousLabel = GLPIImpact.eventData.editCompound.previousLabel; var previousColor = GLPIImpact.eventData.editCompound.previousColor; GLPIImpact.addToUndo(GLPIImpact.ACTION_EDIT_COMPOUND, { id : compound.data('id'), label : compound.data('label'), color : compound.data('color'), oldLabel: previousLabel, oldColor: previousColor, }); } else { var label = $(GLPIImpact.selectors.compoundName).val(); var color = $(GLPIImpact.selectors.compoundColor).val(); GLPIImpact.eventData.newCompound.data.label = label; GLPIImpact.eventData.newCompound.data.color = color; GLPIImpact.addToUndo( GLPIImpact.ACTION_ADD_COMPOUND, _.cloneDeep(GLPIImpact.eventData.newCompound) ); GLPIImpact.eventData.newCompound = null; } }); }, /** * Show the edit compound dialog * * @param {Object} compound label, color */ showEditCompoundDialog: function(compound) { var previousLabel = compound.data('label'); var previousColor = compound.data('color'); // Reset inputs $(GLPIImpact.selectors.compoundName).val(previousLabel); $(GLPIImpact.selectors.compoundColor).val(previousColor); // Set global event data this.eventData.editCompound = { target: compound, previousLabel: previousLabel, previousColor: previousColor, }; // Show modal $(GLPIImpact.selectors.editCompoundDialog).modal('show'); }, /** * Initialise variables * * @param {JQuery} impactContainer * @param {Object} colors properties: default, forward, backward, both * @param {string} startNode */ prepareNetwork: function( impactContainer, colors, startNode ) { // Set container this.impactContainer = impactContainer; // Init directionVisibility this.directionVisibility[GLPIImpact.FORWARD] = true; this.directionVisibility[GLPIImpact.BACKWARD] = true; // Set colors for edges this.defaultColors = colors; this.setEdgeColors(colors); // Set start node this.startNode = startNode; // Init dialogs actions handlers this.prepareEditCompoundDialog(); this.initToolbar(); }, /** * Build the network graph * * @param {string} data (json) */ buildNetwork: function(data, params, readonly) { var layout; // Init workspace status GLPIImpact.showDefaultWorkspaceStatus(); // Load params - phase1 (before cytoscape creation) if (params.impactcontexts_id !== undefined && params.impactcontexts_id !== 0) { // Apply custom colors if defined this.setEdgeColors({ forward : params.impact_color, backward: params.depends_color, both : params.impact_and_depends_color, }); // Apply max depth this.maxDepth = params.max_depth; // Preset layout based on node positions layout = this.getPresetLayout(JSON.parse(params.positions)); } else { // Default params if no context was found this.setEdgeColors(this.defaultColors); this.maxDepth = this.DEFAULT_DEPTH; // Procedural layout layout = this.getDagreLayout(); } // Init cytoscape this.cy = cytoscape({ container: this.impactContainer, elements : data, style : this.getNetworkStyle(), layout : layout, wheelSensitivity: 0.25, }); // If we used the preset layout, some nodes might lack positions this.generateMissingPositions(); this.cy.minZoom(0.5); // Store initial data this.initialState = this.getCurrentState(); // Enable editing if not readonly if (!readonly) { this.enableGraphEdition(); } // Highlight starting node this.cy.filter("node[start]").data({ highlight: 1, start_node: 1, }); // Enable context menu this.cy.contextMenus({ menuItems: this.getContextMenuItems(), menuItemClasses: [], contextMenuClasses: [] }); // Enable grid this.cy.gridGuide({ gridStackOrder: 0, snapToGridOnRelease: false, snapToGridDuringDrag: true, gridSpacing: 12, drawGrid: true, panGrid: true, gridColor: getComputedStyle(document.documentElement).getPropertyValue('--card-border-color'), }); // Disable box selection as we don't need it this.cy.boxSelectionEnabled(false); // Load params - phase 2 (after cytoscape creation) if (params.impactcontexts_id !== undefined && params.impactcontexts_id !== 0) { // Apply saved visibility if (!parseInt(params.show_depends)) { $(GLPIImpact.selectors.toggleImpact).prop("checked", false); } if (!parseInt(params.show_impact)) { $(GLPIImpact.selectors.toggleDepends).prop("checked", false); } this.updateFlags(); // Set viewport if (params.zoom != '0') { // If viewport params are set, apply them this.cy.viewport({ zoom: parseFloat(params.zoom), pan: { x: parseFloat(params.pan_x), y: parseFloat(params.pan_y), } }); // Check viewport is not empty or contains only one item var viewport = GLPIImpact.cy.extent(); var empty = true; GLPIImpact.cy.nodes().forEach(function(node) { if (node.position().x > viewport.x1 && node.position().x < viewport.x2 && node.position().y > viewport.x1 && node.position().y < viewport.x2 ){ empty = false; } }); if (empty || GLPIImpact.cy.filter("node:childless").length == 1) { this.cy.fit(); if (this.cy.zoom() > 2.3) { this.cy.zoom(2.3); this.cy.center(); } } } else { // Else fit the graph and reduce zoom if needed this.cy.fit(); if (this.cy.zoom() > 2.3) { this.cy.zoom(2.3); this.cy.center(); } } } else { // Default params if no context was found this.cy.fit(); if (this.cy.zoom() > 2.3) { this.cy.zoom(2.3); this.cy.center(); } } // Register events handlers for cytoscape object this.cy.on('mousedown', 'node', this.nodeOnMousedown); this.cy.on('mouseup', this.onMouseUp); this.cy.on('mousemove', this.onMousemove); this.cy.on('mouseover', this.onMouseover); this.cy.on('mouseout', this.onMouseout); this.cy.on('click', this.onClick); this.cy.on('click', 'edge', this.edgeOnClick); this.cy.on('click', 'node', this.nodeOnClick); this.cy.on('box', this.onBox); this.cy.on('drag add remove change', this.onChange); this.cy.on('doubleClick', this.onDoubleClick); this.cy.on('remove', this.onRemove); this.cy.on('grabon', this.onGrabOn); this.cy.on('freeon', this.onFreeOn); this.initCanvasOverlay(); // Global events $(document).keydown(this.onKeyDown); $(document).keyup(this.onKeyUp); // Enter EDITION_DEFAULT mode this.setEditionMode(GLPIImpact.EDITION_DEFAULT); // Init depth value var text = GLPIImpact.maxDepth; if (GLPIImpact.maxDepth >= GLPIImpact.MAX_DEPTH) { text = "infinity"; } $(GLPIImpact.selectors.maxDepthView).html(text); $(GLPIImpact.selectors.maxDepth).val(GLPIImpact.maxDepth); // Set color widgets default values $(GLPIImpact.selectors.dependsColor).val( GLPIImpact.edgeColors[GLPIImpact.BACKWARD] ); $(GLPIImpact.selectors.impactColor).val( GLPIImpact.edgeColors[GLPIImpact.FORWARD] ); $(GLPIImpact.selectors.impactAndDependsColor).val( GLPIImpact.edgeColors[GLPIImpact.BOTH] ); }, /** * Set readonly and show toolbar */ enableGraphEdition: function() { // Show toolbar $(this.selectors.save).show(); $(this.selectors.addNode).show(); $(this.selectors.addEdge).show(); $(this.selectors.addCompound).show(); $(this.selectors.deleteElement).show(); $(this.selectors.impactSettings).show(); $(this.selectors.sideToggle).show(); // Keep track of readonly so that events handler can update their behavior this.readonly = false; }, /** * Create ID for nodes and egdes * * @param {number} type (NODE or EDGE) * @param {string} a * @param {string} b * * @returns {string|null} */ makeID: function(type, a, b) { switch (type) { case GLPIImpact.NODE: return a + "::" + b; case GLPIImpact.EDGE: return a + "->" + b; } return null; }, /** * Helper to make an ID selector * We can't use the short syntax "#id" because our ids contains * non-alpha-numeric characters * * @param {string} id * * @returns {string} */ makeIDSelector: function(id) { return "[id='" + id + "']"; }, /** * Reload the graph style */ updateStyle: function() { this.cy.style(this.getNetworkStyle()); // If either the source of the target node of an edge is hidden, hide the // edge too by setting it's dept to the maximum value this.cy.edges().forEach(function(edge) { var source = GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(edge.data('source'))); var target = GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(edge.data('target'))); if (source.visible() && target.visible()) { edge.data('depth', 0); } else { edge.data('depth', Number.MAX_VALUE); } }); }, /** * Compute flags and depth for each nodes */ updateFlags: function() { /** * Assuming A is our starting node and B is a random node on the graph, * the depth of B is the shortest distance between AB and BA. */ // Init flag to GLPIImpact.DEFAULT for all elements of the graph this.cy.elements().forEach(function(ele) { ele.data('flag', GLPIImpact.DEFAULT); }); // First, calculate AB: Apply dijkstra on A and get distances for each // nodes var startNodeDijkstra = this.cy.elements().dijkstra( this.makeIDSelector(this.startNode), function() { return 1; }, // Same weight for each path true // Do not ignore edge directions ); this.cy.$("node:childless").forEach(function(node) { var distanceAB = startNodeDijkstra.distanceTo(node); node.data('depth', distanceAB); // Set node as part of the "Forward" graph if (distanceAB !== Infinity) { node.data('flag', node.data('flag') | GLPIImpact.FORWARD); } }); // Now, calculate BA: apply dijkstra on each nodes of the graph and // get the distance to A this.cy.$("node:childless").forEach(function(node) { // Skip A if (node.data('id') == GLPIImpact.startNode) { return; } var otherNodeDijkstra = GLPIImpact.cy.elements().dijkstra( node, function() { return 1; }, // Same weight for each path true // Do not ignore edge directions ); var distanceBA = otherNodeDijkstra.distanceTo( GLPIImpact.makeIDSelector(GLPIImpact.startNode) ); // If distance BA is shorter than distance AB, use it instead if (node.data('depth') > distanceBA) { node.data('depth', distanceBA); } // Set node as part of the "Backward" graph if (distanceBA !== Infinity) { node.data('flag', node.data('flag') | GLPIImpact.BACKWARD); } }); // Set start node to this.BOTH so it doen't impact the computation of it's neighbors GLPIImpact.cy.$(GLPIImpact.makeIDSelector(GLPIImpact.startNode)).data( 'flag', this.BOTH ); // Handle compounds nodes, their depth should be the lowest depth amongst // their children this.cy.filter("node:parent").forEach(function(compound) { var lowestDepth = Infinity; var flag = GLPIImpact.DEFAULT; compound.children().forEach(function(childNode) { var childNodeDepth = childNode.data('depth'); if (childNodeDepth < lowestDepth) { lowestDepth = childNodeDepth; } flag = flag | childNode.data('flag'); }); compound.data('depth', lowestDepth); compound.data('flag', flag); }); // Apply flag to edges so they can get the right colors this.cy.edges().forEach(function(edge) { var source = GLPIImpact.cy.$(GLPIImpact.makeIDSelector(edge.data('source'))); var target = GLPIImpact.cy.$(GLPIImpact.makeIDSelector(edge.data('target'))); edge.data('flag', source.data('flag') & target.data('flag')); }); // Set start node to this.DEFAULT when all calculation are down so he is // always shown GLPIImpact.cy.$(GLPIImpact.makeIDSelector(GLPIImpact.startNode)).data( 'flag', this.DEFAULT ); GLPIImpact.updateStyle(); }, /** * Toggle impact/depends visibility * * @param {*} toToggle */ toggleVisibility: function(toToggle) { // Update visibility setting GLPIImpact.directionVisibility[toToggle] = !GLPIImpact.directionVisibility[toToggle]; GLPIImpact.updateFlags(); GLPIImpact.cy.trigger("change"); }, /** * Set max depth of the graph * @param {Number} max max depth */ setDepth: function(max) { GLPIImpact.maxDepth = max; if (max >= GLPIImpact.MAX_DEPTH) { max = "infinity"; GLPIImpact.maxDepth = GLPIImpact.NO_DEPTH_LIMIT; } $(GLPIImpact.selectors.maxDepthView).html(max); GLPIImpact.updateStyle(); GLPIImpact.cy.trigger("change"); }, /** * Ask the backend to build a graph from a specific node * * @param {Object} node * @returns {Array|null} */ buildGraphFromNode: function(node) { node.action = "load"; var dfd = jQuery.Deferred(); // Request to backend $.ajax({ type: "GET", url: CFG_GLPI.root_doc + "/ajax/impact.php", dataType: "json", data: node, success: function(data) { dfd.resolve(JSON.parse(data.graph), JSON.parse(data.params)); }, error: function () { dfd.reject(); } }); return dfd.promise(); }, /** * Get distance between two point A and B * @param {Object} a x, y * @param {Object} b x, y * @returns {Number} */ getDistance: function(a, b) { return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); }, /** * Insert another new graph into the current one * * @param {Array} graph * @param {Object} params * @param {Object} startNode data, x, y */ insertGraph: function(graph, params, startNode) { var toAdd = []; var mainBoundingBox = this.cy.filter().boundingBox(); // Try to add the new graph nodes var i; for (i=0; i<graph.length; i++) { var id = graph[i].data.id; // Check that the element is not already on the graph, if (this.cy.filter('[id="' + id + '"]').length > 0) { continue; } // Store node to add them at once with a layout toAdd.push(graph[i]); // Remove node from side list if needed if (graph[i].group == "nodes" && graph[i].data.color === undefined ) { var node_info = graph[i].data.id.split(GLPIImpact.NODE_ID_SEPERATOR); var itemtype = node_info[0]; var items_id = node_info[1]; $("p[data-id=" + items_id + "][data-type='" + itemtype + "']").remove(); } } // Just place the node if only one result is found if (toAdd.length == 1) { toAdd[0].position = { x: startNode.x, y: startNode.y, }; this.cy.add(toAdd); this.addToUndo(this.ACTION_ADD_NODE, { toAdd: toAdd[0] }); return; } // Add nodes and apply layout var eles = this.cy.add(toAdd); var options; if (params.positions === undefined) { options = this.getDagreLayout(); } else { options = this.getPresetLayout(JSON.parse(params.positions)); } // Place the layout anywhere to compute it's bounding box var layout = eles.layout(options); layout.run(); this.generateMissingPositions(); // First, position the graph on the clicked areaa var newGraphBoundingBox = eles.boundingBox(); var center = { x: (newGraphBoundingBox.x1 + newGraphBoundingBox.x2) / 2, y: (newGraphBoundingBox.y1 + newGraphBoundingBox.y2) / 2, }; var centerToClickVector = [ startNode.x - center.x, startNode.y - center.y, ]; // Apply vector to each node eles.nodes().forEach(function(node) { if (!node.isParent()) { node.position({ x: node.position().x + centerToClickVector[0], y: node.position().y + centerToClickVector[1], }); } }); newGraphBoundingBox = eles.boundingBox(); // If the two bouding box overlap if (!(mainBoundingBox.x1 > newGraphBoundingBox.x2 || newGraphBoundingBox.x1 > mainBoundingBox.x2 || mainBoundingBox.y1 > newGraphBoundingBox.y2 || newGraphBoundingBox.y1 > mainBoundingBox.y2)) { // We want to find the point "intersect", which is the closest // intersection between the point at the center of the new bounding box // and the main bouding bouding box. // We then want to find the point "closest" which is the vertice of // the new bounding box which is the closest to the center of the // main bouding box // Then the vector betwteen "intersect" and "closest" can be applied // to the new graph to make it "slide" out of the main graph // Center of the new graph center = { x: Math.round((newGraphBoundingBox.x1 + newGraphBoundingBox.x2) / 2), y: Math.round((newGraphBoundingBox.y1 + newGraphBoundingBox.y2) / 2), }; var directions = [ [1, 0], [0, 1], [-1, 0], [0, -1], [1, 1], [-1, 1], [-1, -1], [1, -1] ]; var edges = [ { a: {x: Math.round(mainBoundingBox.x1), y: Math.round(mainBoundingBox.y1)}, b: {x: Math.round(mainBoundingBox.x2), y: Math.round(mainBoundingBox.y1)}, }, { a: {x: Math.round(mainBoundingBox.x2), y: Math.round(mainBoundingBox.y1)}, b: {x: Math.round(mainBoundingBox.x1), y: Math.round(mainBoundingBox.y2)}, }, { a: {x: Math.round(mainBoundingBox.x1), y: Math.round(mainBoundingBox.y2)}, b: {x: Math.round(mainBoundingBox.x2), y: Math.round(mainBoundingBox.y2)}, }, { a: {x: Math.round(mainBoundingBox.x2), y: Math.round(mainBoundingBox.y2)}, b: {x: Math.round(mainBoundingBox.x1), y: Math.round(mainBoundingBox.y1)}, } ]; i = 0; // Safegard, no more than X tries var intersect; while (i < 50000) { directions.forEach(function(vector) { if (intersect !== undefined) { return; } var point = { x: center.x + (vector[0] * i), y: center.y + (vector[1] * i), }; // Check if the point intersect with one of the edges edges.forEach(function(edge) { if (intersect !== undefined) { return; } if ((GLPIImpact.getDistance(point, edge.a) + GLPIImpact.getDistance(point, edge.b)) == GLPIImpact.getDistance(edge.a, edge.b)) { // Found intersection intersect = { x: point.x, y: point.y, }; } }); }); i++; if (intersect !== undefined) { break; } } if (intersect !== undefined) { // Center of the main graph center = { x: (mainBoundingBox.x1 + mainBoundingBox.x2) / 2, y: (mainBoundingBox.y1 + mainBoundingBox.y2) / 2, }; var vertices = [ {x: newGraphBoundingBox.x1, y: newGraphBoundingBox.y1}, {x: newGraphBoundingBox.x1, y: newGraphBoundingBox.y2}, {x: newGraphBoundingBox.x2, y: newGraphBoundingBox.y1}, {x: newGraphBoundingBox.x2, y: newGraphBoundingBox.y2}, ]; var closest; var min_dist; vertices.forEach(function(vertice) { var dist = GLPIImpact.getDistance(vertice, center); if (min_dist == undefined || dist < min_dist) { min_dist = dist; closest = vertice; } }); // Compute vector between closest and intersect var vector = [ intersect.x - closest.x, intersect.y - closest.y, ]; // Apply vector to each node eles.nodes().forEach(function(node) { if (!node.isParent()) { node.position({ x: node.position().x + vector[0], y: node.position().y + vector[1], }); } }); } } this.generateMissingPositions(); this.cy.animate({ center: { eles : GLPIImpact.cy.filter(""), }, }); this.cy.getElementById(startNode.id).data("highlight", 1); // Set undo/redo data var data = { edges: eles.edges().map(function(edge){ return edge.data(); }), compounds: [], nodes: [], }; eles.nodes().forEach(function(node) { if (node.isParent()) { data.compounds.push({ compoundData : _.clone(node.data()), compoundChildren: node.children().map(function(n) { return n.data('id'); }), }); } else { data.nodes.push({ nodeData : _.clone(node.data()), nodePosition: _.clone(node.position()), }); } }); this.addToUndo(this.ACTION_ADD_GRAPH, data); }, /** * Set the colors * * @param {object} colors default, backward, forward, both */ setEdgeColors: function (colors) { this.setColorIfExist(GLPIImpact.DEFAULT, colors.default); this.setColorIfExist(GLPIImpact.BACKWARD, colors.backward); this.setColorIfExist(GLPIImpact.FORWARD, colors.forward); this.setColorIfExist(GLPIImpact.BOTH, colors.both); }, /** * Set color if exist * * @param {object} colors default, backward, forward, both */ setColorIfExist: function (index, color) { if (color !== undefined) { this.edgeColors[index] = color; } }, /** * Exit current edition mode and enter a new one * * @param {number} mode */ setEditionMode: function (mode) { // Switching to a mode we are already in -> go to default if (this.editionMode == mode) { mode = GLPIImpact.EDITION_DEFAULT; } this.exitEditionMode(); this.enterEditionMode(mode); this.editionMode = mode; }, /** * Exit current edition mode */ exitEditionMode: function() { switch (this.editionMode) { case GLPIImpact.EDITION_DEFAULT: GLPIImpact.cy.nodes().ungrabify(); break; case GLPIImpact.EDITION_ADD_NODE: GLPIImpact.cy.nodes().ungrabify(); $(GLPIImpact.selectors.sideToggleIcon).addClass('fa-chevron-left'); $(GLPIImpact.selectors.sideToggleIcon).removeClass('fa-chevron-right'); $(GLPIImpact.selectors.side).removeClass('impact-side-expanded'); $(GLPIImpact.selectors.sidePanel).removeClass('impact-side-expanded'); $(GLPIImpact.selectors.addNode).removeClass("active"); break; case GLPIImpact.EDITION_ADD_EDGE: $(GLPIImpact.selectors.addEdge).removeClass("active"); // Empty event data and remove tmp node GLPIImpact.eventData.addEdgeStart = null; GLPIImpact.cy.filter("#tmp_node").remove(); break; case GLPIImpact.EDITION_DELETE: GLPIImpact.cy.filter().unselect(); GLPIImpact.cy.data('todelete', 0); $(GLPIImpact.selectors.deleteElement).removeClass("active"); break; case GLPIImpact.EDITION_ADD_COMPOUND: GLPIImpact.cy.panningEnabled(true); GLPIImpact.cy.boxSelectionEnabled(false); $(GLPIImpact.selectors.addCompound).removeClass("active"); break; case GLPIImpact.EDITION_SETTINGS: GLPIImpact.cy.nodes().ungrabify(); $(GLPIImpact.selectors.sideToggleIcon).addClass('fa-chevron-left'); $(GLPIImpact.selectors.sideToggleIcon).removeClass('fa-chevron-right'); $(GLPIImpact.selectors.side).removeClass('impact-side-expanded'); $(GLPIImpact.selectors.sidePanel).removeClass('impact-side-expanded'); $(GLPIImpact.selectors.impactSettings).removeClass("active"); break; } }, /** * Enter a new edition mode * * @param {number} mode */ enterEditionMode: function(mode) { switch (mode) { case GLPIImpact.EDITION_DEFAULT: GLPIImpact.clearHelpText(); GLPIImpact.cy.nodes().grabify(); $(GLPIImpact.impactContainer).css('cursor', "move"); break; case GLPIImpact.EDITION_ADD_NODE: GLPIImpact.cy.nodes().grabify(); GLPIImpact.clearHelpText(); $(GLPIImpact.selectors.sideToggleIcon).removeClass('fa-chevron-left'); $(GLPIImpact.selectors.sideToggleIcon).addClass('fa-chevron-right'); $(GLPIImpact.selectors.side).addClass('impact-side-expanded'); $(GLPIImpact.selectors.sidePanel).addClass('impact-side-expanded'); $(GLPIImpact.selectors.addNode).addClass("active"); $(GLPIImpact.impactContainer).css('cursor', "move"); $(GLPIImpact.selectors.sideSettings).hide(); $(GLPIImpact.selectors.sideAddNode).show(); break; case GLPIImpact.EDITION_ADD_EDGE: GLPIImpact.showHelpText(__("Draw a line between two assets to add an impact relation")); $(GLPIImpact.selectors.addEdge).addClass("active"); $(GLPIImpact.impactContainer).css('cursor', "crosshair"); break; case GLPIImpact.EDITION_DELETE: GLPIImpact.cy.filter().unselect(); GLPIImpact.showHelpText(__("Click on an element to remove it from the network")); $(GLPIImpact.selectors.deleteElement).addClass("active"); $(GLPIImpact.impactContainer).css('cursor', "move"); break; case GLPIImpact.EDITION_ADD_COMPOUND: GLPIImpact.cy.panningEnabled(false); GLPIImpact.cy.boxSelectionEnabled(true); GLPIImpact.showHelpText(__("Draw a square containing the assets you wish to group")); $(GLPIImpact.selectors.addCompound).addClass("active"); $(GLPIImpact.impactContainer).css('cursor', "crosshair"); break; case GLPIImpact.EDITION_SETTINGS: GLPIImpact.cy.nodes().grabify(); $(GLPIImpact.selectors.sideToggleIcon).removeClass('fa-chevron-left'); $(GLPIImpact.selectors.sideToggleIcon).addClass('fa-chevron-right'); $(GLPIImpact.selectors.side).addClass('impact-side-expanded'); $(GLPIImpact.selectors.sidePanel).addClass('impact-side-expanded'); $(GLPIImpact.selectors.impactSettings).addClass("active"); $(GLPIImpact.selectors.sideAddNode).hide(); $(GLPIImpact.selectors.sideSettings).show(); break; } }, /** * Hide the toolbar and show an help text * * @param {string} text */ showHelpText: function(text) { $(GLPIImpact.selectors.helpText).html(text).show(); }, /** * Hide the help text and show the toolbar */ clearHelpText: function() { $(GLPIImpact.selectors.helpText).hide(); }, /** * Export the graph in the given format * * @param {string} format * @param {boolean} transparentBackground (png only) * * @returns {Object} filename, filecontent */ download: function(format, transparentBackground) { var filename; var filecontent; // Create fake link GLPIImpact.impactContainer.append("<a id='impact_download'></a>"); var link = $('#impact_download'); switch (format) { case 'png': filename = "impact.png"; filecontent = this.cy.png({ bg: transparentBackground ? "transparent" : "white" }); break; case 'jpeg': filename = "impact.jpeg"; filecontent = this.cy.jpg(); break; } // Trigger download and remore the link link.prop('download', filename); link.prop("href", filecontent); link[0].click(); link.remove(); }, /** * Get node at target position * * @param {Object} position x, y * @param {function} filter if false return null */ getNodeAt: function(position, filter) { var nodes = this.cy.nodes(); for (var i=0; i<nodes.length; i++) { if (nodes[i].boundingBox().x1 < position.x && nodes[i].boundingBox().x2 > 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'); }, /** * Enable the save button */ showDirtyWorkspaceStatus: function() { $(GLPIImpact.selectors.save).removeClass('clean'); $(GLPIImpact.selectors.save).addClass('dirty'); }, /** * 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 += "<h3>" + title + "</h3>"; html += "<ul>"; elements.forEach(function(element) { var link = CFG_GLPI.root_doc + "/front/" + url + ".form.php?id=" + element.id; html += '<li><a target="_blank" href="' + link + '">' + element.name + '</a></li>'; }); html += "</ul>"; } 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.showEditCompoundDialog(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; // 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.showEditCompoundDialog(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.showOngoingDialog(event.target.data('ITILObjects')); }, /** * Handle "EditCompound" menu event * * @param {JQuery.Event} event */ menuOnEditCompound: function (event) { GLPIImpact.showEditCompoundDialog(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 = '<p class="' + cssClass + '" data-id="' + value['id'] + '" data-type="' + itemtype + '">'; str += '<img src="' + $(GLPIImpact.selectors.sideSearch + " img").attr('src') + '"></img>'; str += value["name"]; if (isHidden) { str += '<i class="fas fa-eye-slash impact-res-hidden"></i>'; } str += "</p>"; $(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);