mirror of
https://github.com/quorafind/Obsidian-Collapse-Node.git
synced 2026-07-22 08:40:32 +00:00
Merge pull request #30 from Quorafind/feat/2.0.0
Refactor And Go Ahead: 2.0.0
This commit is contained in:
commit
60a78514cd
12 changed files with 1279 additions and 250 deletions
34
README.md
34
README.md
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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, {
|
||||
|
|
|
|||
6
src/types/obsidian.d.ts
vendored
6
src/types/obsidian.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
498
src/utils.ts
498
src/utils.ts
|
|
@ -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;
|
||||
|
|
|
|||
230
styles.css
230
styles.css
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue