editor: group selection and group dragging
This commit is contained in:
parent
6a6c13a5d8
commit
b0e00beaac
3 changed files with 153 additions and 41 deletions
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue