Skip to content
Snippets Groups Projects
Commit d32f159c authored by Maximilian Giller's avatar Maximilian Giller :squid:
Browse files

Merge branch 'feature-box-delete'

parents 7f444182 d010c325
No related branches found
No related tags found
No related merge requests found
Pipeline #52785 passed
......@@ -62,3 +62,17 @@ div#ks-editor img.preview-image {
div#ks-editor #toolbar-settings, div#ks-editor #toolbar-save {
float: right;
}
div#ks-editor #box-select-layer {
position: relative;
}
div#ks-editor #box-select {
position: absolute;
z-index: 300000;
border-style: dotted;
border-color: #3e74cc;
border-width: 2px;
background-color: rgba(255, 255, 255, 0.5);
pointer-events: none;
}
<div id="ks-editor">
<!--The id "ks-editor" indicates, that the javascript associated with this should automatically be executed-->
<h1>Interface</h1>
<div id="2d-graph"></div>
<div id="box-select-layer"><div id="2d-graph"></div></div>
<section id="toolbar"></section>
<section id="tool-menu">
<div id="delete-menu" class="hidden">
<p>Drag and drop while pressing SHIFT to delete all the nodes that are being selected.</p>
</div>
<div id="collect-menu" class="hidden">
<h3>Collected items</h3>
<button id="clear-collection">Clear</button>
......
......@@ -7,7 +7,7 @@ import { setSpace, SPACE } from "../../config";
export var state = undefined;
export var graph = undefined;
var graphObj;
export var renderer;
window.onload = function () {
// Only execute, if corresponding dom is present
......@@ -40,7 +40,7 @@ export function loadSpace(spaceId) {
function extractPositions(event) {
return {
graph: graphObj.screen2GraphCoords(event.layerX, event.layerY),
graph: renderer.screen2GraphCoords(event.layerX, event.layerY),
window: { x: event.clientX, y: event.clientY },
};
}
......@@ -49,7 +49,7 @@ function load() {
const graphContainer = document.getElementById("2d-graph");
const width = graphContainer.offsetWidth;
graphObj = ForceGraph()(graphContainer)
renderer = ForceGraph()(graphContainer)
.height(600)
.width(width)
.graphData(graph.data)
......@@ -78,6 +78,6 @@ function load() {
.onLinkClick((link) => state.onLinkClick(link));
graph.onChangeCallbacks.push((data) => {
graphObj.graphData(data);
renderer.graphData(data);
});
}
......@@ -105,7 +105,7 @@ export class Graph extends ManagedData {
var classIndex = this.nodeTypes.indexOf(typeClass);
if (classIndex <= -1) {
return 'black';
return "black";
}
return COLOR_PALETTE[classIndex % COLOR_PALETTE.length];
......@@ -127,6 +127,25 @@ export class Graph extends ManagedData {
this.storeCurrentData("Deleted node with id [" + nodeId + "]");
}
deleteNodes(nodeIds) {
if (nodeIds === undefined || nodeIds.length <= 0) {
return;
}
try {
this.disableStoring();
nodeIds.forEach((id) => {
this.deleteNode(id);
});
} finally {
// Gotta make sure that storing is turned back on again
this.enableStoring();
}
this.storeCurrentData("Deleted nodes with ids [" + nodeIds.join(",") + "]");
}
stopPhysics() {
this.data[GRAPH_NODES].forEach((n) => {
n.fx = n.x;
......
......@@ -8,6 +8,7 @@ export default class ManagedData {
this.history = []; // Newest state is always at 0
this.historyPosition = 0;
this.savedHistoryId = 0;
this.storingEnabled = true;
this.storeCurrentData("Initial state", false);
}
......@@ -39,6 +40,14 @@ export default class ManagedData {
this.updateUnsavedChangesHandler();
}
disableStoring() {
this.storingEnabled = false;
}
enableStoring() {
this.storingEnabled = true;
}
onUndo() {}
onRedo() {}
......@@ -83,6 +92,10 @@ export default class ManagedData {
}
storeCurrentData(description, relevantChanges = true) {
if (this.storingEnabled === false) {
return;
}
var formattedData = this.storableData(this.data);
var nextId = 0;
......
......@@ -27,7 +27,7 @@ export const CONTEXT = {
node: "node",
link: "link",
mixed: "mixed",
nothing: "nothing"
nothing: "nothing",
};
export class State extends Tool {
......@@ -74,6 +74,12 @@ export class State extends Tool {
this.selectedItems.add(item);
}
addSelectedItems(items) {
Object.values(items).forEach((item) => {
this.selectedItems.add(item);
});
}
removeSelectedItem(item) {
this.selectedItems.delete(item);
}
......@@ -104,11 +110,11 @@ export class State extends Tool {
}
linkColor(link) {
return 'red';
return "red";
}
nodeColor(node) {
return 'black';
return "black";
}
onKeyDown(key) {
......@@ -157,7 +163,7 @@ export class State extends Tool {
// Draw image
if (node[Graph.NODE_IMAGE] !== undefined) {
var path = node[Graph.NODE_IMAGE];
if (!path.includes("/")) {
path = Graph.IMAGE_SRC + path;
}
......@@ -176,18 +182,24 @@ export class State extends Tool {
// Draw label
if (this.labelVisible) {
const label = node[Graph.NODE_LABEL];
const fontSize = 11/globalScale;
const fontSize = 11 / globalScale;
ctx.font = `${fontSize}px Sans-Serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
const bckgDimensions = [textWidth, fontSize].map(
(n) => n + fontSize * 0.2
); // some padding
const nodeHeightOffset = Graph.IMAGE_SIZE / 3 + bckgDimensions[1];
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + nodeHeightOffset, ...bckgDimensions);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'white';
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.fillRect(
node.x - bckgDimensions[0] / 2,
node.y - bckgDimensions[1] / 2 + nodeHeightOffset,
...bckgDimensions
);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
ctx.fillText(label, node.x, node.y + nodeHeightOffset);
}
......@@ -243,7 +255,12 @@ export class State extends Tool {
}
// Draw gradient link
var gradient = ctx.createLinearGradient(link.source.x, link.source.y, link.target.x, link.target.y);
var gradient = ctx.createLinearGradient(
link.source.x,
link.source.y,
link.target.x,
link.target.y
);
// Have reversed colors
// Color at source node referencing the target node and vice versa
gradient.addColorStop("0", graph.getNodeColor(link.target));
......@@ -260,7 +277,7 @@ export class State extends Tool {
// if (link === lastLink) {
// ctx.stroke();
// }
return undefined;
}
......
import Tool from "./tool";
import { graph, state } from "../editor";
import { graph, state, renderer } from "../editor";
import * as Graph from "../graph";
import jquery from "jquery";
import ToolMenu from "./menus/toolmenu";
const BOX_SELECT_LAYER_ID = "#box-select-layer";
const BOX_SELECT_ID_WH = "box-select";
const SELECT_BOX_SELECTOR = BOX_SELECT_LAYER_ID + " #" + BOX_SELECT_ID_WH;
/**
* Only one instance of this should exist, since box-delete has to work on a global scale.
*/
var deleteToolInstance = undefined; // Used for box delete
export default class DeleteTool extends Tool {
constructor(key) {
super("Delete", "delete", key);
super("Delete", "delete", key, new ToolMenu());
this.setupBoxSelect();
this.isActive = false;
if (deleteToolInstance === undefined) {
deleteToolInstance = this;
}
}
onBoxSelect(left, bottom, top, right) {
// Filter out selected nodes
const selectedNodes = [];
const tl = renderer.screen2GraphCoords(left, top);
const br = renderer.screen2GraphCoords(right, bottom);
graph.data[Graph.GRAPH_NODES].forEach((node) => {
if (
tl.x < node.x &&
node.x < br.x &&
br.y > node.y &&
node.y > tl.y
) {
selectedNodes.push(node);
}
});
// Was anything even selected?
if (selectedNodes.length <= 0) {
return;
}
// Ask for confirmation to delete
var nodeNames = selectedNodes.map((n) => n[Graph.NODE_LABEL]);
//! Problem: If browser is not actually showing the alerts, it always returns false!
var shouldDelete = confirm(
"Do you wanna delete all these nodes:\n\n" + nodeNames.join("\n")
);
// Delete if confirmed
if (shouldDelete) {
var nodeIds = selectedNodes.map((n) => n[Graph.NODE_ID]);
graph.deleteNodes(nodeIds);
}
}
onNodeClick(node) {
graph.deleteNode(node[Graph.NODE_ID]);
if (state.selectedItem == node) {
state.setSelectedItem(undefined);
}
......@@ -20,9 +71,107 @@ export default class DeleteTool extends Tool {
link[Graph.LINK_SOURCE][Graph.NODE_ID],
link[Graph.LINK_TARGET][Graph.NODE_ID]
);
if (state.selectedItem == link) {
state.setSelectedItem(undefined);
}
}
setupBoxSelect() {
window.addEventListener("load", () => {
// Source: https://github.com/vasturiano/force-graph/issues/151#issuecomment-735850938
// forceGraph element is the element provided to the Force Graph Library
jquery("#2d-graph").on("pointerdown", this.boxSelectOnPointerDown);
jquery("#2d-graph").on("pointermove", this.boxSelectOnPointerMove);
jquery("#2d-graph").on("pointerup", this.boxSelectOnPointerUp);
});
}
boxSelectOnPointerDown(e) {
// Only do anything if delete tool is also active
if (
deleteToolInstance === undefined ||
deleteToolInstance.isActive == false
) {
return;
}
if (!e.shiftKey) {
return;
}
e.preventDefault();
this.boxSelect = document.createElement("div");
this.boxSelect.id = BOX_SELECT_ID_WH;
this.boxSelect.style.left = e.offsetX.toString() + "px";
this.boxSelect.style.top = e.offsetY.toString() + "px";
this.boxSelectStart = {
x: e.offsetX,
y: e.offsetY,
};
// app element is the element just above the forceGraph element.
jquery(BOX_SELECT_LAYER_ID).append(this.boxSelect);
}
boxSelectOnPointerMove(e) {
if (!this.boxSelect) {
return;
}
if (!e.shiftKey) {
jquery(SELECT_BOX_SELECTOR).remove();
return;
}
e.preventDefault();
if (e.offsetX < this.boxSelectStart.x) {
this.boxSelect.style.left = e.offsetX.toString() + "px";
this.boxSelect.style.width =
(this.boxSelectStart.x - e.offsetX).toString() + "px";
} else {
this.boxSelect.style.left = this.boxSelectStart.x.toString() + "px";
this.boxSelect.style.width =
(e.offsetX - this.boxSelectStart.x).toString() + "px";
}
if (e.offsetY < this.boxSelectStart.y) {
this.boxSelect.style.top = e.offsetY.toString() + "px";
this.boxSelect.style.height =
(this.boxSelectStart.y - e.offsetY).toString() + "px";
} else {
this.boxSelect.style.top = this.boxSelectStart.y.toString() + "px";
this.boxSelect.style.height =
(e.offsetY - this.boxSelectStart.y).toString() + "px";
}
}
boxSelectOnPointerUp(e) {
if (!this.boxSelect) {
return;
}
if (!e.shiftKey) {
jquery(SELECT_BOX_SELECTOR).remove();
return;
}
e.preventDefault();
let left, bottom, top, right;
if (e.offsetX < this.boxSelectStart.x) {
left = e.offsetX;
right = this.boxSelectStart.x;
} else {
left = this.boxSelectStart.x;
right = e.offsetX;
}
if (e.offsetY < this.boxSelectStart.y) {
top = e.offsetY;
bottom = this.boxSelectStart.y;
} else {
top = this.boxSelectStart.y;
bottom = e.offsetY;
}
this.boxSelect.remove();
deleteToolInstance.onBoxSelect(left, bottom, top, right);
}
}
......@@ -6,6 +6,7 @@ export default class Tool {
this.warnings = false;
this.menu = menu;
this.toggleBehaviour = toggleBehaviour;
this.isActive = false;
if (this.menu !== undefined) {
this.menu.loadTool(this);
......@@ -31,6 +32,7 @@ export default class Tool {
}
activateTool() {
this.isActive = true;
if (this.menu !== undefined) {
this.menu.show();
}
......@@ -39,6 +41,7 @@ export default class Tool {
}
deactivateTool(nextTool) {
this.isActive = false;
this.onToolDeactivate(nextTool);
if (this.menu !== undefined) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment