mirror of
https://github.com/ninglg/light-mindmap.git
synced 2026-07-22 06:51:49 +00:00
feat: support clickable external links in mindmap nodes
Markdown links in heading text are now rendered as clickable links on the canvas. Bump version to 1.2.2.
This commit is contained in:
parent
4fec5776de
commit
4bed360341
5 changed files with 73 additions and 12 deletions
|
|
@ -143,6 +143,10 @@ All per-file display preferences are written to frontmatter and restored on next
|
|||
- Command palette: **Toggle mindmap / source view**
|
||||
- Command palette: **Cycle mindmap layout (balanced / right / left)**
|
||||
|
||||
### External Links
|
||||
|
||||
Markdown links in heading text (`[text](url)`) are rendered as clickable links directly on the mindmap canvas. Clicking a link opens it in the default browser. During node editing, links are shown as plain text for easy modification.
|
||||
|
||||
### Export PNG
|
||||
|
||||
Click the **Export PNG** button in the toolbar to save the current mindmap as a high-resolution PNG image (2x scale). A system file dialog will let you choose the save location.
|
||||
|
|
|
|||
|
|
@ -144,6 +144,10 @@ type: mindmap
|
|||
- 命令面板:**Toggle mindmap / source view**
|
||||
- 命令面板:**Cycle mindmap layout (balanced / right / left)**
|
||||
|
||||
### 外部链接
|
||||
|
||||
标题中的 Markdown 链接(`[文本](url)`)会直接在思维导图画布上渲染为可点击的链接。点击链接会在默认浏览器中打开。编辑节点时,链接会显示为纯文本以便修改。
|
||||
|
||||
### 导出 PNG
|
||||
|
||||
点击工具栏中的 **Export PNG** 按钮,将当前思维导图保存为高清 PNG 图片(2 倍分辨率)。系统文件对话框允许你选择保存位置。
|
||||
|
|
|
|||
54
main.js
54
main.js
|
|
@ -235,6 +235,37 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
.trim();
|
||||
}
|
||||
|
||||
_renderNodeContent(el, node, overlay) {
|
||||
const raw = node.rawText || node.text || '';
|
||||
if (!raw.includes('](')) {
|
||||
el.textContent = node.text || PLACEHOLDER;
|
||||
return;
|
||||
}
|
||||
el.textContent = '';
|
||||
const re = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
let last = 0;
|
||||
let m;
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
if (m.index > last) {
|
||||
el.appendChild(document.createTextNode(raw.slice(last, m.index)));
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.className = 'lmm-link';
|
||||
a.textContent = m[1];
|
||||
a.href = m[2];
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener';
|
||||
el.appendChild(a);
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
if (last < raw.length) {
|
||||
el.appendChild(document.createTextNode(raw.slice(last)));
|
||||
}
|
||||
if (el.childNodes.length === 0) {
|
||||
el.textContent = node.text || PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
|
||||
_parseStructured(content) {
|
||||
const { frontmatterRaw, body } = this._splitFrontmatter(content);
|
||||
const lines = body.split('\n');
|
||||
|
|
@ -356,9 +387,7 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
|
||||
_serializeNode(node, level) {
|
||||
const cap = Math.min(6, Math.max(1, level));
|
||||
let text = (node.dirty || node.isNew)
|
||||
? (node.text || PLACEHOLDER)
|
||||
: (node.rawText || node.text || PLACEHOLDER);
|
||||
let text = node.rawText || node.text || PLACEHOLDER;
|
||||
if (node.collapsed) {
|
||||
const inner = text.replace(/^\*(?!\*)(.+)\*(?!\*)$/, '$1');
|
||||
text = '*' + inner + '*';
|
||||
|
|
@ -713,13 +742,13 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
if (node.collapsed && node.children.length) {
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.className = 'lmm-node-text';
|
||||
textSpan.textContent = node.text || PLACEHOLDER;
|
||||
this._renderNodeContent(textSpan, node, overlay);
|
||||
el.appendChild(textSpan);
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'lmm-collapse-badge';
|
||||
el.appendChild(badge);
|
||||
} else {
|
||||
el.textContent = node.text || PLACEHOLDER;
|
||||
this._renderNodeContent(el, node, overlay);
|
||||
}
|
||||
node._el = el;
|
||||
this._attachNodeHandlers(el, node, overlay);
|
||||
|
|
@ -740,6 +769,7 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
el.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (el.isContentEditable) return;
|
||||
if (e.target.closest('.lmm-link')) return;
|
||||
this._selectNode(overlay, node, true);
|
||||
});
|
||||
el.addEventListener('dblclick', (e) => {
|
||||
|
|
@ -820,6 +850,7 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
}
|
||||
this._selectNode(overlay, node, false);
|
||||
overlay._lmmEditingNode = node;
|
||||
el.textContent = node.rawText || node.text || PLACEHOLDER;
|
||||
el.contentEditable = 'true';
|
||||
el.classList.add('lmm-editing');
|
||||
el.spellcheck = false;
|
||||
|
|
@ -835,10 +866,10 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
if (overlay._lmmEditingBlur === onBlur) overlay._lmmEditingBlur = null;
|
||||
if (!el.isContentEditable) return;
|
||||
const text = el.textContent;
|
||||
const had = node.text;
|
||||
const had = node.rawText || node.text;
|
||||
this._exitEditMode(overlay, node);
|
||||
this._updateNodeText(node, text);
|
||||
if (node.text !== had) this._persistAndRelayout(overlay);
|
||||
if ((node.rawText || node.text) !== had) this._persistAndRelayout(overlay);
|
||||
};
|
||||
el.addEventListener('blur', onBlur);
|
||||
overlay._lmmEditingBlur = onBlur;
|
||||
|
|
@ -854,21 +885,22 @@ class LightMindMapPlugin extends obsidian.Plugin {
|
|||
el.contentEditable = 'false';
|
||||
el.classList.remove('lmm-editing');
|
||||
if (overlay._lmmEditingNode === node) overlay._lmmEditingNode = null;
|
||||
this._renderNodeContent(el, node, overlay);
|
||||
}
|
||||
|
||||
_updateNodeText(node, newText) {
|
||||
const text = (newText || '').replace(/\s+/g, ' ').trim();
|
||||
if (!text) return;
|
||||
if (text !== node.text) {
|
||||
node.text = text;
|
||||
const hadRaw = node.rawText || node.text;
|
||||
if (text !== hadRaw) {
|
||||
node.rawText = text;
|
||||
node.text = this._stripInline(text);
|
||||
node.dirty = true;
|
||||
if (node._el) node._el.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
_cancelEdit(overlay, node) {
|
||||
this._exitEditMode(overlay, node);
|
||||
if (node._el) node._el.textContent = node.text || PLACEHOLDER;
|
||||
}
|
||||
|
||||
_toggleCollapse(overlay, node) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "light-mindmap",
|
||||
"name": "Light Mindmap",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "Auto-renders markdown headings as a colorful, interactive mindmap — no extra syntax required.",
|
||||
"author": "Light Ning",
|
||||
|
|
|
|||
21
styles.css
21
styles.css
|
|
@ -560,3 +560,24 @@
|
|||
.lmm-fab:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ── Node links ── */
|
||||
|
||||
.lmm-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-underline-offset: 2px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.lmm-link:hover {
|
||||
text-decoration-style: solid;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.lmm-node.lmm-editing .lmm-link {
|
||||
text-decoration: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue