new node / new edge modes, popups

This commit is contained in:
Vivian Lim 2024-04-06 18:19:53 -07:00
parent 277ba8dbe3
commit 4ff8242d48
9 changed files with 289 additions and 67 deletions

View File

@ -22,8 +22,12 @@
"dependencies": {
"@floating-ui/dom": "^1.6.3",
"@shoelace-style/shoelace": "^2.15.0",
"@wavebeem/candy-css": "^0.3.0",
"@types/cytoscape-edgehandles": "^4.0.3",
"@types/cytoscape-popper": "^2.0.4",
"cytoscape": "^3.28.1",
"ts-loader": "^9.5.1"
"cytoscape-edgehandles": "^4.0.1",
"cytoscape-popper": "^4.0.0",
"ts-loader": "^9.5.1",
"tseep": "^1.2.1"
}
}

View File

@ -1,12 +1,41 @@
import * as cytoscape from "cytoscape";
import * as edgehandles from "cytoscape-edgehandles";
import * as cytoscapePopper from "cytoscape-popper";
import { RoomClient } from "../../common";
import { ToggleButtons } from "./togglebuttons";
//import { computePosition, flip } from "@floating-ui/dom";
import { computePosition, flip, shift, limitShift } from "@floating-ui/dom";
function popperFactory(ref: any, content: any, opts: any) {
// see https://floating-ui.com/docs/computePosition#options
const popperOptions = {
// matching the default behaviour from Popper@2
// https://floating-ui.com/docs/migration#configure-middleware
middleware: [flip(), shift({ limiter: limitShift() })],
...opts,
};
function update() {
computePosition(ref, content, popperOptions).then(({ x, y }) => {
console.log(`update object: ${x} ${y}`);
Object.assign(content.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
update();
return { update };
}
export class ThingGraph {
private cy;
private eh;
private client: RoomClient;
private socket: WebSocket;
public constructor() {
public constructor(private modeToggle: ToggleButtons) {
cytoscape.use(edgehandles);
cytoscape.use(cytoscapePopper(popperFactory));
this.cy = cytoscape({
container: document.getElementById("cy"),
elements: [
@ -21,6 +50,31 @@ export class ThingGraph {
},
],
});
this.eh = this.cy.edgehandles({
canConnect: function (sourceNode: any, targetNode: any) {
// whether an edge can be created between source and target
return !sourceNode.same(targetNode); // e.g. disallow loops
},
edgeParams: function (sourceNode: any, targetNode: any) {
// for edges between the specified source and target
// return element object to be passed to cy.add() for edge
return { data: {} };
},
hoverDelay: 150, // time spent hovering over a target node before it is considered selected
snap: true, // when enabled, the edge can be drawn by just moving close to a target node (can be confusing on compound graphs)
snapThreshold: 50, // the target node must be less than or equal to this many pixels away from the cursor/finger
snapFrequency: 15, // the number of times per second (Hz) that snap checks done (lower is less expensive)
noEdgeEventsInDraw: true, // set events:no to edges during draws, prevents mouseouts on compounds
disableBrowserGestures: true, // during an edge drawing gesture, disable browser gestures such as two-finger trackpad swipe and pinch-to-zoom
});
this.modeToggle.emitter.on("toggled", (index, id) => {
if (id === "newEdges") {
this.eh.enableDrawMode();
} else {
this.eh.disableDrawMode();
}
});
this.socket = new WebSocket("ws://localhost:3000");
this.client = new RoomClient((tm) => {
@ -29,11 +83,47 @@ export class ThingGraph {
this.socket.addEventListener("message", (event) => {
this.client.handleMessage(event.data.toString());
});
this.cy.on("click", (event) => {
if (event.target === this.cy) {
console.log("graph clicked.");
if (this.modeToggle.getActiveId() === "newNodes") {
const pos = event.position;
this.cy.add({
group: "nodes",
data: {},
position: pos,
});
}
} else {
const group = event.target.group();
console.log(`a '${group}' was clicked`);
let popper = event.target.popper({
content: () => {
let div = document.createElement("sl-card");
div.innerHTML = "Popper content";
div.classList.add("popper-div");
div.classList.add("card-basic");
document.body.appendChild(div);
return div;
},
});
let update = () => {
popper.update();
};
event.target.on("position", update);
this.cy.on("pan zoom resize", update);
}
});
}
public resize() {
this.cy.resize();
this.cy.fit();
}
}

View File

@ -1,57 +1,68 @@
<sl-split-panel position="25" style="height:100%;">
<sl-split-panel position="15" style="height: 100%">
<sl-icon slot="divider" name="grip-vertical"></sl-icon>
<div
slot="start"
style="--min: 100px; --max: 33%; height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
slot="start"
style="
--min: 100px;
--max: 33%;
height: 100%;
background: var(--sl-color-neutral-50);
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
overflow: hidden;
"
>
stuff
<div>
<sl-button-group label="Mode">
<sl-button size="medium" id="newNodes">New Nodes</sl-button>
<sl-button size="medium" id="newEdges">New Edges</sl-button>
<sl-button size="medium">IDK</sl-button>
</sl-button-group>
</div>
<div>
<sl-button> Button time </sl-button>
</div>
</div>
<div
slot="end"
id="cy"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
</div>
</sl-split-panel>
slot="end"
id="cy"
style="
height: 100%;
background: var(--sl-color-neutral-50);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
"
></div>
</sl-split-panel>
<style>
.split-panel-divider sl-split-panel {
--divider-width: 2px;
}
.split-panel-divider sl-split-panel {
--divider-width: 2px;
}
.split-panel-divider sl-split-panel::part(divider) {
background-color: var(--sl-color-pink-600);
}
.split-panel-divider sl-split-panel::part(divider) {
background-color: var(--sl-color-pink-600);
}
.split-panel-divider sl-icon {
position: absolute;
border-radius: var(--sl-border-radius-small);
background: var(--sl-color-pink-600);
color: var(--sl-color-neutral-0);
padding: 0.5rem 0.125rem;
}
.split-panel-divider sl-icon {
position: absolute;
border-radius: var(--sl-border-radius-small);
background: var(--sl-color-pink-600);
color: var(--sl-color-neutral-0);
padding: 0.5rem 0.125rem;
}
.split-panel-divider sl-split-panel::part(divider):focus-visible {
background-color: var(--sl-color-primary-600);
}
.split-panel-divider sl-split-panel::part(divider):focus-visible {
background-color: var(--sl-color-primary-600);
}
.split-panel-divider sl-split-panel:focus-within sl-icon {
background-color: var(--sl-color-primary-600);
color: var(--sl-color-neutral-0);
}
.split-panel-divider sl-split-panel:focus-within sl-icon {
background-color: var(--sl-color-primary-600);
color: var(--sl-color-neutral-0);
}
</style>
<div>
<button type="button" class="candy-button candy-texture-glossy">
Glossy
</button>
<button type="button" class="candy-button candy-texture-glossy">
Glossy
</button>
<button type="button" class="candy-button candy-texture-glossy">
Glossy
</button>
<button type="button" class="candy-button candy-texture-glossy">
Glossy
</button>
</div>

View File

@ -1,11 +1,16 @@
import { ThingGraph } from "./graph";
import "@wavebeem/candy-css";
import "@shoelace-style/shoelace/dist/themes/light.css";
import "@shoelace-style/shoelace/dist/themes/dark.css";
import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/button-group/button-group.js";
import "@shoelace-style/shoelace/dist/components/split-panel/split-panel.js";
import "@shoelace-style/shoelace/dist/components/popup/popup.js";
import "@shoelace-style/shoelace/dist/components/icon/icon.js";
import "@shoelace-style/shoelace/dist/components/card/card.js";
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
import idk from "./idk.html";
import { ToggleButtons } from "./togglebuttons";
setBasePath("/dist/shoelace/assets");
setBasePath("/dist/shoelace");
function component() {
const element = document.createElement("div");
@ -19,12 +24,17 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("app").innerHTML = idk;
const splitPanel = document.querySelector("sl-split-panel");
const mode = new ToggleButtons(["newNodes", "newEdges"]);
mode.emitter.on("toggled", (index, id) => {
console.log(`toggled: ${index} ${id}`);
});
var once = {
created: false,
};
splitPanel.updateComplete.then(() => {
if (!once.created) {
const g = new ThingGraph();
const g = new ThingGraph(mode);
splitPanel.addEventListener("sl-reposition", () => {
g.resize();
});

View File

@ -0,0 +1,45 @@
import { SlButton } from "@shoelace-style/shoelace";
import { EventEmitter } from "tseep";
export class ToggleButtons {
private activeIndex: number = 0;
private activeId: string;
private buttonElements: SlButton[] = [];
public readonly emitter = new EventEmitter<{
toggled: (index: number, id: string) => void;
}>();
constructor(private buttonIds: string[]) {
for (var i = 0; i < buttonIds.length; i++) {
const thisButtonIndex = i;
const id = buttonIds[i];
const buttonElement = document.querySelector(`sl-button#${id}`);
if (buttonElement === undefined) {
throw new Error(`Button doesn't exist: ${id}`);
}
buttonElement.addEventListener("click", () => {
this.activeIndex = thisButtonIndex;
this.emitter.emit("toggled", thisButtonIndex, id);
this.updateHighlight();
});
this.buttonElements.push(buttonElement as SlButton);
}
this.activeId = buttonIds[0];
this.updateHighlight();
}
public getActiveId() {
return this.activeId;
}
private updateHighlight() {
for (let i = 0; i < this.buttonElements.length; i++) {
const element = this.buttonElements[i];
if (this.activeIndex === i) {
element.setAttribute("variant", "primary");
} else {
element.setAttribute("variant", "default");
}
}
}
}

View File

@ -20,15 +20,27 @@ importers:
'@shoelace-style/shoelace':
specifier: ^2.15.0
version: 2.15.0(@types/react@18.2.74)
'@wavebeem/candy-css':
specifier: ^0.3.0
version: 0.3.0
'@types/cytoscape-edgehandles':
specifier: ^4.0.3
version: 4.0.3
'@types/cytoscape-popper':
specifier: ^2.0.4
version: 2.0.4
cytoscape:
specifier: ^3.28.1
version: 3.28.1
cytoscape-edgehandles:
specifier: ^4.0.1
version: 4.0.1(cytoscape@3.28.1)
cytoscape-popper:
specifier: ^4.0.0
version: 4.0.0(cytoscape@3.28.1)
ts-loader:
specifier: ^9.5.1
version: 9.5.1(typescript@5.4.3)(webpack@5.91.0)
tseep:
specifier: ^1.2.1
version: 1.2.1
devDependencies:
'@types/cytoscape':
specifier: ^3.21.0
@ -448,6 +460,10 @@ packages:
fastq: 1.17.1
dev: false
/@popperjs/core@2.11.8:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@shoelace-style/animations@1.1.0:
resolution: {integrity: sha512-Be+cahtZyI2dPKRm8EZSx3YJQ+jLvEcn3xzRP7tM4tqBnvd/eW/64Xh0iOf0t2w5P8iJKfdBbpVNE9naCaOf2g==}
dev: false
@ -506,9 +522,21 @@ packages:
'@types/express': 4.17.21
dev: true
/@types/cytoscape-edgehandles@4.0.3:
resolution: {integrity: sha512-n/nUzfSudfbrtvcJIsruCvfduoW2zg/r+EjjFmceDDP+Pbdfx5A/fA/bAfAc4QlOwnkZ3HzF2oESAzes5mQHcg==}
dependencies:
'@types/cytoscape': 3.21.0
dev: false
/@types/cytoscape-popper@2.0.4:
resolution: {integrity: sha512-vGRiAMXeEIoY5ziPO0NrS8xmJyVkT8j8ARzvyD/x6CXciAO1+80Q2/Triyd9/+5I4PeatGo1Pch5YBwDMB1D6A==}
dependencies:
'@popperjs/core': 2.11.8
'@types/cytoscape': 3.21.0
dev: false
/@types/cytoscape@3.21.0:
resolution: {integrity: sha512-RN5SPiyVDpUP+LoOlxxlOYAMzkE7iuv3gA1jt3Hx2qTwArpZVPPdO+SI0hUj49OAn4QABR7JK9Gi0hibzGE0Aw==}
dev: true
/@types/eslint-scope@3.7.7:
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@ -633,10 +661,6 @@ packages:
'@types/node': 20.12.4
dev: true
/@wavebeem/candy-css@0.3.0:
resolution: {integrity: sha512-f8vbMQAYryU6a6o36tPhAycQI5WAf6TfrSTxV9hdYPbe8iU/uVbf5/BxDAEmFPuVPf9tW3mMsi/FX0FFiwt4ng==}
dev: false
/@webassemblyjs/ast@1.12.1:
resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==}
dependencies:
@ -1170,6 +1194,24 @@ packages:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dev: false
/cytoscape-edgehandles@4.0.1(cytoscape@3.28.1):
resolution: {integrity: sha512-uSYshkqRZ4luCxK295bEVTg46q4ZW+fwJhcIzMrtfNR7zeAnJ38Z48kUGeu5ibtXkgLbcZAg0YE4ED2dRuaePg==}
peerDependencies:
cytoscape: ^3.2.0
dependencies:
cytoscape: 3.28.1
lodash.memoize: 4.1.2
lodash.throttle: 4.1.1
dev: false
/cytoscape-popper@4.0.0(cytoscape@3.28.1):
resolution: {integrity: sha512-M4q2YeIhZvRDslMLzVuGZKb6HAU3O6M51NAaRc0hr3KubQabiK2c9dEGwfVIBPcDnxr9u/oFAMhAU7DEf2EHaA==}
peerDependencies:
cytoscape: ^3.2.0
dependencies:
cytoscape: 3.28.1
dev: false
/cytoscape@3.28.1:
resolution: {integrity: sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==}
engines: {node: '>=0.10'}
@ -1800,6 +1842,14 @@ packages:
dependencies:
p-locate: 4.1.0
/lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
dev: false
/lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
dev: false
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
@ -2584,6 +2634,10 @@ packages:
webpack: 5.91.0(webpack-cli@5.1.4)
dev: false
/tseep@1.2.1:
resolution: {integrity: sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==}
dev: false
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: true

View File

@ -13,4 +13,15 @@ a {
body {
height: 100vh;
padding: 0;
margin: 0;
}
.popper-div {
z-index: 9999;
padding: 0.25em;
/*pointer-events: none;*/
width: max-content;
position: absolute;
top: 0;
left: 0;
}

View File

@ -1,5 +1 @@
extends layout
block content
h1= title
p Welcome to #{title}
extends layout

View File

@ -1,8 +1,9 @@
doctype html
html
html(class='sl-theme-dark')
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
link(rel='stylesheet', href='/dist/main.css')
script(src='/dist/main.js')
body
block content