Merge pull request #30 from Quorafind/feat/2.0.0

Refactor And Go Ahead: 2.0.0
This commit is contained in:
Boninall 2025-03-05 17:09:39 +08:00 committed by GitHub
commit 60a78514cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1279 additions and 250 deletions

View file

@ -6,22 +6,29 @@ A plugin for [Obsidian](https://obsidian.md) that helps you collapse node in can
## Usage
- Commands
- `Fold All Nodes` - Collapses all nodes in the workspace
- `Expand All Nodes` - Expands all nodes in the workspace
- `Fold Selected Nodes` - Collapses only the selected nodes
- `Expand Selected Nodes` - Expands only the selected nodes
- Context menu in canvas
- `Selection Menu` - Shows the context menu for selected nodes
- `Node Menu` - Shows the context menu for a specific node or nodes
- Direct Click on the Header
- `Click` - Collapse or expand the node
- Commands
- `Fold All Nodes` - Collapses all nodes in the workspace
- `Expand All Nodes` - Expands all nodes in the workspace
- `Fold Selected Nodes` - Collapses only the selected nodes
- `Expand Selected Nodes` - Expands only the selected nodes
- Context menu in canvas
- `Selection Menu` - Shows the context menu for selected nodes
- `Node Menu` - Shows the context menu for a specific node or nodes
- `Set Node Alias` - Set a custom alias for the node (shown in collapsed state)
- `Set Node Thumbnail` - Set a custom thumbnail image for the node (shown in collapsed state)
- `Remove Node Customizations` - Remove alias and thumbnail from the node
- Direct Click on the Header
- `Click` - Collapse or expand the node
- Thumbnails and Aliases in Collapsed State
- Enable in settings to show thumbnails and/or aliases when nodes are collapsed
- For thumbnails: Add `thumbnail: path/to/image.jpg` or `thumbnail: https://example.com/image.jpg` to node metadata, or use the context menu
- For aliases: Add `alias: Your Alias Text` to node metadata, use frontmatter aliases for file nodes, or use the context menu
## Installation
- Not ready for market yet
- Can be installed via the [Brat](https://github.com/TfTHacker/obsidian42-brat) plugin
- Manual installation
- Not ready for market yet
- Can be installed via the [Brat](https://github.com/TfTHacker/obsidian42-brat) plugin
- Manual installation
1. Find the release page on this github page and click
2. Download the latest release zip file
@ -38,4 +45,3 @@ on [https://www.buymeacoffee.com/boninall](https://www.buymeacoffee.com/boninall
.
<a href="https://www.buymeacoffee.com/boninall"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=boninall&button_colour=6495ED&font_colour=ffffff&font_family=Lato&outline_colour=000000&coffee_colour=FFDD00"></a>

View file

@ -1,42 +1,44 @@
import esbuild from "esbuild";
import process from "process";
import builtins from 'builtin-modules'
import builtins from "builtin-modules";
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === 'production');
const prod = process.argv[2] === "production";
esbuild.build({
banner: {
js: banner,
},
entryPoints: ['src/canvasCollapseIndex.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins],
format: 'cjs',
watch: !prod,
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
}).catch(() => process.exit(1));
esbuild
.build({
banner: {
js: banner,
},
entryPoints: ["src/index.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
watch: !prod,
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
})
.catch(() => process.exit(1));

View file

@ -1,7 +1,7 @@
{
"id": "collapse-node",
"name": "Collapse Node",
"version": "1.1.1",
"version": "2.0.0",
"minAppVersion": "1.1.0",
"description": "Collapse node in canvas.",
"author": "Boninall",

View file

@ -1,6 +1,6 @@
{
"name": "collapse-node",
"version": "1.1.1",
"version": "2.0.0",
"description": "A plugin for collapse node in canvas",
"main": "main.js",
"scripts": {
@ -17,7 +17,7 @@
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.14.47",
"obsidian": "latest",
"obsidian": "^1.8.7",
"tslib": "2.4.0",
"typescript": "4.7.4"
},

View file

@ -34,8 +34,8 @@ importers:
specifier: 0.14.47
version: 0.14.47
obsidian:
specifier: latest
version: 1.4.4(@codemirror/state@6.4.1)(@codemirror/view@6.28.1)
specifier: ^1.8.7
version: 1.8.7(@codemirror/state@6.4.1)(@codemirror/view@6.28.1)
tslib:
specifier: 2.4.0
version: 2.4.0
@ -409,6 +409,7 @@ packages:
eslint@8.48.0:
resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@9.6.1:
@ -595,8 +596,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
obsidian@1.4.4:
resolution: {integrity: sha512-q2V5GNT/M40uYOENdVw5kovPSoaO6vppiiyBCkIqWgKp4oN654jA/GQ0OaNBA7p5NdfS245QCeRgCFQ42wOZiw==}
obsidian@1.8.7:
resolution: {integrity: sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==}
peerDependencies:
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
@ -1299,7 +1300,7 @@ snapshots:
natural-compare@1.4.0: {}
obsidian@1.4.4(@codemirror/state@6.4.1)(@codemirror/view@6.28.1):
obsidian@1.8.7(@codemirror/state@6.4.1)(@codemirror/view@6.28.1):
dependencies:
'@codemirror/state': 6.4.1
'@codemirror/view': 6.28.1

View file

@ -1,38 +1,49 @@
import {
CanvasFileNode,
CanvasGroupNode,
CanvasLinkNode,
CanvasNode,
CanvasTextNode,
type CanvasFileNode,
type CanvasGroupNode,
type CanvasLinkNode,
type CanvasNode,
type CanvasTextNode,
Component,
setIcon
parseFrontMatterAliases,
setIcon,
} from "obsidian";
import { HeaderComponent } from "./types/custom";
import { CanvasData, CanvasNodeData } from "obsidian/canvas";
import CanvasCollapsePlugin from "./canvasCollapseIndex";
import type { HeaderComponent } from "./types/custom";
import type CanvasCollapsePlugin from ".";
import type { AllCanvasNodeData } from "obsidian/canvas";
export default class CollapseControlHeader extends Component implements HeaderComponent {
private collapsed: boolean = false;
export default class CollapseControlHeader
extends Component
implements HeaderComponent
{
private collapsed = false;
private collapsedIconEl: HTMLDivElement;
private typeIconEl: HTMLDivElement;
private titleEl: HTMLSpanElement;
private headerEl: HTMLElement;
private thumbnailEl: HTMLImageElement;
private aliasEl: HTMLSpanElement;
private content: string = "";
private content = "";
private node: CanvasNode;
private alias = "";
private thumbnailUrl = "";
private refreshed: boolean = false;
private containingNodes: any[] = [];
private refreshed = false;
private containingNodes: CanvasNode[] = [];
plugin: CanvasCollapsePlugin;
oldFilePath: string = "";
oldFilePath = "";
constructor(plugin: CanvasCollapsePlugin, node: CanvasNode) {
super();
this.plugin = plugin;
this.node = node;
this.collapsed = node.unknownData.collapsed === undefined ? false : node.unknownData.collapsed;
this.collapsed =
node.unknownData.collapsed === undefined
? false
: node.unknownData.collapsed;
}
onload() {
@ -42,20 +53,20 @@ export default class CollapseControlHeader extends Component implements HeaderCo
this.updateNodesInGroup();
this.updateNode();
this.registerEvent(this.plugin.app.vault.on('rename', (file, oldPath) => {
if (oldPath === this.oldFilePath) {
this.titleEl.setText(file.name.split(".")[0]);
this.oldFilePath = file.path;
}
}));
this.registerEvent(
this.plugin.app.vault.on("rename", (file, oldPath) => {
if (oldPath === this.oldFilePath) {
this.titleEl.setText(file.name.split(".")[0]);
this.oldFilePath = file.path;
}
})
);
return this.headerEl;
}
onunload() {
super.onunload();
}
unload() {
@ -67,7 +78,7 @@ export default class CollapseControlHeader extends Component implements HeaderCo
initHeader() {
this.headerEl = createEl("div", {
cls: "canvas-node-collapse-control"
cls: "canvas-node-collapse-control",
});
this.registerDomEvent(this.headerEl, "click", async (evt) => {
evt.preventDefault();
@ -76,15 +87,23 @@ export default class CollapseControlHeader extends Component implements HeaderCo
});
this.collapsedIconEl = this.headerEl.createEl("div", {
cls: "canvas-node-collapse-control-icon"
cls: "canvas-node-collapse-control-icon",
});
this.typeIconEl = this.headerEl.createEl("div", {
cls: "canvas-node-type-icon"
cls: "canvas-node-type-icon",
});
this.titleEl = this.headerEl.createEl("span", {
cls: "canvas-node-collapse-control-title"
cls: "canvas-node-collapse-control-title",
});
this.thumbnailEl = this.node.nodeEl.createEl("img", {
cls: "canvas-node-collapse-control-thumbnail",
});
this.aliasEl = this.headerEl.createEl("span", {
cls: "canvas-node-collapse-control-alias",
});
}
@ -98,31 +117,163 @@ export default class CollapseControlHeader extends Component implements HeaderCo
initContent() {
this.setIconOrContent("setContent");
this.titleEl.setText(this.content?.replace(/^\#{1,} /g, ''));
this.titleEl.setText(this.content?.replace(/^\#{1,} /g, ""));
this.initAlias();
this.initThumbnail();
}
initAlias() {
// Try to get alias from node metadata
if (this.node.unknownData?.alias) {
this.alias = this.node.unknownData.alias;
} else {
// For file nodes, try to get alias from frontmatter
const fileNode = this.node as CanvasFileNode;
if (fileNode.file && this.plugin.app.metadataCache) {
try {
const meta = this.plugin.app.metadataCache.getFileCache(
fileNode.file
);
if (meta?.frontmatter) {
const aliases = parseFrontMatterAliases(
meta.frontmatter
);
if (aliases && aliases.length > 0) {
this.alias = aliases[0];
}
}
} catch (e) {
console.debug("Error getting alias:", e);
}
}
}
if (this.alias) {
this.aliasEl.setText(this.alias);
} else {
this.aliasEl.detach();
}
}
updateNodeAlias(newAlias: string) {
this.alias = newAlias;
if (this.alias) {
// Re-attach if it was detached
if (!this.aliasEl.parentElement) {
this.headerEl.appendChild(this.aliasEl);
}
this.aliasEl.setText(this.alias);
} else {
this.aliasEl.detach();
}
}
initThumbnail() {
// Try to get thumbnail from node metadata
if (this.node.unknownData?.thumbnail) {
this.thumbnailUrl = this.node.unknownData.thumbnail;
} else {
// For file nodes, try to get thumbnail from frontmatter
const fileNode = this.node as CanvasFileNode;
if (fileNode.file && this.plugin.app.metadataCache) {
try {
const meta = this.plugin.app.metadataCache.getFileCache(
fileNode.file
);
if (meta?.frontmatter?.thumbnail) {
this.thumbnailUrl = meta.frontmatter.thumbnail;
}
} catch (e) {
console.debug("Error getting thumbnail:", e);
}
}
}
if (this.thumbnailUrl) {
try {
// Handle both absolute and relative paths
const url = this.thumbnailUrl.startsWith("http")
? this.thumbnailUrl
: this.plugin.app.vault.adapter.getResourcePath(
this.thumbnailUrl
);
this.thumbnailEl.src = url;
} catch (e) {
console.debug("Error setting thumbnail src:", e);
this.thumbnailEl.detach();
}
} else {
this.thumbnailEl.detach();
}
}
updateNodeThumbnail(newThumbnailUrl: string) {
this.thumbnailUrl = newThumbnailUrl;
if (this.thumbnailUrl) {
try {
// Re-attach if it was detached
if (!this.thumbnailEl.parentElement) {
this.headerEl.appendChild(this.thumbnailEl);
}
// Handle both absolute and relative paths
const url = this.thumbnailUrl.startsWith("http")
? this.thumbnailUrl
: this.plugin.app.vault.adapter.getResourcePath(
this.thumbnailUrl
);
this.thumbnailEl.src = url;
} catch (e) {
console.debug("Error setting thumbnail src:", e);
this.thumbnailEl.detach();
}
} else {
this.thumbnailEl.detach();
}
}
setIconOrContent(action: "setIcon" | "setContent") {
const currentType = this.checkNodeType();
switch (currentType) {
case "text":
if (action === "setIcon") setIcon(this.typeIconEl, "sticky-note");
if (action === "setContent") this.content = (this.node as CanvasTextNode).text.slice(0, 10) + ((this.node as CanvasTextNode).text.length > 10 ? "..." : "");
if (action === "setIcon")
setIcon(this.typeIconEl, "sticky-note");
if (action === "setContent")
this.content =
(this.node as CanvasTextNode).text.slice(0, 10) +
((this.node as CanvasTextNode).text.length > 10
? "..."
: "");
break;
case "file":
if (action === "setIcon") {
if ((this.node as CanvasFileNode).file.name.split(".")[1].trim() === "md") setIcon(this.typeIconEl, "file-text");
if (
(this.node as CanvasFileNode).file.name
.split(".")[1]
.trim() === "md"
)
setIcon(this.typeIconEl, "file-text");
else setIcon(this.typeIconEl, "file-image");
}
if (action === "setContent") this.content = (this.node as CanvasFileNode).file?.name.split(".")[0];
if (action === "setContent")
this.content = (
this.node as CanvasFileNode
).file?.name.split(".")[0];
this.oldFilePath = (this.node as CanvasFileNode).file?.path;
break;
case "group":
if (action === "setIcon") setIcon(this.typeIconEl, "create-group");
if (action === "setIcon")
setIcon(this.typeIconEl, "create-group");
if (action === "setContent") this.content = "";
break;
case "link":
if (action === "setIcon") setIcon(this.typeIconEl, "link");
if (action === "setContent") this.content = (this.node as CanvasLinkNode).url;
if (action === "setContent")
this.content = (this.node as CanvasLinkNode).url;
break;
}
@ -131,7 +282,6 @@ export default class CollapseControlHeader extends Component implements HeaderCo
}
}
setCollapsed(collapsed: boolean) {
if (this.node.canvas.readonly) return;
if (this.collapsed === collapsed) return;
@ -150,89 +300,220 @@ export default class CollapseControlHeader extends Component implements HeaderCo
const history = this.node.canvas.history;
if (!history || history.data.length === 0) return;
history.data.forEach((data: CanvasData) => {
data.nodes.forEach((node: CanvasNodeData) => {
for (const data of history.data) {
for (const node of data.nodes) {
if (node.id === this.node.id && node?.collapsed === undefined) {
node.collapsed = false;
}
});
});
}
}
this.refreshed = true;
}
async toggleCollapsed() {
if (this.node.canvas.readonly) return;
const wasCollapsed = this.collapsed;
this.collapsed = !this.collapsed;
this.node.unknownData.collapsed = !this.collapsed;
// Update visual state first for animation
this.updateNode();
// Add delay to allow for animation to complete before updating other elements
setTimeout(() => {
// Update nodes in group after a short delay to allow animation to start
this.updateNodesInGroup();
this.updateEdges();
}, 50);
this.node.canvas.requestSave(false, true);
const canvasCurrentData = this.node.canvas.getData();
const nodeData = canvasCurrentData.nodes.find((node: any) => node.id === this.node.id);
const nodeData = canvasCurrentData.nodes.find(
(node: AllCanvasNodeData) => node.id === this.node.id
);
if (nodeData) {
nodeData.collapsed = this.collapsed;
// If this node was collapsed and is now being expanded, move it to the highest z-index
if (wasCollapsed && !this.collapsed) {
// Remove the node from its current position
const nodeIndex = canvasCurrentData.nodes.findIndex(
(node: AllCanvasNodeData) => node.id === this.node.id
);
if (nodeIndex !== -1) {
const removedNode = canvasCurrentData.nodes.splice(
nodeIndex,
1
)[0];
// Add it back at the end of the array (highest z-index)
canvasCurrentData.nodes.push(removedNode);
}
}
this.refreshHistory();
}
setTimeout(() => {
this.node.canvas.setData(canvasCurrentData);
this.node.canvas.requestSave(true);
}, 0);
this.updateNodesInGroup();
this.updateNode();
this.updateEdges();
}, 300); // Increased timeout to match animation duration
}
updateNode() {
this.node.nodeEl.toggleClass("collapsed", this.collapsed);
setIcon(this.collapsedIconEl, this.collapsed ? "chevron-right" : "chevron-down");
setIcon(this.collapsedIconEl, "chevron-down");
this.collapsedIconEl.toggleClass(
["collapsed", "collapse-handler"],
this.collapsed
);
// Handle thumbnails visibility
this.updateThumbnailVisibility();
// Handle aliases visibility
this.updateAliasVisibility();
}
updateThumbnailVisibility() {
if (this.collapsed || this.plugin.settings.showThumbnailsAlways) {
if (
this.plugin.settings.showThumbnailsInCollapsedState &&
this.thumbnailUrl &&
this.thumbnailUrl !== this.node.unknownData.title
) {
this.thumbnailEl.toggleClass("collapsed-node-hidden", false);
// Hide the title if we're showing an alias instead
this.titleEl.toggleClass("collapsed-node-hidden", true);
} else {
this.thumbnailEl.toggleClass("collapsed-node-hidden", true);
// Make sure title is visible if alias isn't shown
this.titleEl.toggleClass("collapsed-node-hidden", false);
}
} else {
this.thumbnailEl.toggleClass("collapsed-node-hidden", true);
}
}
updateAliasVisibility() {
if (this.collapsed || this.plugin.settings.showAliasesAlways) {
if (
this.plugin.settings.showAliasesInCollapsedState &&
this.alias &&
this.alias !== this.node.unknownData.title
) {
this.aliasEl.toggleClass("collapsed-node-hidden", false);
// Hide the title if we're showing an alias instead
this.titleEl.toggleClass("collapsed-node-hidden", true);
} else {
this.aliasEl.toggleClass("collapsed-node-hidden", true);
// Make sure title is visible if alias isn't shown
this.titleEl.toggleClass("collapsed-node-hidden", false);
}
} else {
this.aliasEl.toggleClass("collapsed-node-hidden", true);
}
}
updateEdges() {
this.node.canvas.nodeInteractionLayer.interactionEl.detach();
this.node.canvas.nodeInteractionLayer.render();
const edges = this.node.canvas.getEdgesForNode(this.node);
edges.forEach((edge: any) => {
for (const edge of edges) {
edge.render();
});
}
}
updateNodesInGroup(expandAll?: boolean) {
if (this.node.unknownData.type === "group" || (this.node as CanvasGroupNode).label) {
const nodes = this.node.canvas.getContainingNodes(this.node.getBBox(true));
if (
this.node.unknownData.type === "group" ||
(this.node as CanvasGroupNode).label
) {
const nodes = this.node.canvas.getContainingNodes(
this.node.getBBox(true)
);
if (expandAll) {
this.collapsed = false;
}
// Add animation class to the group node
this.node.nodeEl.toggleClass("animating", true);
// Remove animation class after animation completes
setTimeout(() => {
this.node.nodeEl.toggleClass("animating", false);
}, 300);
if (this.collapsed) {
nodes.filter((node: any) => node.id !== this.node.id).forEach((node: any) => {
const filteredNodes = nodes.filter(
(node: CanvasNode) => node.id !== this.node.id
);
for (const node of filteredNodes) {
this.containingNodes.push(node);
node.nodeEl.toggleClass("group-nodes-collapsed", this.collapsed);
// Add transition class before changing state
node.nodeEl.toggleClass("node-transitioning", true);
// Apply the collapsed state
node.nodeEl.toggleClass(
"group-nodes-collapsed",
this.collapsed
);
// Remove transition class after animation completes
setTimeout(() => {
node.nodeEl.toggleClass("node-transitioning", false);
}, 300);
this.updateEdgesInGroup(node);
});
}
} else {
const otherGroupNodes = nodes.filter((node: any) => node.id !== this.node.id && node.unknownData.type === "group" && node.unknownData.collapsed);
const otherGroupNodes = nodes.filter(
(node: CanvasNode) =>
node.id !== this.node.id &&
node.unknownData.type === "group" &&
node.unknownData.collapsed
);
// Ignore those nodes in collapsed child group
const ignoreNodes: any[] = [];
const ignoreNodes: CanvasNode[] = [];
for (const groupNode of otherGroupNodes) {
const bbox = groupNode.getBBox(true);
const nodesInGroup = this.node.canvas.getContainingNodes(bbox);
nodesInGroup.forEach((childNode: any) => {
const nodesInGroup =
this.node.canvas.getContainingNodes(bbox);
for (const childNode of nodesInGroup) {
if (childNode.id !== groupNode.id) {
ignoreNodes.push(childNode);
}
});
}
}
this.containingNodes.filter((t) => !ignoreNodes.find((k) => k.id === t.id)).forEach((node: any) => {
node.nodeEl.toggleClass("group-nodes-collapsed", this.collapsed);
this.updateEdgesInGroup(node);
});
ignoreNodes.forEach((node: any) => {
const filteredContainingNodes = this.containingNodes.filter(
(t) => !ignoreNodes.find((k) => k.id === t.id)
);
for (const node of filteredContainingNodes) {
// Add transition class before changing state
node.nodeEl.toggleClass("node-transitioning", true);
// Apply the expanded state
node.nodeEl.toggleClass(
"group-nodes-collapsed",
this.collapsed
);
// Remove transition class after animation completes
setTimeout(() => {
node.nodeEl.toggleClass("node-transitioning", false);
}, 300);
this.updateEdgesInGroup(node, true);
}
for (const node of ignoreNodes) {
this.updateEdgesInGroup(node, node.unknownData.collapsed);
});
}
this.containingNodes = [];
}
@ -243,11 +524,23 @@ export default class CollapseControlHeader extends Component implements HeaderCo
updateEdgesInGroup(node: CanvasNode, triggerCollapsed?: boolean) {
const edges = this.node.canvas.getEdgesForNode(node);
edges.forEach((edge: any) => {
edge.labelElement?.wrapperEl?.classList.toggle("group-edges-collapsed", triggerCollapsed || this.collapsed);
edge.lineGroupEl.classList.toggle("group-edges-collapsed", triggerCollapsed || this.collapsed);
edge.lineEndGroupEl?.classList.toggle("group-edges-collapsed", triggerCollapsed || this.collapsed);
edge.lineStartGroupEl?.classList.toggle("group-edges-collapsed", triggerCollapsed || this.collapsed);
});
for (const edge of edges) {
edge.labelElement?.wrapperEl?.classList.toggle(
"group-edges-collapsed",
triggerCollapsed || this.collapsed
);
edge.lineGroupEl.classList.toggle(
"group-edges-collapsed",
triggerCollapsed || this.collapsed
);
edge.lineEndGroupEl?.classList.toggle(
"group-edges-collapsed",
triggerCollapsed || this.collapsed
);
edge.lineStartGroupEl?.classList.toggle(
"group-edges-collapsed",
triggerCollapsed || this.collapsed
);
}
}
}

View file

@ -1,31 +1,16 @@
import {
addIcon,
Canvas,
CanvasCoords,
CanvasGroupNode,
CanvasNode,
CanvasView,
debounce,
editorInfoField,
Menu,
Plugin,
PluginSettingTab,
setIcon,
Setting,
setTooltip,
ViewState,
WorkspaceLeaf,
} from "obsidian";
import { around } from "monkey-around";
import CollapseControlHeader from "./ControlHeader";
import { CanvasData } from "obsidian/canvas";
import {
getSelectionCoords,
handleCanvasMenu,
handleMultiNodesViaNodes,
handleNodeContextMenu,
handleNodesViaCommands,
handleSelectionContextMenu,
handleSingleNode,
handleSelectionContextMenu,
refreshAllCanvasView,
} from "./utils";
import { EditorView, ViewUpdate } from "@codemirror/view";
@ -34,7 +19,6 @@ import {
patchCanvasInteraction,
patchCanvasMenu,
patchCanvasNode,
updateAllNodeWithHeader,
} from "./patchUtils";
interface CollapsableNodeSettings {
@ -48,6 +32,11 @@ interface CollapsableNodeSettings {
interface OtherSettings {
minLineAmount: number;
minimalControlHeader: boolean;
showThumbnailsInCollapsedState: boolean;
showAliasesInCollapsedState: boolean;
showAliasesAlways: boolean;
showThumbnailsAlways: boolean;
hideDefaultNodeTitle: boolean;
}
type CanvasCollapseSettings = CollapsableNodeSettings & OtherSettings;
@ -61,6 +50,11 @@ const DEFAULT_SETTINGS: CanvasCollapseSettings = {
minLineAmount: 0,
minimalControlHeader: false,
showThumbnailsInCollapsedState: false,
showAliasesInCollapsedState: false,
showAliasesAlways: false,
showThumbnailsAlways: false,
hideDefaultNodeTitle: false,
};
const DynamicUpdateControlHeader = (plugin: CanvasCollapsePlugin) => {
@ -108,6 +102,8 @@ const DynamicUpdateControlHeader = (plugin: CanvasCollapsePlugin) => {
(node.containerEl as HTMLDivElement).prepend(
node.headerComponent.onload()
);
node.headerComponent.updateNode();
}
}
}
@ -121,22 +117,26 @@ export default class CanvasCollapsePlugin extends Plugin {
settings: CanvasCollapseSettings;
headerComponents: { [key: string]: CollapseControlHeader[] } = {};
async onload() {
this.loadSettings();
await this.loadSettings();
this.addSettingTab(new CollapseSettingTab(this.app, this));
this.registerCommands();
this.registerCanvasEvents();
this.registerCustomIcons();
aroundCanvasMethods(this);
patchCanvasMenu(this);
patchCanvasInteraction(this);
patchCanvasNode(this);
this.registerEditorExtension([DynamicUpdateControlHeader(this)]);
this.initGlobalCss();
this.app.workspace.onLayoutReady(() => {
aroundCanvasMethods(this);
patchCanvasMenu(this);
patchCanvasInteraction(this);
patchCanvasNode(this);
});
}
onunload() {
@ -208,6 +208,10 @@ export default class CanvasCollapsePlugin extends Plugin {
"minimal-control-header",
this.settings?.minimalControlHeader
);
document.body.toggleClass(
"hide-default-node-title",
this.settings?.hideDefaultNodeTitle
);
}
async loadSettings() {
@ -218,8 +222,17 @@ export default class CanvasCollapsePlugin extends Plugin {
);
}
debounceReloadLeaves = debounce(() => {
const leaves = this.app.workspace.getLeavesOfType("canvas");
leaves.forEach((leaf) => {
leaf.rebuildView();
});
}, 1000);
async saveSettings() {
await this.saveData(this.settings);
this.debounceReloadLeaves();
}
}
@ -311,6 +324,78 @@ export class CollapseSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Show thumbnails in collapsed state")
.setDesc("Show thumbnails in the collapsed state of the node")
.addToggle((toggle) => {
toggle
.setValue(
this.plugin.settings.showThumbnailsInCollapsedState
)
.onChange(async (value) => {
this.plugin.settings.showThumbnailsInCollapsedState =
value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Show aliases in collapsed state")
.setDesc("Show aliases in the collapsed state of the node")
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.showAliasesInCollapsedState)
.onChange(async (value) => {
this.plugin.settings.showAliasesInCollapsedState =
value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Hide default node title")
.setDesc("Hide the default title of the node")
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.hideDefaultNodeTitle)
.onChange(async (value) => {
this.plugin.settings.hideDefaultNodeTitle = value;
document.body.toggleClass(
"hide-default-node-title",
value
);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Show aliases always")
.setDesc(
"Show aliases always in the collapsed/expanded state of the node. Replace current title with alias."
)
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.showAliasesAlways)
.onChange(async (value) => {
this.plugin.settings.showAliasesAlways = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Show thumbnails always")
.setDesc(
"Show thumbnails always in the collapsed state of the node"
)
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.showThumbnailsAlways)
.onChange(async (value) => {
this.plugin.settings.showThumbnailsAlways = value;
await this.plugin.saveSettings();
});
});
}
createCollapsableSetting(

View file

@ -13,7 +13,7 @@ import {
import { CanvasCoords } from "obsidian";
import { CanvasView } from "obsidian";
import CanvasCollapsePlugin from "./canvasCollapseIndex";
import CanvasCollapsePlugin from ".";
import { CanvasData } from "obsidian/canvas";
import {
@ -210,12 +210,18 @@ export const patchCanvasMenu = (plugin: CanvasCollapsePlugin) => {
plugin.triggerByPlugin = true;
};
const patchMenu = () => {
const canvasView = plugin.app.workspace
const patchMenu = async () => {
const canvasLeaf = plugin.app.workspace
.getLeavesOfType("canvas")
.first()?.view;
if (!canvasView) return false;
.first();
if (canvasLeaf?.isDeferred) {
await canvasLeaf.loadIfDeferred();
}
const canvasView = canvasLeaf?.view;
if (!canvasView) return false;
const menu = (canvasView as CanvasView)?.canvas.menu;
if (!menu) return false;
@ -288,10 +294,10 @@ export const patchCanvasMenu = (plugin: CanvasCollapsePlugin) => {
return true;
};
plugin.app.workspace.onLayoutReady(() => {
if (!patchMenu()) {
const evt = plugin.app.workspace.on("layout-change", () => {
patchMenu() && plugin.app.workspace.offref(evt);
plugin.app.workspace.onLayoutReady(async () => {
if (!(await patchMenu())) {
const evt = plugin.app.workspace.on("layout-change", async () => {
(await patchMenu()) && plugin.app.workspace.offref(evt);
});
plugin.registerEvent(evt);
}
@ -356,59 +362,55 @@ export const renderNodeWithHeader = (
plugin: CanvasCollapsePlugin,
node: any
) => {
// If header already exists, don't create another one
if (node.headerComponent) return;
if (
!plugin.settings.collapsableFileNode &&
node.unknownData.type === "file" &&
node.file.extension === "md"
)
return;
if (
!plugin.settings.collapsableAttachmentNode &&
node.unknownData.type === "file" &&
node.file.extension !== "md"
)
return;
if (
!plugin.settings.collapsableGroupNode &&
node.unknownData.type === "group"
)
return;
if (
!plugin.settings.collapsableLinkNode &&
node.unknownData.type === "link"
)
return;
if (
!plugin.settings.collapsableTextNode &&
node.unknownData.type === "text"
)
return;
// Check node type against plugin settings
const nodeType = node.unknownData.type;
// Return early if this node type is disabled in settings
if (
nodeType === "file" &&
node.file?.extension === "md" &&
!plugin.settings.collapsableFileNode
)
return;
if (
nodeType === "file" &&
node.file?.extension !== "md" &&
!plugin.settings.collapsableAttachmentNode
)
return;
if (nodeType === "group" && !plugin.settings.collapsableGroupNode) return;
if (nodeType === "link" && !plugin.settings.collapsableLinkNode) return;
if (nodeType === "text" && !plugin.settings.collapsableTextNode) return;
// Check minimum line amount for text and file nodes
if (
plugin.settings.minLineAmount > 0 &&
(node.unknownData.type === "text" || node.unknownData.type === "file")
(nodeType === "text" || nodeType === "file")
) {
if (
typeof node.text === "string" &&
node.text.split("\n").length < plugin.settings.minLineAmount
)
return;
if (
node.file &&
node.file.extension === "md" &&
node.child &&
node.child.data?.split("\n").length < plugin.settings.minLineAmount
)
return;
let lineCount = 0;
if (nodeType === "text" && typeof node.text === "string") {
lineCount = node.text.split("\n").length;
} else if (
nodeType === "file" &&
node.file?.extension === "md" &&
node.child
) {
lineCount = node.child.data?.split("\n").length || 0;
}
if (lineCount < plugin.settings.minLineAmount) return;
}
// Create header component
node.headerComponent = initControlHeader(
plugin,
node
) as CollapseControlHeader;
node.nodeEl.setAttribute("data-node-type", node.unknownData.type);
node.nodeEl.setAttribute("data-node-type", nodeType);
// Wait for containerEl to be loaded before adding header
const addHeader = () => {
@ -422,6 +424,7 @@ export const renderNodeWithHeader = (
};
addHeader();
// Apply collapsed state if needed
if (node.unknownData.collapsed) {
node.nodeEl.toggleClass("collapsed", true);
node.headerComponent.updateEdges();
@ -469,6 +472,8 @@ export const patchCanvasNode = (plugin: CanvasCollapsePlugin) => {
}
}
console.log(prototype);
if (!prototype) return false;
const uninstaller = around(prototype, {

View file

@ -204,6 +204,11 @@ declare module "obsidian" {
lineEndGroupEl: SVGGElement;
lineGroupEl: SVGGElement;
labelElement: {
wrapperEl: HTMLElement;
textEl: HTMLElement;
};
path: {
display: SVGPathElement;
interaction: SVGPathElement;
@ -211,6 +216,7 @@ declare module "obsidian" {
canvas: Canvas;
bbox: CanvasCoords;
render(): void;
unknownData: CanvasNodeUnknownData;
}

View file

@ -1,6 +1,17 @@
import { App, Canvas, CanvasCoords, CanvasNode, ItemView, Menu, MenuItem } from "obsidian";
import {
App,
Canvas,
CanvasCoords,
CanvasNode,
ItemView,
Menu,
MenuItem,
Modal,
SuggestModal,
TFile,
} from "obsidian";
import CollapseControlHeader from "./ControlHeader";
import CanvasCollapsePlugin from "./canvasCollapseIndex";
import CanvasCollapsePlugin from ".";
const getBoundingRect = (nodes: CanvasNode[]) => {
const bboxArray = nodes.map((t: CanvasNode) => t.getBBox());
@ -20,17 +31,29 @@ const getBoundingRect = (nodes: CanvasNode[]) => {
const updateSelection = (canvas: Canvas) => {
if (canvas.menu.selection.bbox) {
const selection = Array.from(canvas.selection);
const currentNodesInSelection = canvas.getContainingNodes(canvas.menu.selection.bbox);
const currentNodesInSelection = canvas.getContainingNodes(
canvas.menu.selection.bbox
);
if (currentNodesInSelection.length > 0) {
const boundingRect = getBoundingRect(selection.length > currentNodesInSelection.length ? selection : currentNodesInSelection);
const boundingRect = getBoundingRect(
selection.length > currentNodesInSelection.length
? selection
: currentNodesInSelection
);
if (boundingRect) {
canvas.menu.selection.update(boundingRect);
}
}
}
};
const handleMultiNodes = (canvas: Canvas, allNodes: boolean, collapse: boolean) => {
const nodes = allNodes ? Array.from(canvas.nodes.values()) : Array.from(canvas.selection) as any[];
const handleMultiNodes = (
canvas: Canvas,
allNodes: boolean,
collapse: boolean
) => {
const nodes = allNodes
? Array.from(canvas.nodes.values())
: (Array.from(canvas.selection) as any[]);
const canvasData = canvas.getData();
if (nodes && nodes.length > 0) {
@ -39,7 +62,9 @@ const handleMultiNodes = (canvas: Canvas, allNodes: boolean, collapse: boolean)
node.headerComponent.updateNodesInGroup(collapse);
}
node.headerComponent?.setCollapsed(collapse);
const nodeData = canvasData.nodes.find((t: any) => t.id === node.id);
const nodeData = canvasData.nodes.find(
(t: any) => t.id === node.id
);
if (nodeData) nodeData.collapsed = collapse;
}
canvas.setData(canvasData);
@ -48,16 +73,26 @@ const handleMultiNodes = (canvas: Canvas, allNodes: boolean, collapse: boolean)
canvas.requestFrame();
updateSelection(canvas);
};
export const handleMultiNodesViaNodes = (canvas: Canvas, nodes: CanvasNode[], collapse: boolean) => {
export const handleMultiNodesViaNodes = (
canvas: Canvas,
nodes: CanvasNode[],
collapse: boolean
) => {
const canvasData = canvas.getData();
if (nodes && nodes.length > 0) {
for (const node of nodes) {
if (node.unknownData.type === "group") {
(node.headerComponent as CollapseControlHeader).updateNodesInGroup(collapse);
(
node.headerComponent as CollapseControlHeader
).updateNodesInGroup(collapse);
}
(node.headerComponent as CollapseControlHeader)?.setCollapsed(collapse);
const nodeData = canvasData.nodes.find((t: any) => t.id === node.id);
(node.headerComponent as CollapseControlHeader)?.setCollapsed(
collapse
);
const nodeData = canvasData.nodes.find(
(t: any) => t.id === node.id
);
if (nodeData) nodeData.collapsed = collapse;
}
canvas.setData(canvasData);
@ -76,7 +111,13 @@ export const handleSingleNode = (node: CanvasNode, collapse: boolean) => {
node.canvas.requestSave(true, true);
updateSelection(node.canvas);
};
export const handleNodesViaCommands = (plugin: CanvasCollapsePlugin, checking: boolean, allNodes: boolean, collapse: boolean) => {
export const handleNodesViaCommands = (
plugin: CanvasCollapsePlugin,
checking: boolean,
allNodes: boolean,
collapse: boolean
) => {
plugin.triggerByPlugin = true;
const currentView = plugin.app.workspace.getActiveViewOfType(ItemView);
if (currentView && currentView.getViewType() === "canvas") {
@ -89,46 +130,435 @@ export const handleNodesViaCommands = (plugin: CanvasCollapsePlugin, checking: b
return true;
}
};
const createHandleContextMenu = (section: string, callback: (isFold: boolean) => Promise<void>) => {
const createHandleContextMenu = (
section: string,
callback: (isFold: boolean) => Promise<void>
) => {
return (menu: Menu) => {
menu.addItem((item: MenuItem) => {
const subMenu = item.setSection(section).setTitle('Collapse node').setIcon('chevrons-left-right').setSubmenu();
const subMenu = item
.setSection(section)
.setTitle("Collapse node")
.setIcon("chevrons-left-right")
.setSubmenu();
handleCanvasMenu(subMenu, callback);
});
};
};
export const handleCanvasMenu = (subMenu: Menu, callback: (isFold: boolean) => Promise<void>) => {
return subMenu.addItem((item: MenuItem) => {
item
.setIcon("fold-vertical")
.setTitle("Fold selected nodes")
.onClick(async () => {
await callback(true);
});
}).addItem((item: any) => {
item
.setIcon("unfold-vertical")
.setTitle("Expand selected nodes")
.onClick(async () => {
await callback(false);
});
});
export const handleCanvasMenu = (
subMenu: Menu,
callback: (isFold: boolean) => Promise<void>
) => {
return subMenu
.addItem((item: MenuItem) => {
item.setIcon("fold-vertical")
.setTitle("Fold selected nodes")
.onClick(async () => {
await callback(true);
});
})
.addItem((item: any) => {
item.setIcon("unfold-vertical")
.setTitle("Expand selected nodes")
.onClick(async () => {
await callback(false);
});
});
};
export const handleSelectionContextMenu = (plugin: CanvasCollapsePlugin, menu: Menu, canvas: Canvas) => {
export const handleSelectionContextMenu = (
plugin: CanvasCollapsePlugin,
menu: Menu,
canvas: Canvas
) => {
plugin.triggerByPlugin = true;
const callback = async (isFold: boolean) => {
handleMultiNodes(canvas, false, isFold);
};
createHandleContextMenu('action', callback)(menu);
createHandleContextMenu("action", callback)(menu);
};
export const handleNodeContextMenu = (plugin: CanvasCollapsePlugin, menu: Menu, node: CanvasNode) => {
export const handleNodeContextMenu = (
plugin: CanvasCollapsePlugin,
menu: Menu,
node: CanvasNode
) => {
plugin.triggerByPlugin = true;
const callback = async (isFold: boolean) => {
handleSingleNode(node, isFold);
};
createHandleContextMenu('canvas', callback)(menu);
createHandleContextMenu("canvas", callback)(menu);
// Add Alias and Thumbnail menu items
menu.addItem((item: MenuItem) => {
item.setSection("canvas")
.setTitle("Set Node Alias")
.setIcon("text-cursor-input")
.onClick(async () => {
await setNodeAlias(plugin, node);
});
});
menu.addItem((item: MenuItem) => {
item.setSection("canvas")
.setTitle("Set Node Thumbnail")
.setIcon("image")
.onClick(async () => {
await setNodeThumbnail(plugin, node);
});
});
// Add option to remove alias/thumbnail
if (node.unknownData.alias || node.unknownData.thumbnail) {
menu.addItem((item: MenuItem) => {
item.setSection("canvas")
.setTitle("Remove Node Customizations")
.setIcon("trash")
.onClick(async () => {
await removeNodeCustomizations(plugin, node);
});
});
}
};
// Function to set alias for a node
export const setNodeAlias = async (
plugin: CanvasCollapsePlugin,
node: CanvasNode
) => {
const modal = new TextInputModal(
plugin.app,
"Enter alias for node",
node.unknownData.alias || "",
"Set Alias"
);
const alias = await modal.openAndGetValue();
if (alias !== null) {
// Set the alias in node data
node.unknownData.alias = alias;
// Update the node if it has a header component
if (
node.headerComponent &&
node.headerComponent instanceof CollapseControlHeader
) {
const header = node.headerComponent as CollapseControlHeader;
header.updateNodeAlias(alias);
header.updateNode();
}
// Save the canvas state
node.canvas.requestSave(false, true);
}
};
// Function to set thumbnail for a node
export const setNodeThumbnail = async (
plugin: CanvasCollapsePlugin,
node: CanvasNode
) => {
const modal = new ThumbnailSelectionModal(plugin.app);
const thumbnailPath = await modal.openAndGetValue();
if (thumbnailPath) {
// Set the thumbnail in node data
node.unknownData.thumbnail = thumbnailPath;
// Update the node if it has a header component
if (
node.headerComponent &&
node.headerComponent instanceof CollapseControlHeader
) {
const header = node.headerComponent as CollapseControlHeader;
header.updateNodeThumbnail(thumbnailPath);
header.updateNode();
}
// Save the canvas state
node.canvas.requestSave(false, true);
}
};
// Function to remove alias and thumbnail from node
export const removeNodeCustomizations = async (
plugin: CanvasCollapsePlugin,
node: CanvasNode
) => {
delete node.unknownData.alias;
delete node.unknownData.thumbnail;
// Update the node if it has a header component
if (
node.headerComponent &&
node.headerComponent instanceof CollapseControlHeader
) {
const header = node.headerComponent as CollapseControlHeader;
header.updateNodeAlias("");
header.updateNodeThumbnail("");
header.updateNode();
}
// Save the canvas state
node.canvas.requestSave(false, true);
};
// Class for text input modal
class TextInputModal extends Modal {
private value: string;
private inputEl: HTMLInputElement;
private resolvePromise: (value: string | null) => void;
private buttonText: string;
constructor(
app: App,
private title: string,
initialValue: string,
buttonText: string
) {
super(app);
this.value = initialValue;
this.buttonText = buttonText;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: this.title });
this.inputEl = contentEl.createEl("input", {
type: "text",
value: this.value,
});
this.inputEl.style.width = "100%";
this.inputEl.style.marginBottom = "1em";
// Focus input
setTimeout(() => this.inputEl.focus(), 10);
// Add buttons
const buttonContainer = contentEl.createDiv();
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
buttonContainer.style.gap = "10px";
const cancelButton = buttonContainer.createEl("button", {
text: "Cancel",
});
const saveButton = buttonContainer.createEl("button", {
text: this.buttonText,
cls: "mod-cta",
});
cancelButton.addEventListener("click", () => {
this.resolvePromise(null);
this.close();
});
saveButton.addEventListener("click", () => {
this.resolvePromise(this.inputEl.value);
this.close();
});
// Handle Enter key
this.inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
this.resolvePromise(this.inputEl.value);
this.close();
}
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
openAndGetValue(): Promise<string | null> {
return new Promise((resolve) => {
this.resolvePromise = resolve;
this.open();
});
}
}
// Class for thumbnail selection modal
class ThumbnailSelectionModal extends Modal {
private resolvePromise: (value: string | null) => void;
private inputEl: HTMLInputElement;
private fileInput: HTMLInputElement;
private preview: HTMLImageElement;
constructor(app: App) {
super(app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: "Set Node Thumbnail" });
// URL input
contentEl.createEl("p", {
text: "Enter image URL or path to an attachment:",
});
this.inputEl = contentEl.createEl("input", {
type: "text",
placeholder: "https://example.com/image.jpg or image.jpg",
});
this.inputEl.style.width = "100%";
this.inputEl.style.marginBottom = "1em";
// Or use file picker to select from vault
contentEl.createEl("p", {
text: "Or select an image from your attachments:",
});
const selectFileButton = contentEl.createEl("button", {
text: "Browse vault files",
});
selectFileButton.addEventListener("click", () => {
this.openFilePicker();
});
// Preview area
contentEl.createEl("p", {
text: "Preview:",
cls: "thumbnail-preview-label",
});
this.preview = contentEl.createEl("img", { cls: "thumbnail-preview" });
this.preview.style.maxWidth = "100%";
this.preview.style.maxHeight = "150px";
this.preview.style.display = "none";
// Update preview when URL changes
this.inputEl.addEventListener("input", () => {
this.updatePreview();
});
// Buttons
const buttonContainer = contentEl.createDiv();
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
buttonContainer.style.gap = "10px";
buttonContainer.style.marginTop = "1em";
const cancelButton = buttonContainer.createEl("button", {
text: "Cancel",
});
const saveButton = buttonContainer.createEl("button", {
text: "Set Thumbnail",
cls: "mod-cta",
});
cancelButton.addEventListener("click", () => {
this.resolvePromise(null);
this.close();
});
saveButton.addEventListener("click", () => {
this.resolvePromise(this.inputEl.value);
this.close();
});
}
openFilePicker() {
const fileSuggestModal = new FileSuggestModal(this.app, (file) => {
if (file) {
this.inputEl.value = file.path;
this.updatePreview();
}
});
fileSuggestModal.open();
}
updatePreview() {
const url = this.inputEl.value;
if (url) {
try {
// For remote URLs
if (url.startsWith("http")) {
this.preview.src = url;
this.preview.style.display = "block";
}
// For local files
else {
this.preview.src =
this.app.vault.adapter.getResourcePath(url);
this.preview.style.display = "block";
}
} catch (e) {
this.preview.style.display = "none";
}
} else {
this.preview.style.display = "none";
}
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
openAndGetValue(): Promise<string | null> {
return new Promise((resolve) => {
this.resolvePromise = resolve;
this.open();
});
}
}
// File selection modal for picking a file from the vault
class FileSuggestModal extends SuggestModal<TFile> {
private callback: (file: TFile | null) => void;
constructor(app: App, callback: (file: TFile | null) => void) {
super(app);
this.callback = callback;
}
getSuggestions(query: string): TFile[] {
const imageExtensions = [
"png",
"jpg",
"jpeg",
"gif",
"bmp",
"svg",
"webp",
];
// Get all image files from the vault
const files = this.app.vault
.getFiles()
.filter(
(file) =>
imageExtensions.includes(file.extension.toLowerCase()) &&
file.path.toLowerCase().includes(query.toLowerCase())
);
return files;
}
renderSuggestion(file: TFile, el: HTMLElement) {
el.createEl("div", { text: file.path });
}
onChooseSuggestion(file: TFile, evt: MouseEvent | KeyboardEvent) {
this.callback(file);
}
onClose() {
// If no selection was made, call the callback with null
if (this.callback) {
this.callback(null);
}
}
}
export const refreshAllCanvasView = (app: App) => {
const cavasLeaves = app.workspace.getLeavesOfType("canvas");
if (!cavasLeaves || cavasLeaves.length === 0) return;

View file

@ -4,19 +4,35 @@
overflow: hidden;
width: calc(100% * var(--zoom-multiplier));
min-width: 100%;
height: calc(40px * var(--zoom-multiplier));
height: calc(30px * var(--zoom-multiplier));
/*max-height: 100px;*/
min-height: 20px;
min-height: max(calc(30px * var(--zoom-multiplier)), 20px);
cursor: pointer;
display: flex;
align-items: center;
padding-left: var(--size-4-2);
gap: var(--size-4-3);
margin-top: -1px;
margin-top: calc(30px * var(--zoom-multiplier) * -1);
transition: background-color 0.2s ease-in-out;
}
.canvas-node-container:has(.canvas-node-content > img)
.canvas-node-collapse-control {
position: absolute;
}
.canvas-node-container:not(:has(.canvas-node-content > img)):has(
.canvas-node-collapse-control
) {
padding-top: calc(30px * var(--zoom-multiplier));
}
.canvas-node-container:has(.canvas-node-content > img)
.canvas-node-collapse-control {
margin-top: 0;
}
.canvas-node:not(.collapsed) .canvas-node-collapse-control {
@ -31,7 +47,6 @@
background-color: rgb(var(--canvas-color));
}
.canvas-node:has(.canvas-node-collapse-control) {
width: fit-content;
height: fit-content;
@ -41,7 +56,8 @@
/* pointer-events: unset;*/
/*}*/
.canvas-node-group:has(.canvas-node-collapse-control) .canvas-node-collapse-control {
.canvas-node-group:has(.canvas-node-collapse-control)
.canvas-node-collapse-control {
pointer-events: initial !important;
}
@ -50,40 +66,224 @@
/*}*/
.canvas-node.collapsed {
height: calc(40px * var(--zoom-multiplier)) !important;
height: calc(30px * var(--zoom-multiplier)) !important;
max-height: fit-content;
transition: height 0.2s ease-in-out, max-height 0.2s ease-in-out;
}
.canvas-node.collapsed .canvas-node-placeholder {
display: none;
}
.group-nodes-collapsed, .group-edges-collapsed {
.group-nodes-collapsed,
.group-edges-collapsed {
display: none;
transition: opacity 0.2s ease-in-out;
opacity: 0;
}
.canvas-node.collapsed .canvas-node-content {
display: none;
transition: opacity 0.2s ease-in-out;
opacity: 0;
}
.collapsed-interaction {
height: calc(40px * var(--zoom-multiplier)) !important;
height: calc(30px * var(--zoom-multiplier)) !important;
transition: height 0.2s ease-in-out;
}
.canvas-node-collapse-control-icon, .canvas-node-type-icon {
.canvas-node-collapse-control-icon,
.canvas-node-type-icon {
display: flex;
}
.canvas-node-collapse-control-icon svg, .canvas-node-type-icon svg {
.canvas-node-collapse-control-icon svg,
.canvas-node-type-icon svg {
height: calc(18px * var(--zoom-multiplier));
width: calc(18px * var(--zoom-multiplier));
}
.canvas-node-collapse-control + .canvas-node-content.media-embed img {
height: 100%;
height: auto;
max-height: 100%;
object-fit: contain;
}
.minimal-control-header .canvas-node-collapse-control-title, .minimal-control-header .canvas-node-type-icon {
.minimal-control-header .canvas-node-collapse-control-title,
.minimal-control-header .canvas-node-type-icon {
display: none;
}
.canvas-node {
/* Add transition for height and max-height only */
transition: height 0.2s ease-in-out, max-height 0.2s ease-in-out;
}
.canvas-node-content {
/* Add transition for opacity */
transition: opacity 0.2s ease-in-out;
opacity: 1;
}
.canvas-node.collapsed .canvas-node-content {
opacity: 0;
/* Keep the display:none but add it with a delay so the opacity transition is visible */
transition: opacity 0.2s ease-in-out;
}
/* Add rotation animation to the collapse icon */
.canvas-node-collapse-control-icon svg {
transition: transform 0.2s ease;
}
.canvas-node.collapsed .canvas-node-collapse-control-icon svg {
transform: rotate(-90deg);
transition: transform 0.2s ease;
}
/* Remove hover scale effect that causes node drift */
/* .canvas-node:not(.collapsed):hover {
transform: scale(1.01);
} */
/* Animate the collapse control background */
.canvas-node-collapse-control {
transition: background-color 0.2s ease-in-out;
}
/* Modify drawer animation to prevent drift */
.canvas-node-group .group-nodes,
.canvas-node-group .group-edges {
transition: opacity 0.2s ease-in-out, max-height 0.2s ease-in-out;
/* Remove transform transitions that cause drift */
}
.canvas-node-group.collapsed .group-nodes-collapsed,
.canvas-node-group.collapsed .group-edges-collapsed {
opacity: 0;
transition: opacity 0.2s ease-in-out;
/* Remove transform that causes drift */
}
/* Animation classes for transitions */
.node-transitioning {
transition: opacity 0.2s ease-in-out, max-height 0.2s ease-in-out !important;
/* Remove transform transition that causes drift */
}
.animating {
overflow: hidden !important;
}
/* Simplified drawer animation to prevent drift */
.canvas-node-group .group-nodes {
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out;
will-change: opacity, max-height;
/* Remove transform properties that cause drift */
}
.canvas-node-group.collapsed .group-nodes {
max-height: 0;
opacity: 0;
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out;
/* Remove transform that causes drift */
}
/* Animation for edges */
.canvas-edge {
transition: opacity 0.2s ease-in-out;
}
.group-edges-collapsed {
transition: opacity 0.2s ease-in-out;
opacity: 0;
}
/* Remove bounce animation that causes drift */
/* @keyframes bounceIn {
0% {
transform: scale(0.95);
opacity: 0;
}
70% {
transform: scale(1.02);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.canvas-node:not(.collapsed):not(.animating) {
animation: bounceIn 0.4s ease-out;
} */
.collapse-handler.collapsed svg {
transform: rotate(-90deg);
transition: transform 0.2s ease-in-out;
}
.collapse-handler svg {
transform: rotate(0deg);
transition: transform 0.2s ease-in-out;
}
.canvas-node .canvas-node-collapse-control-thumbnail {
display: none;
}
/* Styles for thumbnails and aliases in collapsed state */
.canvas-node.collapsed .canvas-node-collapse-control-thumbnail {
max-width: 100%;
max-height: 100px;
object-fit: contain;
margin: 5px auto;
display: block;
border-radius: 4px;
position: absolute;
bottom: calc(27px * var(--zoom-multiplier));
}
.canvas-node.collapsed .canvas-node-collapse-control-alias {
display: block;
font-weight: bold;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-normal);
}
/* Adjust the header to accommodate thumbnails and aliases */
.canvas-node.collapsed .canvas-node-collapse-control {
display: flex;
flex-direction: row;
width: 100%;
}
/* Make sure the header elements are properly spaced in collapsed mode */
.canvas-node .canvas-node-collapse-control-icon,
.canvas-node .canvas-node-type-icon {
margin-bottom: 4px;
}
body.hide-default-node-title
.canvas-node:has(.canvas-node-collapse-control)
.canvas-node-label {
display: none;
}
.collapsed-node-hidden {
display: none !important;
}
span.canvas-node-collapse-control-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}

View file

@ -2,5 +2,6 @@
"1.0.0": "1.1.0",
"1.0.1": "1.1.0",
"1.1.0": "1.1.0",
"1.1.1": "1.1.0"
"1.1.1": "1.1.0",
"2.0.0": "1.1.0"
}