editor: group selection and group dragging

This commit is contained in:
Vašek Šraier 2020-10-03 23:41:57 +02:00
parent 6a6c13a5d8
commit b0e00beaac
3 changed files with 153 additions and 41 deletions

View file

@ -246,6 +246,7 @@
<Graph
{tasks}
selectionToolEnabled={true}
on:selectTask={clickTask}
on:preSelectTask={startHovering}
bind:this={graph}
@ -256,7 +257,7 @@
<div class="container">
<div class="topLeftHint">
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 class="right">

View file

@ -9,7 +9,9 @@
export let tasks: TasksFile;
export let nodeDraggingEnabled: boolean = false;
export let selectionToolEnabled: boolean = false;
export let showHiddenEdges: boolean = false;
export let selection: Set<TaskDescriptor> = new Set();
let hoveredTask: null | string = null;
@ -18,6 +20,8 @@
let clientHeight: number;
let clientWidth: number;
let svgElement: SVGElement;
let innerSvgGroup: SVGElement;
let selectionRectangle: [[number, number], [number, number]] | null = null;
$: nodes = tasks.tasks;
$: edges = createEdges(nodes);
@ -25,6 +29,11 @@
const eventDispatcher = createEventDispatcher();
function nodeClick(task: TaskDescriptor) {
function eventHandler(e: CustomEvent<MouseEvent>) {
if (selectionToolEnabled) {
selection.clear();
selection.add(task);
selection = selection;
}
eventDispatcher("selectTask", task);
}
return eventHandler;
@ -61,6 +70,110 @@
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(() => {
setupZoom();
});
@ -85,26 +198,49 @@
:global(#page) {
flex-grow: 1;
}
rect {
fill: transparent;
stroke-dasharray: 5, 5;
stroke: gainsboro;
}
</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}">
<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}
<GraphEdge {edge} showLabelEdge={showHiddenEdges} />
{/each}
{#each nodes as task}
<GraphNode
{task}
on:mousedown={dragStart}
selected={selection.has(task)}
on:taskClick
on:click={nodeClick(task)}
on:hoveringChange={nodeHover(task)}
on:positionChange={() => {
tasks = tasks;
}}
status={$taskStatuses.get(task.id)}
draggingEnabled={nodeDraggingEnabled}
on:dblclick={nodeDoubleClick(task)} />
{/each}
</g>

View file

@ -6,7 +6,7 @@
import type { TaskDescriptor } from "./tasks";
export let task: TaskDescriptor;
export let draggingEnabled: boolean = false;
export let selected: bool = false;
export let status: TaskStatus | undefined = undefined;
let hovering: boolean = false;
@ -47,36 +47,6 @@
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) {
eventDispatcher("dblclick", e);
e.stopPropagation();
@ -112,18 +82,23 @@
.solved .taskNode {
fill: green; /* TODO */
}
.selected > ellipse {
stroke-width: 4px;
stroke: red;
}
</style>
<g
bind:this={mainGroup}
on:mousedown
on:mouseenter={enter}
on:mouseleave={leave}
on:click={click}
on:mousedown={dragStart}
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 draggingEnabled }
{#if selected }
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
{/if}
<text