editor: first version
This commit is contained in:
parent
040d73492a
commit
2006216a99
6 changed files with 219 additions and 46 deletions
|
@ -13,7 +13,7 @@
|
||||||
<script type="module" src='/build/bundle.js'></script>
|
<script type="module" src='/build/bundle.js'></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body style="padding: 0">
|
||||||
<div id="svelte-root"></div>
|
<div id="svelte-root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Graph from "./Graph.svelte";
|
import Graph from "./Graph.svelte";
|
||||||
import GraphNode from "./GraphNode.svelte";
|
|
||||||
import { loadTasks } from "./task-loader";
|
import { loadTasks } from "./task-loader";
|
||||||
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
||||||
import TasksLoader from "./TasksLoader.svelte";
|
import TasksLoader from "./TasksLoader.svelte";
|
||||||
import TaskPanel from "./TaskPanel.svelte";
|
import TaskPanel from "./TaskPanel.svelte";
|
||||||
|
import Editor from "./Editor.svelte";
|
||||||
|
|
||||||
const tasksPromise: Promise<TasksFile> = loadTasks();
|
const tasksPromise: Promise<TasksFile> = loadTasks();
|
||||||
|
|
||||||
|
@ -15,12 +15,13 @@
|
||||||
finalSelect = true
|
finalSelect = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hash = window.location.hash.substr(1);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1em;
|
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
@ -40,13 +41,14 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!--
|
{#if hash == "editor"}
|
||||||
<svg height=200 width=200>
|
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||||
<GraphNode task="null" />
|
<Editor tasks={t} />
|
||||||
</svg>
|
</TasksLoader>
|
||||||
-->
|
{:else}
|
||||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||||
<TaskPanel bind:finalSelect={finalSelect} selectedTask={selectedTask} />
|
<TaskPanel bind:finalSelect={finalSelect} selectedTask={selectedTask} />
|
||||||
<Graph tasks={t} bind:selectedTask={selectedTask} on:selectTask={clickTask} />
|
<Graph tasks={t} bind:selectedTask={selectedTask} on:selectTask={clickTask} />
|
||||||
</TasksLoader>
|
</TasksLoader>
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
143
frontend/src/Editor.svelte
Normal file
143
frontend/src/Editor.svelte
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<script type="ts">
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import { grabAssignment } from "./ksp-task-grabber";
|
||||||
|
import type { TaskDescriptor, TasksFile } from "./task-loader";
|
||||||
|
import { createTaskMap, getCategories } from "./task-loader";
|
||||||
|
|
||||||
|
export let tasks: TasksFile;
|
||||||
|
|
||||||
|
let selectedTask: string | null = null;
|
||||||
|
let clickedTask: string | null = null;
|
||||||
|
|
||||||
|
let clicked: string[] = [];
|
||||||
|
|
||||||
|
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
||||||
|
// ukladani seznamu poslednich kliknuti
|
||||||
|
clicked.push(selectedTask);
|
||||||
|
if (clicked.length > 3)
|
||||||
|
clicked = clicked.slice(clicked.length - 3, clicked.length);
|
||||||
|
clicked = clicked;
|
||||||
|
|
||||||
|
// trackovani, kam se naposledy kliknulo
|
||||||
|
if (clickedTask == selectedTask) {
|
||||||
|
clickedTask = null;
|
||||||
|
} else {
|
||||||
|
clickedTask = selectedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: taskMap = createTaskMap(tasks);
|
||||||
|
$: currentTask = clickedTask != null ? clickedTask : selectedTask;
|
||||||
|
|
||||||
|
function addEdge() {
|
||||||
|
if (clicked.length > 1) {
|
||||||
|
const first = clicked[clicked.length - 1];
|
||||||
|
const second = clicked[clicked.length - 2];
|
||||||
|
|
||||||
|
tasks.tasks.forEach((t) => {
|
||||||
|
if (t.id == first) {
|
||||||
|
t.requires.push(second);
|
||||||
|
t = t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tasks = tasks;
|
||||||
|
} else {
|
||||||
|
alert("Nope, prvni musis nekam klikat...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hovnoDivnaPromenaKteraJeFaktFuj = true;
|
||||||
|
function toggleDivnaPromena() {
|
||||||
|
hovnoDivnaPromenaKteraJeFaktFuj = ! hovnoDivnaPromenaKteraJeFaktFuj;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
background-color: black;
|
||||||
|
height: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 40vw;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 60vw;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbox {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
height: 50%;
|
||||||
|
background-color: pink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskDetails {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
height: 50%;
|
||||||
|
background-color: aqua;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastClicked {
|
||||||
|
height: 5%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="left">
|
||||||
|
<div class="lastClicked">Last clicked: <b>{clicked.join(' | ')}</b></div>
|
||||||
|
<div class="graph">
|
||||||
|
<Graph {tasks} bind:selectedTask on:selectTask={clickTask} runSimulationWeirdHack={hovnoDivnaPromenaKteraJeFaktFuj} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="toolbox">
|
||||||
|
<div>Toolbox</div>
|
||||||
|
<div>
|
||||||
|
<button on:click={addEdge}>Pridat hranu - posledni vyzaduje predposledni</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button on:click={toggleDivnaPromena}>Spustit simulaci</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="taskDetails">
|
||||||
|
{#if currentTask != null}
|
||||||
|
<h3>{currentTask}</h3>
|
||||||
|
<span>{taskMap.get(currentTask).comment}</span>
|
||||||
|
<ul>
|
||||||
|
{#each getCategories(tasks, currentTask) as cat}
|
||||||
|
<li>{cat}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
{#await grabAssignment(currentTask)}
|
||||||
|
Loading...
|
||||||
|
{:then text}
|
||||||
|
{@html text}
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<h3>Nothing selected...</h3>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,48 +6,35 @@
|
||||||
import { createLinksFromTaskMap } from "./task-loader";
|
import { createLinksFromTaskMap } from "./task-loader";
|
||||||
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
import type { TasksFile, TaskDescriptor } from "./task-loader";
|
||||||
|
|
||||||
const eventDispatcher = createEventDispatcher()
|
const eventDispatcher = createEventDispatcher();
|
||||||
|
|
||||||
export let tasks: TasksFile;
|
export let tasks: TasksFile;
|
||||||
let nodes = tasks.tasks;
|
export let selectedTask: null | string = null;
|
||||||
let edges = createLinksFromTaskMap(tasks);
|
|
||||||
|
$: nodes = tasks.tasks;
|
||||||
|
$: edges = createLinksFromTaskMap(tasks);
|
||||||
|
|
||||||
export let selectedTask: null | string = null
|
|
||||||
const nodeClick = (task: TaskDescriptor) => (e: CustomEvent<MouseEvent>) => {
|
const nodeClick = (task: TaskDescriptor) => (e: CustomEvent<MouseEvent>) => {
|
||||||
selectedTask = task.id
|
selectedTask = task.id;
|
||||||
eventDispatcher("selectTask", task)
|
eventDispatcher("selectTask", task);
|
||||||
}
|
};
|
||||||
|
|
||||||
const nodeHover = (task: TaskDescriptor) => (hovering: CustomEvent<boolean>) => {
|
const nodeHover = (task: TaskDescriptor) => (
|
||||||
|
hovering: CustomEvent<boolean>
|
||||||
|
) => {
|
||||||
if (hovering.detail) {
|
if (hovering.detail) {
|
||||||
selectedTask = task.id
|
selectedTask = task.id;
|
||||||
} else {
|
} else {
|
||||||
if (selectedTask == task.id)
|
if (selectedTask == task.id) selectedTask = null;
|
||||||
selectedTask = null
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Svelte automatically fills this with a reference
|
// Svelte automatically fills this with a reference
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
|
|
||||||
onMount(async () => {
|
function runSimulation() {
|
||||||
// set the dimensions and margins of the graph
|
|
||||||
var margin = { top: 10, right: 30, bottom: 30, left: 40 },
|
|
||||||
width = container.clientWidth - margin.left - margin.right,
|
|
||||||
height = container.clientHeight - margin.top - margin.bottom;
|
|
||||||
|
|
||||||
// resize the svg object
|
|
||||||
var svg = d3
|
|
||||||
.select(container)
|
|
||||||
.select("svg")
|
|
||||||
.attr("width", width + margin.left + margin.right)
|
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
|
||||||
.attr("viewBox", [-width / 2, -height / 2, width, height])
|
|
||||||
.select("g")
|
|
||||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
|
||||||
|
|
||||||
// Let's list the force we wanna apply on the network
|
// Let's list the force we wanna apply on the network
|
||||||
var simulation = d3
|
let simulation = d3
|
||||||
.forceSimulation(nodes) // Force algorithm is applied to data.nodes
|
.forceSimulation(nodes) // Force algorithm is applied to data.nodes
|
||||||
.force(
|
.force(
|
||||||
"link",
|
"link",
|
||||||
|
@ -69,12 +56,41 @@
|
||||||
edges = edges;
|
edges = edges;
|
||||||
nodes = nodes;
|
nodes = nodes;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run on create
|
||||||
|
onMount(() => {
|
||||||
|
// set the dimensions and margins of the graph
|
||||||
|
var margin = { top: 10, right: 30, bottom: 30, left: 40 },
|
||||||
|
width = container.clientWidth - margin.left - margin.right,
|
||||||
|
height = container.clientHeight - margin.top - margin.bottom;
|
||||||
|
|
||||||
|
// resize the svg object
|
||||||
|
var svg = d3
|
||||||
|
.select(container)
|
||||||
|
.select("svg")
|
||||||
|
.attr("width", width + margin.left + margin.right)
|
||||||
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
|
.attr("viewBox", [-width / 2, -height / 2, width, height])
|
||||||
|
.select("g")
|
||||||
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
|
runSimulation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// don't forget to vomit 🤮🤢
|
||||||
|
export let runSimulationWeirdHack: boolean = true;
|
||||||
|
$: {
|
||||||
|
runSimulationWeirdHack;
|
||||||
|
runSimulation();
|
||||||
|
}
|
||||||
|
// now it's safe to stop vomitting 🤮
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -83,10 +99,13 @@
|
||||||
<svg>
|
<svg>
|
||||||
<g>
|
<g>
|
||||||
{#each edges as edge}
|
{#each edges as edge}
|
||||||
<GraphEdge {edge} />
|
<GraphEdge {edge} />
|
||||||
{/each}
|
{/each}
|
||||||
{#each nodes as task}
|
{#each nodes as task}
|
||||||
<GraphNode {task} on:click={nodeClick(task)} on:hoveringChange={nodeHover(task)} />
|
<GraphNode
|
||||||
|
{task}
|
||||||
|
on:click={nodeClick(task)}
|
||||||
|
on:hoveringChange={nodeHover(task)} />
|
||||||
{/each}
|
{/each}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -72,7 +72,7 @@ async function loadTask({ url, startElement }: TaskLocation) {
|
||||||
return parseTask(startElement, rText)
|
return parseTask(startElement, rText)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function grabAssignment(id: string) {
|
export async function grabAssignment(id: string): Promise<string> {
|
||||||
const l = getLocation(id, false)
|
const l = getLocation(id, false)
|
||||||
if (!l) return "úloha je virtuální a neexistuje"
|
if (!l) return "úloha je virtuální a neexistuje"
|
||||||
return await loadTask(l)
|
return await loadTask(l)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3";
|
||||||
|
|
||||||
export type TaskDescriptor = {
|
export type TaskDescriptor = {
|
||||||
id: string
|
id: string
|
||||||
requires: []
|
requires: string[]
|
||||||
comment?: string
|
comment?: string
|
||||||
} & SimulationNodeDatum
|
} & SimulationNodeDatum
|
||||||
|
|
||||||
|
@ -49,3 +49,12 @@ export function createLinksFromTaskMap(tasks: TasksFile): SimulationLinkDatum<Ta
|
||||||
|
|
||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCategories(tasks: TasksFile, taskId: string): string[] {
|
||||||
|
let res: string[] = [];
|
||||||
|
for (let [cat, ids] of Object.entries(tasks.clusters)) {
|
||||||
|
if (ids.indexOf(taskId) >= 0) res.push(cat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
Reference in a new issue