update
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -10,7 +10,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cytoscape": "^3.21.9",
|
"@types/cytoscape": "^3.21.9",
|
||||||
"cytoscape": "^3.33.4"
|
"cytoscape": "^3.33.4",
|
||||||
|
"cytoscape-node-html-label": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
@@ -518,6 +519,21 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cytoscape-node-html-label": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cytoscape-node-html-label/-/cytoscape-node-html-label-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-oUVwrlsIlaJJ8QrQFSMdv3uXVXPg6tMH/Tfofr8JuZIovqI4fPqBi6sQgCMcVpS6k9Td0TTjowBsNRw32CESWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/cytoscape": "^3.1.0",
|
||||||
|
"cytoscape": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/cytoscape": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cytoscape": "^3.21.9",
|
"@types/cytoscape": "^3.21.9",
|
||||||
"cytoscape": "^3.33.4"
|
"cytoscape": "^3.33.4",
|
||||||
|
"cytoscape-node-html-label": "^1.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,40 @@
|
|||||||
import { App, Notice, WorkspaceLeaf, normalizePath } from "obsidian";
|
import { App, Notice, WorkspaceLeaf, normalizePath } from "obsidian";
|
||||||
import cytoscape, { Core, NodeSingular } from "cytoscape";
|
import cytoscape, { Core, NodeSingular } from "cytoscape";
|
||||||
|
// @ts-ignore — no exported types
|
||||||
|
import nodeHtmlLabel from "cytoscape-node-html-label";
|
||||||
import { CORE_FILE } from "../const";
|
import { CORE_FILE } from "../const";
|
||||||
|
|
||||||
|
let htmlLabelRegistered = false;
|
||||||
|
function ensureHtmlLabel(): void {
|
||||||
|
if (htmlLabelRegistered) return;
|
||||||
|
cytoscape.use(nodeHtmlLabel as never);
|
||||||
|
htmlLabelRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s: string): string {
|
||||||
|
return s
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeTemplate(data: { name: string; features: string[] }): string {
|
||||||
|
const name = escapeHtml(data.name);
|
||||||
|
const badges = data.features
|
||||||
|
.map(
|
||||||
|
(f) =>
|
||||||
|
`<span class="pk-badge" data-action="open-feature" data-feature="${escapeHtml(f)}">${escapeHtml(f)}</span>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
return `
|
||||||
|
<div class="pk-graph-node" data-node="${name}">
|
||||||
|
<strong data-action="open-collection">${name}</strong>
|
||||||
|
${data.features.length ? `<div class="pk-badges">${badges}</div>` : ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
import {
|
import {
|
||||||
projectPath,
|
projectPath,
|
||||||
collectionPath,
|
collectionPath,
|
||||||
@@ -9,6 +43,7 @@ import {
|
|||||||
listCollectionFeatures,
|
listCollectionFeatures,
|
||||||
createCollection,
|
createCollection,
|
||||||
renameCollection,
|
renameCollection,
|
||||||
|
createFeature,
|
||||||
deleteRecursive,
|
deleteRecursive,
|
||||||
} from "../fs";
|
} from "../fs";
|
||||||
import { menu, openMarkdown } from "../ui";
|
import { menu, openMarkdown } from "../ui";
|
||||||
@@ -37,6 +72,7 @@ function validateName(name: string, taken: string[], current?: string): string |
|
|||||||
export class GraphView {
|
export class GraphView {
|
||||||
private cy: Core | null = null;
|
private cy: Core | null = null;
|
||||||
private host: HTMLElement | null = null;
|
private host: HTMLElement | null = null;
|
||||||
|
private docListeners: Array<() => void> = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private app: App,
|
private app: App,
|
||||||
@@ -46,6 +82,8 @@ export class GraphView {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
for (const off of this.docListeners) off();
|
||||||
|
this.docListeners = [];
|
||||||
if (this.cy) {
|
if (this.cy) {
|
||||||
this.cy.destroy();
|
this.cy.destroy();
|
||||||
this.cy = null;
|
this.cy = null;
|
||||||
@@ -55,6 +93,7 @@ export class GraphView {
|
|||||||
|
|
||||||
async render(parent: HTMLElement): Promise<void> {
|
async render(parent: HTMLElement): Promise<void> {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
ensureHtmlLabel();
|
||||||
const container = parent.createDiv({ cls: "pk-graph" });
|
const container = parent.createDiv({ cls: "pk-graph" });
|
||||||
this.host = container;
|
this.host = container;
|
||||||
|
|
||||||
@@ -63,11 +102,8 @@ export class GraphView {
|
|||||||
|
|
||||||
const elements = this.buildElements(data, collections);
|
const elements = this.buildElements(data, collections);
|
||||||
const styles = getComputedStyle(parent);
|
const styles = getComputedStyle(parent);
|
||||||
const textColor = styles.getPropertyValue("--text-normal").trim() || "#222";
|
|
||||||
const borderColor = styles.getPropertyValue("--background-modifier-border").trim() || "#ccc";
|
|
||||||
const edgeColor = styles.getPropertyValue("--text-muted").trim() || "#888";
|
const edgeColor = styles.getPropertyValue("--text-muted").trim() || "#888";
|
||||||
const accent = styles.getPropertyValue("--interactive-accent").trim() || "#7b6cd9";
|
const accent = styles.getPropertyValue("--interactive-accent").trim() || "#7b6cd9";
|
||||||
const bgNode = styles.getPropertyValue("--background-primary").trim() || "#fff";
|
|
||||||
|
|
||||||
this.cy = cytoscape({
|
this.cy = cytoscape({
|
||||||
container,
|
container,
|
||||||
@@ -77,22 +113,15 @@ export class GraphView {
|
|||||||
userPanningEnabled: true,
|
userPanningEnabled: true,
|
||||||
style: [
|
style: [
|
||||||
{
|
{
|
||||||
selector: "node",
|
selector: "node[name]",
|
||||||
style: {
|
style: {
|
||||||
label: "data(label)",
|
"background-color": "rgba(0,0,0,0)",
|
||||||
"text-valign": "center",
|
"background-opacity": 0,
|
||||||
"text-halign": "center",
|
"border-width": 0,
|
||||||
"text-wrap": "wrap",
|
|
||||||
"text-max-width": "160px",
|
|
||||||
"background-color": bgNode,
|
|
||||||
"border-color": borderColor,
|
|
||||||
"border-width": 1,
|
|
||||||
color: textColor,
|
|
||||||
"font-size": 12,
|
|
||||||
shape: "round-rectangle",
|
shape: "round-rectangle",
|
||||||
width: "label" as unknown as number,
|
label: "",
|
||||||
height: "label" as unknown as number,
|
width: "data(w)" as unknown as number,
|
||||||
padding: "10px",
|
height: "data(h)" as unknown as number,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -100,9 +129,13 @@ export class GraphView {
|
|||||||
style: {
|
style: {
|
||||||
shape: "ellipse",
|
shape: "ellipse",
|
||||||
"background-color": accent,
|
"background-color": accent,
|
||||||
|
"background-opacity": 1,
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
"border-width": 0,
|
"border-width": 0,
|
||||||
"font-weight": "bold",
|
"font-weight": "bold",
|
||||||
|
label: "data(label)",
|
||||||
|
"text-valign": "center",
|
||||||
|
"text-halign": "center",
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
},
|
},
|
||||||
@@ -112,6 +145,7 @@ export class GraphView {
|
|||||||
style: {
|
style: {
|
||||||
"border-color": accent,
|
"border-color": accent,
|
||||||
"border-width": 3,
|
"border-width": 3,
|
||||||
|
"background-opacity": 0.05,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -129,9 +163,9 @@ export class GraphView {
|
|||||||
name: "concentric",
|
name: "concentric",
|
||||||
concentric: (node) => -((node.data("depth") as number) ?? 0),
|
concentric: (node) => -((node.data("depth") as number) ?? 0),
|
||||||
levelWidth: () => 1,
|
levelWidth: () => 1,
|
||||||
minNodeSpacing: 60,
|
minNodeSpacing: 80,
|
||||||
// @ts-ignore — spacingFactor exists in concentric layout
|
// @ts-ignore — spacingFactor exists in concentric layout
|
||||||
spacingFactor: 1.4,
|
spacingFactor: 1.6,
|
||||||
fit: true,
|
fit: true,
|
||||||
padding: 30,
|
padding: 30,
|
||||||
startAngle: -Math.PI / 2,
|
startAngle: -Math.PI / 2,
|
||||||
@@ -139,6 +173,18 @@ export class GraphView {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-ignore — added by cytoscape-node-html-label
|
||||||
|
this.cy.nodeHtmlLabel([
|
||||||
|
{
|
||||||
|
query: "node[name]",
|
||||||
|
halign: "center",
|
||||||
|
valign: "center",
|
||||||
|
halignBox: "center",
|
||||||
|
valignBox: "center",
|
||||||
|
tpl: (d: { name: string; features: string[] }) => nodeTemplate(d),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
this.bindHandlers();
|
this.bindHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,13 +207,11 @@ export class GraphView {
|
|||||||
if (visited.has(child)) continue;
|
if (visited.has(child)) continue;
|
||||||
visited.add(child);
|
visited.add(child);
|
||||||
const id = `n_${child}`;
|
const id = `n_${child}`;
|
||||||
|
const features = listCollectionFeatures(this.app, this.project, child)
|
||||||
|
.map((f) => f.basename);
|
||||||
|
const { w, h } = this.nodeSize(features.length);
|
||||||
els.push({
|
els.push({
|
||||||
data: {
|
data: { id, name: child, depth, features, w, h },
|
||||||
id,
|
|
||||||
name: child,
|
|
||||||
label: this.nodeLabel(child),
|
|
||||||
depth,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
els.push({ data: { source: parentId, target: id } });
|
els.push({ data: { source: parentId, target: id } });
|
||||||
walk(child, id, depth + 1);
|
walk(child, id, depth + 1);
|
||||||
@@ -177,11 +221,11 @@ export class GraphView {
|
|||||||
return els;
|
return els;
|
||||||
}
|
}
|
||||||
|
|
||||||
private nodeLabel(collection: string): string {
|
private nodeSize(featureCount: number): { w: number; h: number } {
|
||||||
const features = listCollectionFeatures(this.app, this.project, collection)
|
const w = 180;
|
||||||
.map((f) => f.basename);
|
const rows = Math.max(1, Math.ceil(featureCount / 2));
|
||||||
if (features.length === 0) return collection;
|
const h = 40 + rows * 26;
|
||||||
return `${collection}\n${features.map((f) => `• ${f}`).join(" ")}`;
|
return { w, h };
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindHandlers(): void {
|
private bindHandlers(): void {
|
||||||
@@ -190,31 +234,9 @@ export class GraphView {
|
|||||||
|
|
||||||
cy.on("tap", "node", (ev) => {
|
cy.on("tap", "node", (ev) => {
|
||||||
const node = ev.target as NodeSingular;
|
const node = ev.target as NodeSingular;
|
||||||
if (node.id() === ROOT_ID) {
|
if (node.id() !== ROOT_ID) return;
|
||||||
const corePath = normalizePath(`${projectPath(this.project)}/${CORE_FILE}`);
|
const corePath = normalizePath(`${projectPath(this.project)}/${CORE_FILE}`);
|
||||||
void openMarkdown(this.app, corePath, this.leaf);
|
void openMarkdown(this.app, corePath, this.leaf);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = node.data("name") as string;
|
|
||||||
void openMarkdown(this.app, collectionMdPath(this.project, name), this.leaf);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.on("cxttap", "node", (ev) => {
|
|
||||||
const node = ev.target as NodeSingular;
|
|
||||||
const oe = ev.originalEvent as MouseEvent;
|
|
||||||
oe.preventDefault();
|
|
||||||
if (node.id() === ROOT_ID) {
|
|
||||||
menu(oe, [
|
|
||||||
{ title: "Node erstellen", icon: "plus", onClick: () => this.openCreateChild(null) },
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = node.data("name") as string;
|
|
||||||
menu(oe, [
|
|
||||||
{ title: "Child-Node erstellen", icon: "plus", onClick: () => this.openCreateChild(name) },
|
|
||||||
{ title: "Umbenennen", icon: "pencil", onClick: () => this.openRename(name) },
|
|
||||||
{ title: "Löschen", icon: "trash", onClick: () => this.deleteNode(name) },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.on("cxttap", (ev) => {
|
cy.on("cxttap", (ev) => {
|
||||||
@@ -226,74 +248,153 @@ export class GraphView {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let grabbed: NodeSingular | null = null;
|
if (this.host) this.bindHtmlHandlers(this.host);
|
||||||
let grabStart: { x: number; y: number } | null = null;
|
|
||||||
|
|
||||||
cy.on("grab", "node", (ev) => {
|
|
||||||
const n = ev.target as NodeSingular;
|
|
||||||
if (n.id() === ROOT_ID) return;
|
|
||||||
grabbed = n;
|
|
||||||
const pos = n.position();
|
|
||||||
grabStart = { x: pos.x, y: pos.y };
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.on("drag", "node", (ev) => {
|
|
||||||
if (!grabbed || ev.target.id() !== grabbed.id()) return;
|
|
||||||
const hovered = this.nodeAtPoint(grabbed.position(), grabbed.id());
|
|
||||||
cy.nodes(".pk-drop-target").removeClass("pk-drop-target");
|
|
||||||
if (hovered) hovered.addClass("pk-drop-target");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.on("free", "node", (ev) => {
|
|
||||||
if (!grabbed || ev.target.id() !== grabbed.id()) return;
|
|
||||||
const node = grabbed;
|
|
||||||
const start = grabStart;
|
|
||||||
grabbed = null;
|
|
||||||
grabStart = null;
|
|
||||||
cy.nodes(".pk-drop-target").removeClass("pk-drop-target");
|
|
||||||
|
|
||||||
const target = this.nodeAtPoint(node.position(), node.id());
|
|
||||||
if (!target) {
|
|
||||||
if (start) node.position(start);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const childName = node.data("name") as string;
|
|
||||||
const newParent = target.id() === ROOT_ID ? null : (target.data("name") as string);
|
|
||||||
void this.handleReparent(childName, newParent, start);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private nodeAtPoint(pos: { x: number; y: number }, excludeId: string): NodeSingular | null {
|
private bindHtmlHandlers(host: HTMLElement): void {
|
||||||
if (!this.cy) return null;
|
host.addEventListener("contextmenu", (ev) => {
|
||||||
let hit: NodeSingular | null = null;
|
const target = ev.target as HTMLElement;
|
||||||
this.cy.nodes().forEach((n) => {
|
const nodeEl = target.closest<HTMLElement>(".pk-graph-node");
|
||||||
if (n.id() === excludeId) return;
|
if (!nodeEl) return;
|
||||||
const bb = n.boundingBox({});
|
const name = nodeEl.dataset.node;
|
||||||
if (pos.x >= bb.x1 && pos.x <= bb.x2 && pos.y >= bb.y1 && pos.y <= bb.y2) {
|
if (!name) return;
|
||||||
hit = n;
|
ev.preventDefault();
|
||||||
}
|
ev.stopPropagation();
|
||||||
|
menu(ev as MouseEvent, [
|
||||||
|
{ title: "Feature erstellen", icon: "plus", onClick: () => this.openCreateFeature(name) },
|
||||||
|
{ title: "Child-Node erstellen", icon: "plus", onClick: () => this.openCreateChild(name) },
|
||||||
|
{ title: "Umbenennen", icon: "pencil", onClick: () => this.openRename(name) },
|
||||||
|
{ title: "Löschen", icon: "trash", onClick: () => this.deleteNode(name) },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
return hit;
|
|
||||||
|
const THRESHOLD = 6;
|
||||||
|
let dragName: string | null = null;
|
||||||
|
let sourceEl: HTMLElement | null = null;
|
||||||
|
let downPos: { x: number; y: number } | null = null;
|
||||||
|
let dragging = false;
|
||||||
|
|
||||||
|
const clearDropClasses = () => {
|
||||||
|
host.querySelectorAll<HTMLElement>(".pk-drop-target").forEach((el) =>
|
||||||
|
el.removeClass("pk-drop-target"),
|
||||||
|
);
|
||||||
|
host.removeClass("pk-drop-target-bg");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetDrag = () => {
|
||||||
|
if (sourceEl) sourceEl.removeClass("pk-dragging");
|
||||||
|
clearDropClasses();
|
||||||
|
dragName = null;
|
||||||
|
sourceEl = null;
|
||||||
|
downPos = null;
|
||||||
|
dragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
host.addEventListener("mousedown", (ev) => {
|
||||||
|
if (ev.button !== 0) return;
|
||||||
|
const nodeEl = (ev.target as HTMLElement).closest<HTMLElement>(".pk-graph-node");
|
||||||
|
if (!nodeEl) return;
|
||||||
|
dragName = nodeEl.dataset.node ?? null;
|
||||||
|
sourceEl = nodeEl;
|
||||||
|
downPos = { x: ev.clientX, y: ev.clientY };
|
||||||
|
dragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onMouseMove = (ev: MouseEvent) => {
|
||||||
|
if (!dragName || !downPos) return;
|
||||||
|
const dx = ev.clientX - downPos.x;
|
||||||
|
const dy = ev.clientY - downPos.y;
|
||||||
|
if (!dragging) {
|
||||||
|
if (Math.hypot(dx, dy) < THRESHOLD) return;
|
||||||
|
dragging = true;
|
||||||
|
sourceEl?.addClass("pk-dragging");
|
||||||
|
}
|
||||||
|
clearDropClasses();
|
||||||
|
const el = document.elementFromPoint(ev.clientX, ev.clientY);
|
||||||
|
const hit = el?.closest<HTMLElement>(".pk-graph-node");
|
||||||
|
if (hit && hit !== sourceEl) {
|
||||||
|
hit.addClass("pk-drop-target");
|
||||||
|
} else if (host.contains(el as Node) && !hit) {
|
||||||
|
host.addClass("pk-drop-target-bg");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (ev: MouseEvent) => {
|
||||||
|
if (!dragName) return;
|
||||||
|
|
||||||
|
if (!dragging) {
|
||||||
|
const el = document.elementFromPoint(ev.clientX, ev.clientY);
|
||||||
|
const upNode = el?.closest<HTMLElement>(".pk-graph-node");
|
||||||
|
const upName = upNode?.dataset.node ?? null;
|
||||||
|
if (upName && upName === dragName) {
|
||||||
|
const featEl = el?.closest<HTMLElement>("[data-action='open-feature']");
|
||||||
|
if (featEl?.dataset.feature) {
|
||||||
|
void openMarkdown(
|
||||||
|
this.app,
|
||||||
|
normalizePath(`${collectionPath(this.project, upName)}/${featEl.dataset.feature}.md`),
|
||||||
|
this.leaf,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
void openMarkdown(this.app, collectionMdPath(this.project, upName), this.leaf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = dragName;
|
||||||
|
const el = document.elementFromPoint(ev.clientX, ev.clientY);
|
||||||
|
const targetEl = el?.closest<HTMLElement>(".pk-graph-node");
|
||||||
|
const targetInHost = host.contains(el as Node);
|
||||||
|
const targetName = targetEl && targetEl !== sourceEl
|
||||||
|
? (targetEl.dataset.node ?? null)
|
||||||
|
: null;
|
||||||
|
const validDrop = !!targetEl || targetInHost;
|
||||||
|
resetDrag();
|
||||||
|
if (!validDrop) return;
|
||||||
|
if (targetName === source) return;
|
||||||
|
void this.handleReparent(source, targetName);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
|
this.docListeners.push(
|
||||||
|
() => document.removeEventListener("mousemove", onMouseMove),
|
||||||
|
() => document.removeEventListener("mouseup", onMouseUp),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleReparent(
|
private async handleReparent(
|
||||||
child: string,
|
child: string,
|
||||||
newParent: string | null,
|
newParent: string | null,
|
||||||
start: { x: number; y: number } | null,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const data = await readGraph(this.app, this.project);
|
const data = await readGraph(this.app, this.project);
|
||||||
const next = setParent(data, child, newParent);
|
const next = setParent(data, child, newParent);
|
||||||
if (JSON.stringify(next) === JSON.stringify(data)) {
|
if (JSON.stringify(next) === JSON.stringify(data)) return;
|
||||||
if (start && this.cy) {
|
|
||||||
const n = this.cy.getElementById(`n_${child}`);
|
|
||||||
if (n.nonempty()) n.position(start);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await writeGraph(this.app, this.project, next);
|
await writeGraph(this.app, this.project, next);
|
||||||
this.onChange();
|
this.onChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openCreateFeature(collectionName: string): void {
|
||||||
|
const taken = listCollectionFeatures(this.app, this.project, collectionName).map(
|
||||||
|
(f) => f.basename,
|
||||||
|
);
|
||||||
|
new NameModal(this.app, {
|
||||||
|
title: "Neues Feature",
|
||||||
|
label: "Feature-Name",
|
||||||
|
cta: "Erstellen",
|
||||||
|
validate: (n) => validateName(n, taken),
|
||||||
|
onSubmit: async (name) => {
|
||||||
|
try {
|
||||||
|
await createFeature(this.app, this.project, collectionName, name);
|
||||||
|
this.onChange();
|
||||||
|
} catch (e) {
|
||||||
|
new Notice(`Fehler: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).open();
|
||||||
|
}
|
||||||
|
|
||||||
private openCreateChild(parent: string | null): void {
|
private openCreateChild(parent: string | null): void {
|
||||||
const taken = listFolders(this.app, projectPath(this.project)).map((c) => c.name);
|
const taken = listFolders(this.app, projectPath(this.project)).map((c) => c.name);
|
||||||
new NameModal(this.app, {
|
new NameModal(this.app, {
|
||||||
|
|||||||
50
styles.css
50
styles.css
@@ -213,6 +213,56 @@
|
|||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Graph: HTML-Node */
|
||||||
|
.pk-graph-node {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 160px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: stretch;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-normal);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
font-size: 0.85em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph-node strong {
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph-node strong:hover {
|
||||||
|
color: var(--text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph-node .pk-badges {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph-node .pk-badge {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph-node.pk-drop-target {
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph-node.pk-dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-graph.pk-drop-target-bg {
|
||||||
|
outline: 2px dashed var(--interactive-accent);
|
||||||
|
outline-offset: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Collection-Card Badges */
|
/* Collection-Card Badges */
|
||||||
.pk-badges {
|
.pk-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user