mirror of
https://github.com/miro0o/miniWorldMap.git
synced 2026-07-22 07:46:00 +00:00
add temporary pin function
This commit is contained in:
parent
34062027b8
commit
bd1c91ed02
2 changed files with 594 additions and 22 deletions
499
main.js
499
main.js
|
|
@ -724,7 +724,7 @@ class WorldMapIndex {
|
|||
}
|
||||
|
||||
aggregateVisibleLinkEdges(visible, state, rootId) {
|
||||
const needsHoverLinks = hoverHighlightsNoteLinks(state.hoverHighlightMode);
|
||||
const needsHoverLinks = hoverHighlightsNoteLinks(state.hoverHighlightMode) || Boolean(state.pinNeedsHoverLinks);
|
||||
if (!state.showLinkOverlay && !needsHoverLinks) return {
|
||||
linkEdges: [],
|
||||
hoverLinkEdges: [],
|
||||
|
|
@ -1090,6 +1090,14 @@ class MiniWorldMapView extends ItemView {
|
|||
this.positions = new Map();
|
||||
this.selectedNodeId = null;
|
||||
this.selectedLink = null;
|
||||
this.pinnedPaths = [];
|
||||
this.pinGroups = [];
|
||||
this.selectedPinIds = new Set();
|
||||
this.nextPinId = 1;
|
||||
this.nextPinGroupId = 1;
|
||||
this.lastHoverPinCandidate = null;
|
||||
this.lastHoverPinCandidateExpiresAt = 0;
|
||||
this.pinGroupName = "";
|
||||
this.lastLayout = null;
|
||||
this.renderTimer = null;
|
||||
this.activeHighlightElements = new Set();
|
||||
|
|
@ -1206,6 +1214,7 @@ class MiniWorldMapView extends ItemView {
|
|||
sidePanelPages() {
|
||||
return [
|
||||
["inspect", "Inspect", "info"],
|
||||
["pins", "Pins", "pin"],
|
||||
["view", "View", "navigation"],
|
||||
["controls", "Controls", "sliders-horizontal"],
|
||||
["defaults", "Defaults", "settings"]
|
||||
|
|
@ -1230,6 +1239,7 @@ class MiniWorldMapView extends ItemView {
|
|||
this.labelVisibilitySelect = null;
|
||||
this.swirlInput = null;
|
||||
this.swirlLabel = null;
|
||||
this.pinGroupInput = null;
|
||||
this.panelControlRefs = new Map();
|
||||
}
|
||||
|
||||
|
|
@ -1358,6 +1368,273 @@ class MiniWorldMapView extends ItemView {
|
|||
return textArea;
|
||||
}
|
||||
|
||||
needsCanvasHoverLinks() {
|
||||
return hoverHighlightsNoteLinks(this.state.hoverHighlightMode) || this.pinnedPathsNeedHoverLinks();
|
||||
}
|
||||
|
||||
pinnedPathsNeedHoverLinks() {
|
||||
return this.pinnedPaths.some(pin =>
|
||||
pin.kind === "link" || (pin.kind === "node" && hoverHighlightsNoteLinks(pin.mode))
|
||||
);
|
||||
}
|
||||
|
||||
rememberHoverPinCandidate(index, graph) {
|
||||
const candidate = this.currentHoverPinCandidate(index, graph);
|
||||
if (!candidate) return;
|
||||
this.lastHoverPinCandidate = candidate;
|
||||
this.lastHoverPinCandidateExpiresAt = performance.now() + 15000;
|
||||
}
|
||||
|
||||
currentHoverPinCandidate(index, graph) {
|
||||
if (!index || !graph) return null;
|
||||
if (this.hoverNodeId !== null && this.hoverNodeId !== undefined) {
|
||||
const node = graphNode(index, graph, this.hoverNodeId);
|
||||
return node ? this.createNodePinCandidate(node, this.state.hoverHighlightMode) : null;
|
||||
}
|
||||
if (this.hoverLink) return this.createLinkPinCandidate(index, graph, this.hoverLink);
|
||||
return null;
|
||||
}
|
||||
|
||||
createNodePinCandidate(node, mode) {
|
||||
return {
|
||||
kind: "node",
|
||||
nodeId: node.id,
|
||||
mode: normalizeHoverHighlightMode(mode),
|
||||
title: node.title || node.id || ROOT_TITLE,
|
||||
path: node.path || "/",
|
||||
nodeType: node.type
|
||||
};
|
||||
}
|
||||
|
||||
createLinkPinCandidate(index, graph, edge) {
|
||||
const source = graphNode(index, graph, edge.source);
|
||||
const target = graphNode(index, graph, edge.target);
|
||||
const sourceTitle = source ? source.title : edge.source;
|
||||
const targetTitle = target ? target.title : edge.target;
|
||||
const sourcePath = source ? source.path : edge.source;
|
||||
const targetPath = target ? target.path : edge.target;
|
||||
return {
|
||||
kind: "link",
|
||||
edgeId: edge.id || `${edge.source}->${edge.target}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
title: `${sourceTitle} -> ${targetTitle}`,
|
||||
path: `${sourcePath || "/"} -> ${targetPath || "/"}`,
|
||||
mode: "note-links",
|
||||
edge: Object.assign({}, edge)
|
||||
};
|
||||
}
|
||||
|
||||
validatePinCandidate(candidate, index, graph) {
|
||||
if (!candidate) return null;
|
||||
if (candidate.kind === "node") {
|
||||
const node = graphNode(index, graph, candidate.nodeId);
|
||||
return node ? this.createNodePinCandidate(node, candidate.mode) : null;
|
||||
}
|
||||
if (candidate.kind === "link" && candidate.source !== null && candidate.source !== undefined && candidate.target !== null && candidate.target !== undefined) {
|
||||
const edge = this.findGraphEdgeForPin(candidate, graph) || candidate.edge || {
|
||||
id: candidate.edgeId,
|
||||
type: "visible-link",
|
||||
source: candidate.source,
|
||||
target: candidate.target,
|
||||
weight: 1
|
||||
};
|
||||
return this.createLinkPinCandidate(index, graph, edge);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pinCandidateKey(candidate) {
|
||||
if (!candidate) return "";
|
||||
if (candidate.kind === "node") return `node:${candidate.nodeId}:${normalizeHoverHighlightMode(candidate.mode)}`;
|
||||
return `link:${candidate.edgeId || `${candidate.source}->${candidate.target}`}`;
|
||||
}
|
||||
|
||||
addPinnedPath(candidate) {
|
||||
const key = this.pinCandidateKey(candidate);
|
||||
if (!key) return null;
|
||||
const existing = this.pinnedPaths.find(pin => pin.key === key);
|
||||
if (existing) {
|
||||
this.selectedPinIds.add(existing.id);
|
||||
new Notice("That path is already pinned.");
|
||||
return existing;
|
||||
}
|
||||
|
||||
const pin = Object.assign({}, candidate, {
|
||||
id: `pin-${this.nextPinId++}`,
|
||||
key,
|
||||
groupId: null,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
this.pinnedPaths.push(pin);
|
||||
this.selectedPinIds.add(pin.id);
|
||||
return pin;
|
||||
}
|
||||
|
||||
pinCurrentHoverPath() {
|
||||
const { index, graph } = this.lastCanvasBundle || {};
|
||||
if (!index || !graph) return;
|
||||
const freshCandidate = this.currentHoverPinCandidate(index, graph);
|
||||
const cachedCandidate = performance.now() <= this.lastHoverPinCandidateExpiresAt
|
||||
? this.validatePinCandidate(this.lastHoverPinCandidate, index, graph)
|
||||
: null;
|
||||
const pin = this.addPinnedPath(freshCandidate || cachedCandidate);
|
||||
if (!pin) {
|
||||
new Notice("Hover a path before pinning.");
|
||||
return;
|
||||
}
|
||||
this.state.sidePage = "pins";
|
||||
this.render();
|
||||
}
|
||||
|
||||
removePinnedPath(pinId) {
|
||||
this.pinnedPaths = this.pinnedPaths.filter(pin => pin.id !== pinId);
|
||||
this.selectedPinIds.delete(pinId);
|
||||
this.dropEmptyPinGroups();
|
||||
this.render();
|
||||
}
|
||||
|
||||
clearPinnedPaths() {
|
||||
this.pinnedPaths = [];
|
||||
this.pinGroups = [];
|
||||
this.selectedPinIds.clear();
|
||||
this.render();
|
||||
}
|
||||
|
||||
groupSelectedPinnedPaths() {
|
||||
const selectedPins = this.pinnedPaths.filter(pin => this.selectedPinIds.has(pin.id));
|
||||
if (!selectedPins.length) {
|
||||
new Notice("Select pinned paths to group.");
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = `pin-group-${this.nextPinGroupId++}`;
|
||||
const name = this.pinGroupName.trim() || `Group ${this.pinGroups.length + 1}`;
|
||||
this.pinGroups.push({ id: groupId, name });
|
||||
for (const pin of selectedPins) pin.groupId = groupId;
|
||||
this.dropEmptyPinGroups();
|
||||
this.pinGroupName = "";
|
||||
this.selectedPinIds.clear();
|
||||
|
||||
if (!this.state.fullscreen && this.lastCanvasBundle) {
|
||||
this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph);
|
||||
}
|
||||
}
|
||||
|
||||
removePinGroup(groupId) {
|
||||
this.pinGroups = this.pinGroups.filter(group => group.id !== groupId);
|
||||
for (const pin of this.pinnedPaths) {
|
||||
if (pin.groupId === groupId) pin.groupId = null;
|
||||
}
|
||||
if (!this.state.fullscreen && this.lastCanvasBundle) {
|
||||
this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph);
|
||||
}
|
||||
}
|
||||
|
||||
ungroupPinnedPath(pinId) {
|
||||
const pin = this.pinnedPaths.find(item => item.id === pinId);
|
||||
if (!pin) return;
|
||||
pin.groupId = null;
|
||||
this.dropEmptyPinGroups();
|
||||
if (!this.state.fullscreen && this.lastCanvasBundle) {
|
||||
this.renderSidePanel(this.lastCanvasBundle.index, this.lastCanvasBundle.graph);
|
||||
}
|
||||
}
|
||||
|
||||
dropEmptyPinGroups() {
|
||||
const usedGroups = new Set(this.pinnedPaths.map(pin => pin.groupId).filter(Boolean));
|
||||
this.pinGroups = this.pinGroups.filter(group => usedGroups.has(group.id));
|
||||
}
|
||||
|
||||
findGraphEdgeForPin(pin, graph) {
|
||||
if (!pin || pin.kind !== "link" || !graph) return null;
|
||||
const edges = [
|
||||
...(graph.linkEdges || []),
|
||||
...(graph.hoverLinkEdges || [])
|
||||
];
|
||||
return edges.find(edge =>
|
||||
(pin.edgeId && edge.id === pin.edgeId)
|
||||
|| (edge.source === pin.source && edge.target === pin.target)
|
||||
) || null;
|
||||
}
|
||||
|
||||
findCanvasEdgeForPin(pin) {
|
||||
if (!pin || pin.kind !== "link" || !this.canvasData) return null;
|
||||
if (pin.edgeId && this.canvasData.edgesById.has(pin.edgeId)) {
|
||||
return this.canvasData.edgesById.get(pin.edgeId);
|
||||
}
|
||||
const collections = [
|
||||
...(this.canvasData.links || []),
|
||||
...(this.canvasData.hoverLinks || []),
|
||||
...(this.canvasData.hierarchy || [])
|
||||
];
|
||||
return collections.find(item =>
|
||||
item && item.source === pin.source && item.target === pin.target
|
||||
) || null;
|
||||
}
|
||||
|
||||
centerPinnedPath(pin) {
|
||||
if (!pin || !this.canvas || !this.canvasData) return;
|
||||
if (pin.kind === "node") {
|
||||
const nodeId = this.plugin.index.visualNodeId(pin.nodeId);
|
||||
const item = this.canvasData.nodesById.get(nodeId);
|
||||
if (item) this.centerCanvasOnPoint(item.point);
|
||||
return;
|
||||
}
|
||||
|
||||
const edge = this.findCanvasEdgeForPin(pin);
|
||||
if (!edge || !edge.sourcePoint || !edge.targetPoint) return;
|
||||
this.centerCanvasOnPoint({
|
||||
x: (edge.sourcePoint.x + edge.targetPoint.x) / 2,
|
||||
y: (edge.sourcePoint.y + edge.targetPoint.y) / 2
|
||||
});
|
||||
}
|
||||
|
||||
centerCanvasOnPoint(point) {
|
||||
if (!point || !this.canvas) return;
|
||||
const viewport = this.canvasViewport();
|
||||
const zoom = clampFloat(this.state.zoom, this.canvasMinZoom(viewport), this.canvasMaxZoom(viewport), 1);
|
||||
this.canvasPanX = viewport.width / 2 - point.x * zoom;
|
||||
this.canvasPanY = viewport.height / 2 - point.y * zoom;
|
||||
this.saveViewportState();
|
||||
this.requestCanvasDraw("full");
|
||||
}
|
||||
|
||||
inspectPinnedPath(pin, index, graph) {
|
||||
if (!pin) return;
|
||||
if (pin.kind === "node") {
|
||||
this.state.sidePage = "inspect";
|
||||
this.selectedNodeId = this.plugin.index.visualNodeId(pin.nodeId);
|
||||
this.selectedLink = null;
|
||||
this.applyPersistentHighlight();
|
||||
if (!this.state.fullscreen) this.renderSidePanel(index, graph, this.selectedNodeId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.sidePage = "inspect";
|
||||
this.selectedLink = this.findGraphEdgeForPin(pin, graph) || pin.edge || null;
|
||||
this.selectedNodeId = null;
|
||||
this.applyPersistentHighlight();
|
||||
if (!this.state.fullscreen) this.renderSidePanel(index, graph);
|
||||
}
|
||||
|
||||
pinnedCanvasLabelIds() {
|
||||
if (!this.canvasData) return [];
|
||||
const ids = new Set();
|
||||
for (const pin of this.pinnedPaths) {
|
||||
if (pin.kind === "node") {
|
||||
const nodeId = this.plugin.index.visualNodeId(pin.nodeId);
|
||||
if (this.canvasData.nodesById.has(nodeId)) ids.add(nodeId);
|
||||
continue;
|
||||
}
|
||||
const edge = this.findCanvasEdgeForPin(pin);
|
||||
if (!edge) continue;
|
||||
if (edge.source !== null && edge.source !== undefined && this.canvasData.nodesById.has(edge.source)) ids.add(edge.source);
|
||||
if (edge.target !== null && edge.target !== undefined && this.canvasData.nodesById.has(edge.target)) ids.add(edge.target);
|
||||
}
|
||||
return Array.from(ids);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.syncColorSchemeClass();
|
||||
const index = this.plugin.index;
|
||||
|
|
@ -1397,7 +1674,8 @@ class MiniWorldMapView extends ItemView {
|
|||
const started = performance.now();
|
||||
const graphState = Object.assign({}, this.state, {
|
||||
selectedNodeId: this.selectedNodeId,
|
||||
selectedLink: this.selectedLink
|
||||
selectedLink: this.selectedLink,
|
||||
pinNeedsHoverLinks: this.pinnedPathsNeedHoverLinks()
|
||||
});
|
||||
const graph = index.buildVisibleGraph(graphState);
|
||||
if (this.applyPreLayoutPressureGuard(graph)) return;
|
||||
|
|
@ -1790,7 +2068,7 @@ class MiniWorldMapView extends ItemView {
|
|||
layout,
|
||||
query,
|
||||
this.plugin.nativeGraphSettings || DEFAULT_NATIVE_GRAPH_SETTINGS,
|
||||
{ includeHoverLinks: hoverHighlightsNoteLinks(this.state.hoverHighlightMode) }
|
||||
{ includeHoverLinks: this.needsCanvasHoverLinks() }
|
||||
);
|
||||
this.lastCanvasBundle = { index, graph, layout };
|
||||
|
||||
|
|
@ -1869,6 +2147,17 @@ class MiniWorldMapView extends ItemView {
|
|||
this.resetToVaultRoot();
|
||||
});
|
||||
|
||||
const pinButton = controls.createEl("button", {
|
||||
cls: "mwm-floating-button",
|
||||
attr: { type: "button", "aria-label": "Pin hover", title: "Pin hover" }
|
||||
});
|
||||
setIcon(pinButton, "pin");
|
||||
pinButton.addEventListener("click", evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.pinCurrentHoverPath();
|
||||
});
|
||||
|
||||
const fullscreenButton = controls.createEl("button", {
|
||||
cls: "mwm-floating-button",
|
||||
attr: {
|
||||
|
|
@ -2027,6 +2316,7 @@ class MiniWorldMapView extends ItemView {
|
|||
canvas.setPointerCapture?.(evt.pointerId);
|
||||
this.hoverNodeId = hit.node.node.id;
|
||||
this.hoverLink = null;
|
||||
this.rememberHoverPinCandidate(this.lastCanvasBundle?.index, this.lastCanvasBundle?.graph);
|
||||
this.graphHost.classList.add("is-panning", "is-pointing");
|
||||
this.requestCanvasDraw("interactive");
|
||||
return;
|
||||
|
|
@ -2380,6 +2670,10 @@ class MiniWorldMapView extends ItemView {
|
|||
this.hoverLink = hasNodeHover ? null : nextLink;
|
||||
this.graphHost.classList.toggle("is-hovering", hasNodeHover || Boolean(this.hoverLink));
|
||||
this.graphHost.classList.toggle("is-pointing", hasNodeHover || Boolean(this.hoverLink));
|
||||
if (hasNodeHover || this.hoverLink) {
|
||||
const { index, graph } = this.lastCanvasBundle || {};
|
||||
this.rememberHoverPinCandidate(index, graph);
|
||||
}
|
||||
if (this.canvasNeedsFastDraw()) {
|
||||
this.requestCanvasDraw("interactive");
|
||||
this.scheduleSettledCanvasDraw(180);
|
||||
|
|
@ -2672,28 +2966,68 @@ class MiniWorldMapView extends ItemView {
|
|||
const relatedNodes = new Set();
|
||||
const highlightedEdges = new Set();
|
||||
const labelNodes = new Set();
|
||||
const pinnedNodeIds = new Set();
|
||||
let hasPinnedActive = false;
|
||||
let hasActiveLink = false;
|
||||
|
||||
if (activeNodeId !== null && activeNodeId !== undefined) {
|
||||
relatedNodes.add(activeNodeId);
|
||||
labelNodes.add(activeNodeId);
|
||||
if (hoverHighlightsNoteLinks(hoverMode)) {
|
||||
for (const edge of data.linkEdgesByNode.get(activeNodeId) || []) {
|
||||
const addNodeHighlight = (nodeId, mode, pinned = false) => {
|
||||
let visualNodeId = this.plugin.index.visualNodeId(nodeId);
|
||||
if (visualNodeId !== null && visualNodeId !== undefined && !data.nodesById.has(visualNodeId)) visualNodeId = null;
|
||||
if (visualNodeId === null || visualNodeId === undefined) return null;
|
||||
relatedNodes.add(visualNodeId);
|
||||
labelNodes.add(visualNodeId);
|
||||
if (pinned) pinnedNodeIds.add(visualNodeId);
|
||||
if (hoverHighlightsNoteLinks(mode)) {
|
||||
for (const edge of data.linkEdgesByNode.get(visualNodeId) || []) {
|
||||
highlightedEdges.add(edge.key);
|
||||
if (edge.source !== null && edge.source !== undefined) relatedNodes.add(edge.source);
|
||||
if (edge.target !== null && edge.target !== undefined) relatedNodes.add(edge.target);
|
||||
if (edge.source !== null && edge.source !== undefined) labelNodes.add(edge.source);
|
||||
if (edge.target !== null && edge.target !== undefined) labelNodes.add(edge.target);
|
||||
}
|
||||
}
|
||||
addHierarchyHoverHighlights(data, activeNodeId, hoverMode, relatedNodes, highlightedEdges, labelNodes);
|
||||
addHierarchyHoverHighlights(data, visualNodeId, mode, relatedNodes, highlightedEdges, labelNodes);
|
||||
return visualNodeId;
|
||||
};
|
||||
|
||||
const addLinkHighlight = (edgeId, fallbackEdge) => {
|
||||
let edge = edgeId ? data.edgesById.get(edgeId) : null;
|
||||
if (!edge && fallbackEdge) {
|
||||
edge = this.findCanvasEdgeForPin({
|
||||
kind: "link",
|
||||
edgeId: fallbackEdge.id,
|
||||
source: fallbackEdge.source,
|
||||
target: fallbackEdge.target
|
||||
});
|
||||
}
|
||||
if (!edge) return null;
|
||||
highlightedEdges.add(edge.key);
|
||||
if (edge.source !== null && edge.source !== undefined) {
|
||||
relatedNodes.add(edge.source);
|
||||
labelNodes.add(edge.source);
|
||||
pinnedNodeIds.add(edge.source);
|
||||
}
|
||||
if (edge.target !== null && edge.target !== undefined) {
|
||||
relatedNodes.add(edge.target);
|
||||
labelNodes.add(edge.target);
|
||||
pinnedNodeIds.add(edge.target);
|
||||
}
|
||||
return edge.key;
|
||||
};
|
||||
|
||||
if (activeNodeId !== null && activeNodeId !== undefined) {
|
||||
addNodeHighlight(activeNodeId, hoverMode, false);
|
||||
}
|
||||
|
||||
if (activeLinkId) {
|
||||
const edge = data.edgesById.get(activeLinkId);
|
||||
if (edge) {
|
||||
highlightedEdges.add(edge.key);
|
||||
relatedNodes.add(edge.source);
|
||||
relatedNodes.add(edge.target);
|
||||
labelNodes.add(edge.source);
|
||||
labelNodes.add(edge.target);
|
||||
if (activeLinkId || this.hoverLink || this.selectedLink) {
|
||||
hasActiveLink = addLinkHighlight(activeLinkId, this.hoverLink || this.selectedLink) !== null;
|
||||
}
|
||||
|
||||
for (const pin of this.pinnedPaths) {
|
||||
if (pin.kind === "node") {
|
||||
if (addNodeHighlight(pin.nodeId, pin.mode, true) !== null) hasPinnedActive = true;
|
||||
} else if (pin.kind === "link") {
|
||||
if (addLinkHighlight(pin.edgeId, pin.edge || pin) !== null) hasPinnedActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2702,12 +3036,13 @@ class MiniWorldMapView extends ItemView {
|
|||
}
|
||||
|
||||
return {
|
||||
hasActive: activeNodeId !== null && activeNodeId !== undefined || Boolean(activeLinkId),
|
||||
hasActive: activeNodeId !== null && activeNodeId !== undefined || hasActiveLink || hasPinnedActive,
|
||||
activeNodeId,
|
||||
activeLinkId,
|
||||
relatedNodes,
|
||||
highlightedEdges,
|
||||
labelNodes
|
||||
labelNodes,
|
||||
pinnedNodeIds
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2737,11 +3072,13 @@ class MiniWorldMapView extends ItemView {
|
|||
currentNodeIds.add(nodeId);
|
||||
const isFocused = nodeId === active.activeNodeId
|
||||
|| nodeId === this.selectedNodeId
|
||||
|| active.pinnedNodeIds.has(nodeId)
|
||||
|| nodeId === this.lastCanvasBundle.graph.focusId
|
||||
|| item.searchMatch;
|
||||
const isRelated = active.relatedNodes.has(nodeId);
|
||||
const directLabel = nodeId === active.activeNodeId
|
||||
|| nodeId === this.selectedNodeId
|
||||
|| active.pinnedNodeIds.has(nodeId)
|
||||
|| item.searchMatch;
|
||||
const explicitLabel = directLabel || (!hoverOnlyLabels && active.labelNodes.has(nodeId));
|
||||
const target = {
|
||||
|
|
@ -3105,7 +3442,7 @@ class MiniWorldMapView extends ItemView {
|
|||
? (this.canvasData?.interactiveLabelItems || nodes)
|
||||
: (this.canvasData?.labelItems || nodes);
|
||||
if (interactive) {
|
||||
const extraIds = [this.selectedNodeId, this.hoverNodeId, this.lastCanvasBundle?.graph?.focusId]
|
||||
const extraIds = [this.selectedNodeId, this.hoverNodeId, this.lastCanvasBundle?.graph?.focusId, ...this.pinnedCanvasLabelIds()]
|
||||
.filter(id => id !== null && id !== undefined);
|
||||
if (extraIds.length) {
|
||||
const seen = new Set(labelItems.map(item => item.node.id));
|
||||
|
|
@ -3116,9 +3453,17 @@ class MiniWorldMapView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
const pinnedLabelIds = new Set(this.pinnedCanvasLabelIds());
|
||||
for (const item of labelItems) {
|
||||
if (bounds && !circleInBounds(item.point, item.radius + 180 / zoom, bounds)) continue;
|
||||
if (interactive && !item.searchMatch && item.labelRank > FAST_CANVAS_LABEL_LIMIT && item.node.id !== this.selectedNodeId && item.node.id !== this.hoverNodeId) continue;
|
||||
if (
|
||||
interactive
|
||||
&& !item.searchMatch
|
||||
&& item.labelRank > FAST_CANVAS_LABEL_LIMIT
|
||||
&& item.node.id !== this.selectedNodeId
|
||||
&& item.node.id !== this.hoverNodeId
|
||||
&& !pinnedLabelIds.has(item.node.id)
|
||||
) continue;
|
||||
const visual = visuals.nodes.get(item.node.id) || { label: 0, focus: 0 };
|
||||
if (visual.label <= 0.02) continue;
|
||||
if (fastInteractive && visual.focus <= 0.02 && !item.searchMatch && item.labelRank > 36) continue;
|
||||
|
|
@ -3365,6 +3710,7 @@ class MiniWorldMapView extends ItemView {
|
|||
this.sidePanelContent = content;
|
||||
try {
|
||||
if (this.state.sidePage === "view") this.renderViewPage(index, graph);
|
||||
else if (this.state.sidePage === "pins") this.renderPinsPage(index, graph);
|
||||
else if (this.state.sidePage === "controls") this.renderControlsPage(index, graph);
|
||||
else if (this.state.sidePage === "defaults") this.renderDefaultsPage(index, graph);
|
||||
else this.renderInspectPage(index, graph, forcedNodeId);
|
||||
|
|
@ -3459,6 +3805,105 @@ class MiniWorldMapView extends ItemView {
|
|||
this.createPanelAction(commandGrid, "maximize-2", "Full screen", () => this.toggleFullscreen(), this.state.fullscreen);
|
||||
}
|
||||
|
||||
renderPinsPage(index, graph) {
|
||||
const panel = this.getSidePanelTarget();
|
||||
this.createPanelPageTitle(panel, "Pins");
|
||||
|
||||
const actions = this.createPanelSection(panel, "Actions");
|
||||
const actionGrid = actions.createDiv({ cls: "mwm-panel-action-grid" });
|
||||
this.createPanelAction(actionGrid, "pin", "Pin hover", () => this.pinCurrentHoverPath());
|
||||
const clearButton = this.createPanelAction(actionGrid, "trash-2", "Clear pins", () => this.clearPinnedPaths());
|
||||
clearButton.disabled = this.pinnedPaths.length === 0;
|
||||
|
||||
const grouping = this.createPanelSection(panel, "Group");
|
||||
this.pinGroupInput = this.createPanelText(
|
||||
grouping,
|
||||
"pin-group-name",
|
||||
"Name",
|
||||
this.pinGroupName,
|
||||
"Group name",
|
||||
value => {
|
||||
this.pinGroupName = value;
|
||||
}
|
||||
);
|
||||
const groupGrid = grouping.createDiv({ cls: "mwm-panel-action-grid" });
|
||||
const groupButton = this.createPanelAction(groupGrid, "folder-plus", "Group selected", () => this.groupSelectedPinnedPaths());
|
||||
groupButton.disabled = this.selectedPinIds.size === 0;
|
||||
|
||||
const listSection = this.createPanelSection(panel, `Pinned Paths (${this.pinnedPaths.length})`);
|
||||
if (!this.pinnedPaths.length) {
|
||||
listSection.createDiv({ cls: "mwm-side-muted", text: "No pinned paths." });
|
||||
return;
|
||||
}
|
||||
|
||||
const groupsById = new Map(this.pinGroups.map(group => [group.id, group]));
|
||||
const pinsByGroup = new Map();
|
||||
const ungrouped = [];
|
||||
for (const pin of this.pinnedPaths) {
|
||||
if (!pin.groupId || !groupsById.has(pin.groupId)) {
|
||||
ungrouped.push(pin);
|
||||
continue;
|
||||
}
|
||||
if (!pinsByGroup.has(pin.groupId)) pinsByGroup.set(pin.groupId, []);
|
||||
pinsByGroup.get(pin.groupId).push(pin);
|
||||
}
|
||||
|
||||
if (ungrouped.length) {
|
||||
this.renderPinnedPathGroup(listSection, index, graph, null, ungrouped);
|
||||
}
|
||||
for (const group of this.pinGroups) {
|
||||
const pins = pinsByGroup.get(group.id) || [];
|
||||
if (pins.length) this.renderPinnedPathGroup(listSection, index, graph, group, pins);
|
||||
}
|
||||
}
|
||||
|
||||
renderPinnedPathGroup(parent, index, graph, group, pins) {
|
||||
const wrapper = parent.createDiv({ cls: "mwm-pin-group" });
|
||||
const header = wrapper.createDiv({ cls: "mwm-pin-group-header" });
|
||||
const title = header.createDiv({ cls: "mwm-pin-group-title" });
|
||||
title.createSpan({ text: group ? group.name : "Ungrouped" });
|
||||
title.createSpan({ cls: "mwm-pin-count", text: String(pins.length) });
|
||||
if (group) {
|
||||
this.createIconButton(header, "folder-minus", "Ungroup all", () => this.removePinGroup(group.id), "mwm-pin-icon-button");
|
||||
}
|
||||
|
||||
const list = wrapper.createDiv({ cls: "mwm-pin-list" });
|
||||
for (const pin of pins) {
|
||||
this.renderPinnedPathItem(list, index, graph, pin);
|
||||
}
|
||||
}
|
||||
|
||||
renderPinnedPathItem(parent, index, graph, pin) {
|
||||
const row = parent.createDiv({ cls: "mwm-pin-row" });
|
||||
const checkbox = row.createEl("input", {
|
||||
cls: "mwm-pin-check",
|
||||
attr: { type: "checkbox", title: "Select for grouping", "aria-label": "Select for grouping" }
|
||||
});
|
||||
checkbox.checked = this.selectedPinIds.has(pin.id);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) this.selectedPinIds.add(pin.id);
|
||||
else this.selectedPinIds.delete(pin.id);
|
||||
this.renderSidePanel(index, graph);
|
||||
});
|
||||
|
||||
const main = row.createEl("button", {
|
||||
cls: "mwm-pin-main",
|
||||
attr: { type: "button", title: pin.path || pin.title }
|
||||
});
|
||||
main.addEventListener("click", () => this.centerPinnedPath(pin));
|
||||
main.createDiv({ cls: "mwm-pin-title", text: pin.title || "Pinned path" });
|
||||
const kind = pin.kind === "link" ? "link" : hoverHighlightModeLabel(pin.mode);
|
||||
main.createDiv({ cls: "mwm-pin-meta", text: `${kind} - ${pin.path || "/"}` });
|
||||
|
||||
const actions = row.createDiv({ cls: "mwm-pin-actions" });
|
||||
this.createIconButton(actions, "locate-fixed", "Locate", () => this.centerPinnedPath(pin), "mwm-pin-icon-button");
|
||||
this.createIconButton(actions, "info", "Inspect", () => this.inspectPinnedPath(pin, index, graph), "mwm-pin-icon-button");
|
||||
if (pin.groupId) {
|
||||
this.createIconButton(actions, "folder-minus", "Ungroup", () => this.ungroupPinnedPath(pin.id), "mwm-pin-icon-button");
|
||||
}
|
||||
this.createIconButton(actions, "x", "Remove", () => this.removePinnedPath(pin.id), "mwm-pin-icon-button");
|
||||
}
|
||||
|
||||
renderControlsPage(index, graph) {
|
||||
const panel = this.getSidePanelTarget();
|
||||
this.createPanelPageTitle(panel, "Controls");
|
||||
|
|
@ -3920,6 +4365,12 @@ class MiniWorldMapView extends ItemView {
|
|||
}
|
||||
|
||||
const actions = panel.createDiv({ cls: "mwm-side-actions" });
|
||||
this.createIconButton(actions, "pin", "Pin highlighted path", () => {
|
||||
const pin = this.addPinnedPath(this.createNodePinCandidate(node, this.state.hoverHighlightMode));
|
||||
if (!pin) return;
|
||||
this.state.sidePage = "pins";
|
||||
this.render();
|
||||
}, "");
|
||||
if (node.type === "note") {
|
||||
this.createIconButton(actions, "file-text", "Open note", () => this.openNode(node.id), "");
|
||||
this.createIconButton(actions, "locate-fixed", "Focus note", () => {
|
||||
|
|
@ -3996,6 +4447,12 @@ class MiniWorldMapView extends ItemView {
|
|||
}
|
||||
|
||||
const actions = panel.createDiv({ cls: "mwm-side-actions" });
|
||||
this.createIconButton(actions, "pin", "Pin link", () => {
|
||||
const pin = this.addPinnedPath(this.createLinkPinCandidate(index, graph, edge));
|
||||
if (!pin) return;
|
||||
this.state.sidePage = "pins";
|
||||
this.render();
|
||||
}, "");
|
||||
if (source) {
|
||||
const button = actions.createEl("button", { cls: "mwm-text-button", attr: { type: "button" } });
|
||||
button.textContent = "Source";
|
||||
|
|
|
|||
117
styles.css
117
styles.css
|
|
@ -461,7 +461,7 @@
|
|||
|
||||
@container (max-width: 290px) {
|
||||
.mwm-page-tabs {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.mwm-page-tab {
|
||||
|
|
@ -511,6 +511,12 @@
|
|||
border-color: color-mix(in srgb, var(--text-accent) 58%, var(--mwm-border));
|
||||
}
|
||||
|
||||
.mwm-panel-action:disabled,
|
||||
.mwm-icon-button:disabled {
|
||||
opacity: 0.48;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mwm-panel-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
|
@ -645,6 +651,115 @@
|
|||
background: var(--mwm-panel-alt);
|
||||
}
|
||||
|
||||
.mwm-pin-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--mwm-border);
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--mwm-bg) 72%, var(--mwm-panel));
|
||||
}
|
||||
|
||||
.mwm-pin-group + .mwm-pin-group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mwm-pin-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mwm-pin-group-title {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--mwm-text);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mwm-pin-count {
|
||||
min-width: 20px;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--mwm-border);
|
||||
border-radius: 999px;
|
||||
color: var(--mwm-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mwm-pin-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mwm-pin-row {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.mwm-pin-check {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mwm-pin-main {
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--mwm-border);
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--mwm-bg) 78%, var(--mwm-panel));
|
||||
color: var(--mwm-text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mwm-pin-main:hover {
|
||||
background: var(--mwm-panel-alt);
|
||||
}
|
||||
|
||||
.mwm-pin-title,
|
||||
.mwm-pin-meta {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mwm-pin-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mwm-pin-meta {
|
||||
color: var(--mwm-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mwm-pin-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mwm-pin-icon-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.mwm-pin-icon-button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.mwm-edge-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue