Browse Source

editor: group selection and group dragging

mj-deploy
Vašek Šraier 4 years ago
parent
commit
b0e00beaac
  1. 3
      frontend/src/Editor.svelte
  2. 148
      frontend/src/Graph.svelte
  3. 43
      frontend/src/GraphNode.svelte

3
frontend/src/Editor.svelte

@ -246,6 +246,7 @@
<Graph <Graph
{tasks} {tasks}
selectionToolEnabled={true}
on:selectTask={clickTask} on:selectTask={clickTask}
on:preSelectTask={startHovering} on:preSelectTask={startHovering}
bind:this={graph} bind:this={graph}
@ -256,7 +257,7 @@
<div class="container"> <div class="container">
<div class="topLeftHint"> <div class="topLeftHint">
Last clicked: <b>{clicked.join(' | ')}</b><br /><i>Double click na node Last clicked: <b>{clicked.join(' | ')}</b><br /><i>Double click na node
otevře detail. Po kliknutí na label se zobrazí možnost rotace.</i> otevře detail. Po kliknutí na label se zobrazí možnost rotace. Držením pravého tlačítka je možné udělat skupinový výběr.</i>
</div> </div>
<div class="right"> <div class="right">

148
frontend/src/Graph.svelte

@ -9,7 +9,9 @@
export let tasks: TasksFile; export let tasks: TasksFile;
export let nodeDraggingEnabled: boolean = false; export let nodeDraggingEnabled: boolean = false;
export let selectionToolEnabled: boolean = false;
export let showHiddenEdges: boolean = false; export let showHiddenEdges: boolean = false;
export let selection: Set<TaskDescriptor> = new Set();
let hoveredTask: null | string = null; let hoveredTask: null | string = null;
@ -18,6 +20,8 @@
let clientHeight: number; let clientHeight: number;
let clientWidth: number; let clientWidth: number;
let svgElement: SVGElement; let svgElement: SVGElement;
let innerSvgGroup: SVGElement;
let selectionRectangle: [[number, number], [number, number]] | null = null;
$: nodes = tasks.tasks; $: nodes = tasks.tasks;
$: edges = createEdges(nodes); $: edges = createEdges(nodes);
@ -25,6 +29,11 @@
const eventDispatcher = createEventDispatcher(); const eventDispatcher = createEventDispatcher();
function nodeClick(task: TaskDescriptor) { function nodeClick(task: TaskDescriptor) {
function eventHandler(e: CustomEvent<MouseEvent>) { function eventHandler(e: CustomEvent<MouseEvent>) {
if (selectionToolEnabled) {
selection.clear();
selection.add(task);
selection = selection;
}
eventDispatcher("selectTask", task); eventDispatcher("selectTask", task);
} }
return eventHandler; return eventHandler;
@ -61,6 +70,110 @@
d3.select(container).call(zoomer); d3.select(container).call(zoomer);
} }
function groupSelectionHandler(e: MouseEvent) {
// not enabled?
if (!selectionToolEnabled) return;
// not a right button?
if (e.button != 2) return;
// setup drag start
const startPos = d3.pointer(e, innerSvgGroup);
// prevent default
e.preventDefault();
e.stopPropagation();
function updateSelection() {
selection.clear();
tasks.tasks.forEach((t) => {
if (
selectionRectangle![0][0] < (t.position ?? [0, 0])[0] &&
(t.position ?? [0, 0])[0] < selectionRectangle![1][0] &&
selectionRectangle![0][1] < (t.position ?? [0, 0])[1] &&
(t.position ?? [0, 0])[1] < selectionRectangle![1][1]
)
selection.add(t);
});
selection = selection;
}
// setup mouse move
function mouseMove(e: MouseEvent) {
const np = d3.pointer(e, innerSvgGroup);
selectionRectangle = [
[Math.min(np[0], startPos[0]), Math.min(np[1], startPos[1])],
[Math.max(np[0], startPos[0]), Math.max(np[1], startPos[1])],
];
updateSelection();
}
window.addEventListener("mousemove", mouseMove);
window.addEventListener("mouseup", mouseUp, { once: true });
// setup mouse down
function mouseUp(e: MouseEvent) {
// not a right button?
if (e.button != 2) return;
e.preventDefault();
e.stopPropagation();
// save selection
const np = d3.pointer(e, innerSvgGroup);
selectionRectangle = [
[Math.min(np[0], startPos[0]), Math.min(np[1], startPos[1])],
[Math.max(np[0], startPos[0]), Math.max(np[1], startPos[1])],
];
updateSelection();
selectionRectangle = null;
// cleanup listeners
window.removeEventListener("mousemove", mouseMove);
}
}
// dragging
function dragStart(e: MouseEvent) {
if (!nodeDraggingEnabled) return;
// is the left button pressed?
if (e.button != 0) return;
e.preventDefault();
e.stopPropagation();
let startPos = d3.pointer(e, innerSvgGroup);
function drag(e: MouseEvent) {
if (!nodeDraggingEnabled) return;
const currPos = d3.pointer(e, innerSvgGroup);
const [dx, dy] = [currPos[0] - startPos[0], currPos[1] - startPos[1]];
for (const [t, _] of selection.entries()) {
t.position = [
(t.position ?? [0, 0])[0] + dx,
(t.position ?? [0, 0])[1] + dy,
];
}
tasks = tasks;
startPos = currPos;
e.preventDefault();
e.stopPropagation();
}
function dragStop(e: MouseEvent) {
if (!nodeDraggingEnabled) return;
e.preventDefault();
e.stopPropagation();
window.removeEventListener("mousemove", drag);
}
window.addEventListener("mousemove", drag);
window.addEventListener("mouseup", dragStop, { once: true });
}
onMount(() => { onMount(() => {
setupZoom(); setupZoom();
}); });
@ -85,26 +198,49 @@
:global(#page) { :global(#page) {
flex-grow: 1; flex-grow: 1;
} }
rect {
fill: transparent;
stroke-dasharray: 5, 5;
stroke: gainsboro;
}
</style> </style>
<div bind:this={container} bind:clientHeight bind:clientWidth> <div
bind:this={container}
bind:clientHeight
bind:clientWidth
on:mousedown={groupSelectionHandler}
on:contextmenu={(e) => {
if (selectionToolEnabled) {
e.preventDefault();
return false;
}
}}>
<svg bind:this={svgElement} viewBox="{0},{0},{clientWidth},{clientHeight}"> <svg bind:this={svgElement} viewBox="{0},{0},{clientWidth},{clientHeight}">
<g> <g>
<g transform="translate({clientWidth / 2}, {clientHeight / 2})"> <g
bind:this={innerSvgGroup}
transform="translate({clientWidth / 2}, {clientHeight / 2})">
{#if selectionRectangle != null}
<rect
x={selectionRectangle[0][0]}
y={selectionRectangle[0][1]}
width={selectionRectangle[1][0] - selectionRectangle[0][0]}
height={selectionRectangle[1][1] - selectionRectangle[0][1]} />
{/if}
{#each edges as edge} {#each edges as edge}
<GraphEdge {edge} showLabelEdge={showHiddenEdges} /> <GraphEdge {edge} showLabelEdge={showHiddenEdges} />
{/each} {/each}
{#each nodes as task} {#each nodes as task}
<GraphNode <GraphNode
{task} {task}
on:mousedown={dragStart}
selected={selection.has(task)}
on:taskClick on:taskClick
on:click={nodeClick(task)} on:click={nodeClick(task)}
on:hoveringChange={nodeHover(task)} on:hoveringChange={nodeHover(task)}
on:positionChange={() => {
tasks = tasks;
}}
status={$taskStatuses.get(task.id)} status={$taskStatuses.get(task.id)}
draggingEnabled={nodeDraggingEnabled}
on:dblclick={nodeDoubleClick(task)} /> on:dblclick={nodeDoubleClick(task)} />
{/each} {/each}
</g> </g>

43
frontend/src/GraphNode.svelte

@ -6,7 +6,7 @@
import type { TaskDescriptor } from "./tasks"; import type { TaskDescriptor } from "./tasks";
export let task: TaskDescriptor; export let task: TaskDescriptor;
export let draggingEnabled: boolean = false; export let selected: bool = false;
export let status: TaskStatus | undefined = undefined; export let status: TaskStatus | undefined = undefined;
let hovering: boolean = false; let hovering: boolean = false;
@ -47,36 +47,6 @@
ensureTextFits(); ensureTextFits();
} }
// dragging
let dragging: boolean = false;
function dragStart(e: MouseEvent) {
if (!draggingEnabled) return;
dragging = true;
e.preventDefault();
e.stopPropagation();
window.addEventListener("mousemove", drag)
window.addEventListener("mouseup", dragStop, { once: true })
}
function drag(e: MouseEvent) {
if (!draggingEnabled) return;
if (!dragging) return;
task.position = d3.pointer(e, mainGroup);
eventDispatcher("positionChange");
e.preventDefault();
e.stopPropagation();
}
function dragStop(e: MouseEvent) {
if (!draggingEnabled) return;
dragging = false;
e.preventDefault();
e.stopPropagation();
window.removeEventListener("mousemove", drag)
}
function dblclick(e: MouseEvent) { function dblclick(e: MouseEvent) {
eventDispatcher("dblclick", e); eventDispatcher("dblclick", e);
e.stopPropagation(); e.stopPropagation();
@ -112,18 +82,23 @@
.solved .taskNode { .solved .taskNode {
fill: green; /* TODO */ fill: green; /* TODO */
} }
.selected > ellipse {
stroke-width: 4px;
stroke: red;
}
</style> </style>
<g <g
bind:this={mainGroup} bind:this={mainGroup}
on:mousedown
on:mouseenter={enter} on:mouseenter={enter}
on:mouseleave={leave} on:mouseleave={leave}
on:click={click} on:click={click}
on:mousedown={dragStart}
on:dblclick={dblclick} on:dblclick={dblclick}
class="{status == null ? '' : status.solved ? 'solved' : status.submitted ? 'submitted' : ''} {task.type}"> class="{status == null ? '' : status.solved ? 'solved' : status.submitted ? 'submitted' : ''} {task.type} {selected ? 'selected' : 'notSelected'}">
{#if task.type == 'label'} {#if task.type == 'label'}
{#if draggingEnabled } {#if selected }
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} /> <ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
{/if} {/if}
<text <text