mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
jQuery UI dialog buttons don't work properly with emojis in the label text. Removed emojis from button labels while keeping them in the dialog content. Changed: - '🔍 Search' → 'Search' - '📁 Browse' → 'Browse' The emojis are still visible in the dialog content boxes, just not in the actual button labels.
805 lines
27 KiB
JavaScript
805 lines
27 KiB
JavaScript
"use strict";
|
|
|
|
// Modern Markdown Notes Editor with Obsidian Integration
|
|
|
|
function editObsidianNote(elementId, elementType, coordinates) {
|
|
const {x, y} = coordinates;
|
|
|
|
// Show choice dialog: automatic search or manual browse
|
|
showSearchMethodDialog(elementId, elementType, coordinates);
|
|
}
|
|
|
|
function showSearchMethodDialog(elementId, elementType, coordinates) {
|
|
const element = getElementData(elementId, elementType);
|
|
const elementName = element.name || elementId;
|
|
|
|
alertMessage.innerHTML = `
|
|
<div style="padding: 1em;">
|
|
<p style="margin-bottom: 1em;"><strong>${elementName}</strong></p>
|
|
<p style="margin-bottom: 1.5em; color: #666;">How would you like to find the note for this ${elementType}?</p>
|
|
|
|
<div style="margin: 1em 0; padding: 12px; background: #f0f8ff; border: 1px solid #0066cc; border-radius: 4px;">
|
|
<div style="font-weight: bold; margin-bottom: 4px;">🔍 Automatic Search</div>
|
|
<div style="font-size: 0.9em; color: #666;">Search by linked ID or nearby coordinates</div>
|
|
</div>
|
|
|
|
<div style="margin: 1em 0; padding: 12px; background: #fff8e1; border: 1px solid #ffa000; border-radius: 4px;">
|
|
<div style="font-weight: bold; margin-bottom: 4px;">📁 Browse Manually</div>
|
|
<div style="font-size: 0.9em; color: #666;">Browse your vault's folder tree</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$("#alert").dialog({
|
|
title: "Select Note",
|
|
width: "450px",
|
|
buttons: {
|
|
"Search": function () {
|
|
$(this).dialog("close");
|
|
// Show loading and do automatic search
|
|
showLoadingDialog();
|
|
findOrCreateNote(elementId, elementType, coordinates)
|
|
.then(noteData => {
|
|
showMarkdownEditor(noteData, elementType, elementId, coordinates);
|
|
})
|
|
.catch(error => {
|
|
ERROR && console.error("Failed to load note:", error);
|
|
tip("Failed to load Obsidian note: " + error.message, true, "error", 5000);
|
|
closeDialogs("#obsidianNoteLoading");
|
|
});
|
|
},
|
|
"Browse": async function () {
|
|
$(this).dialog("close");
|
|
try {
|
|
const noteData = await promptCreateNewNote(elementId, elementType, coordinates);
|
|
showMarkdownEditor(noteData, elementType, elementId, coordinates);
|
|
} catch (error) {
|
|
if (error.message !== "Cancelled") {
|
|
ERROR && console.error("Failed to load note:", error);
|
|
tip("Failed to load Obsidian note: " + error.message, true, "error", 5000);
|
|
}
|
|
}
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
}
|
|
},
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
}
|
|
|
|
async function findOrCreateNote(elementId, elementType, coordinates) {
|
|
const {x, y} = coordinates;
|
|
|
|
// First try to find by FMG ID
|
|
let note = await ObsidianBridge.findNoteByFmgId(elementId);
|
|
|
|
if (note) {
|
|
INFO && console.log("Found note by FMG ID:", note.path);
|
|
closeDialogs("#obsidianNoteLoading");
|
|
// Show dialog with option to open linked note or choose different one
|
|
return await showLinkedNoteDialog(note, elementId, elementType, coordinates);
|
|
}
|
|
|
|
// Find by coordinates
|
|
const matches = await ObsidianBridge.findNotesByCoordinates(x, y, 8);
|
|
|
|
closeDialogs("#obsidianNoteLoading");
|
|
|
|
if (matches.length === 0) {
|
|
// No matches - offer to create new note
|
|
return await promptCreateNewNote(elementId, elementType, coordinates);
|
|
}
|
|
|
|
if (matches.length === 1) {
|
|
// Single match - show dialog with option to use it or choose different one
|
|
const match = matches[0];
|
|
const content = await ObsidianBridge.getNote(match.path);
|
|
const noteData = {
|
|
path: match.path,
|
|
name: match.name,
|
|
content,
|
|
frontmatter: match.frontmatter
|
|
};
|
|
return await showSingleMatchDialog(noteData, elementId, elementType, coordinates);
|
|
}
|
|
|
|
// Multiple matches - show selection dialog
|
|
return await showNoteSelectionDialog(matches, elementId, elementType, coordinates);
|
|
}
|
|
|
|
async function showLinkedNoteDialog(note, elementId, elementType, coordinates) {
|
|
return new Promise((resolve, reject) => {
|
|
alertMessage.innerHTML = `
|
|
<div style="padding: 1em;">
|
|
<p style="margin-bottom: 1em;"><strong>✓ Found linked note:</strong></p>
|
|
<div style="padding: 12px; background: #f0f8ff; border: 1px solid #0066cc; border-radius: 4px; margin-bottom: 1.5em;">
|
|
<div style="font-weight: bold; margin-bottom: 4px;">${note.name}</div>
|
|
<div style="font-size: 0.9em; color: #666;">Path: ${note.path}</div>
|
|
</div>
|
|
<p style="font-size: 0.9em; color: #666;">This element is already linked to the note above. You can open it or choose a different note.</p>
|
|
</div>
|
|
`;
|
|
|
|
$("#alert").dialog({
|
|
title: "Linked Note Found",
|
|
width: "500px",
|
|
buttons: {
|
|
"Open Linked Note": function () {
|
|
$(this).dialog("close");
|
|
resolve(note);
|
|
},
|
|
"Choose Different Note": async function () {
|
|
$(this).dialog("close");
|
|
try {
|
|
const differentNote = await promptCreateNewNote(elementId, elementType, coordinates);
|
|
resolve(differentNote);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
reject(new Error("Cancelled"));
|
|
}
|
|
},
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function showSingleMatchDialog(note, elementId, elementType, coordinates) {
|
|
return new Promise((resolve, reject) => {
|
|
alertMessage.innerHTML = `
|
|
<div style="padding: 1em;">
|
|
<p style="margin-bottom: 1em;"><strong>✓ Found note by coordinates:</strong></p>
|
|
<div style="padding: 12px; background: #f0fff0; border: 1px solid #00aa00; border-radius: 4px; margin-bottom: 1.5em;">
|
|
<div style="font-weight: bold; margin-bottom: 4px;">${note.name}</div>
|
|
<div style="font-size: 0.9em; color: #666;">Path: ${note.path}</div>
|
|
</div>
|
|
<p style="font-size: 0.9em; color: #666;">Found a note near this location. You can use it or browse for a different one.</p>
|
|
</div>
|
|
`;
|
|
|
|
$("#alert").dialog({
|
|
title: "Note Found Nearby",
|
|
width: "500px",
|
|
buttons: {
|
|
"Use This Note": function () {
|
|
$(this).dialog("close");
|
|
resolve(note);
|
|
},
|
|
"Browse/Search": async function () {
|
|
$(this).dialog("close");
|
|
try {
|
|
const differentNote = await promptCreateNewNote(elementId, elementType, coordinates);
|
|
resolve(differentNote);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
reject(new Error("Cancelled"));
|
|
}
|
|
},
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
});
|
|
}
|
|
|
|
function showLoadingDialog() {
|
|
alertMessage.innerHTML = `
|
|
<div style="text-align: center; padding: 2em;">
|
|
<div class="spinner" style="margin: 0 auto 1em;"></div>
|
|
<p>Searching Obsidian vault for matching notes...</p>
|
|
</div>
|
|
`;
|
|
|
|
$("#alert").dialog({
|
|
title: "Loading from Obsidian",
|
|
width: "400px",
|
|
closeOnEscape: false,
|
|
buttons: {},
|
|
dialogClass: "no-close",
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
}
|
|
|
|
async function showNoteSelectionDialog(matches, elementId, elementType, coordinates) {
|
|
return new Promise((resolve, reject) => {
|
|
const matchList = matches
|
|
.map(
|
|
(match, index) => `
|
|
<div class="note-match" data-index="${index}" style="
|
|
padding: 12px;
|
|
margin: 8px 0;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
" onmouseover="this.style.background='#f0f0f0'" onmouseout="this.style.background='white'">
|
|
<div style="font-weight: bold; margin-bottom: 4px;">${match.name}</div>
|
|
<div style="font-size: 0.9em; color: #666;">
|
|
Distance: ${match.distance.toFixed(1)} units<br/>
|
|
Coordinates: (${match.coordinates.x}, ${match.coordinates.y})<br/>
|
|
Path: ${match.path}
|
|
</div>
|
|
</div>
|
|
`
|
|
)
|
|
.join("");
|
|
|
|
alertMessage.innerHTML = `
|
|
<div style="max-height: 60vh; overflow-y: auto;">
|
|
<p style="margin-bottom: 1em;">Found ${matches.length} notes near this location. Select one:</p>
|
|
${matchList}
|
|
</div>
|
|
`;
|
|
|
|
$("#alert").dialog({
|
|
title: "Select Obsidian Note",
|
|
width: "600px",
|
|
buttons: {
|
|
"Create New": async function () {
|
|
$(this).dialog("close");
|
|
try {
|
|
const newNote = await promptCreateNewNote(elementId, elementType, coordinates);
|
|
resolve(newNote);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
reject(new Error("Cancelled"));
|
|
}
|
|
},
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
|
|
// Add click handlers to matches
|
|
document.querySelectorAll(".note-match").forEach((el, index) => {
|
|
el.addEventListener("click", async () => {
|
|
$("#alert").dialog("close");
|
|
try {
|
|
const match = matches[index];
|
|
const content = await ObsidianBridge.getNote(match.path);
|
|
resolve({
|
|
path: match.path,
|
|
name: match.name,
|
|
content,
|
|
frontmatter: match.frontmatter
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function promptCreateNewNote(elementId, elementType, coordinates) {
|
|
return new Promise((resolve, reject) => {
|
|
const element = getElementData(elementId, elementType);
|
|
const suggestedName = element.name || `${elementType}-${element.i}`;
|
|
|
|
// Build context info for the element
|
|
let contextInfo = "";
|
|
if (element.state) {
|
|
contextInfo += `<div style="color: #666; font-size: 0.9em;">State: ${element.state}</div>`;
|
|
}
|
|
if (element.province) {
|
|
contextInfo += `<div style="color: #666; font-size: 0.9em;">Province: ${element.province}</div>`;
|
|
}
|
|
|
|
// Pre-fill search with state or element name
|
|
const defaultSearch = element.state || element.name || "";
|
|
|
|
alertMessage.innerHTML = `
|
|
<div style="margin-bottom: 1.5em;">
|
|
<p><strong>${element.name || elementId}</strong></p>
|
|
${contextInfo}
|
|
<p style="margin-top: 0.5em;">No matching notes found by coordinates.</p>
|
|
</div>
|
|
|
|
<div style="margin: 1.5em 0; padding: 1em; background: #f5f5f5; border-radius: 4px;">
|
|
<label for="obsidianSearch" style="display: block; margin-bottom: 0.5em; font-weight: bold;">Search your vault:</label>
|
|
<input id="obsidianSearch" type="text" placeholder="Type to search..." value="${defaultSearch}" style="width: 100%; padding: 8px; font-size: 1em; margin-bottom: 8px;"/>
|
|
<button id="obsidianSearchBtn" style="padding: 6px 12px;">Search</button>
|
|
<button id="obsidianBrowseBtn" style="padding: 6px 12px; margin-left: 8px;">Browse All Notes</button>
|
|
<div id="obsidianSearchResults" style="margin-top: 1em; max-height: 200px; overflow-y: auto;"></div>
|
|
</div>
|
|
|
|
<div style="margin-top: 1.5em; padding-top: 1.5em; border-top: 1px solid #ddd;">
|
|
<p style="font-weight: bold; margin-bottom: 1em;">Or create a new note:</p>
|
|
<div style="margin: 1em 0;">
|
|
<label for="newNoteName" style="display: block; margin-bottom: 0.5em;">Note name:</label>
|
|
<input id="newNoteName" type="text" value="${suggestedName}" style="width: 100%; padding: 8px; font-size: 1em;"/>
|
|
</div>
|
|
<div style="margin: 1em 0;">
|
|
<label for="newNotePath" style="display: block; margin-bottom: 0.5em;">Folder (optional):</label>
|
|
<input id="newNotePath" type="text" placeholder="e.g., Locations/Cities" style="width: 100%; padding: 8px; font-size: 1em;"/>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$("#alert").dialog({
|
|
title: "Find or Create Note",
|
|
width: "600px",
|
|
buttons: {
|
|
Create: async function () {
|
|
const name = byId("newNoteName").value.trim();
|
|
const folder = byId("newNotePath").value.trim();
|
|
|
|
if (!name) {
|
|
tip("Please enter a note name", false, "error");
|
|
return;
|
|
}
|
|
|
|
const notePath = folder ? `${folder}/${name}.md` : `${name}.md`;
|
|
|
|
$(this).dialog("close");
|
|
|
|
try {
|
|
const template = ObsidianBridge.generateNoteTemplate(element, elementType, elementId);
|
|
await ObsidianBridge.createNote(notePath, template);
|
|
|
|
const {frontmatter} = ObsidianBridge.parseFrontmatter(template);
|
|
|
|
// Add to FMG ID index for instant future lookups
|
|
const fmgId = frontmatter["fmg-id"] || frontmatter.fmgId;
|
|
if (fmgId) {
|
|
ObsidianBridge.addToFmgIdIndex(fmgId, notePath);
|
|
INFO && console.log(`New note added to index: ${fmgId} → ${notePath}`);
|
|
}
|
|
|
|
resolve({
|
|
path: notePath,
|
|
name,
|
|
content: template,
|
|
frontmatter,
|
|
isNew: true
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
reject(new Error("Cancelled"));
|
|
}
|
|
},
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
|
|
// Add event handlers for search and browse
|
|
const searchBtn = byId("obsidianSearchBtn");
|
|
const browseBtn = byId("obsidianBrowseBtn");
|
|
const searchInput = byId("obsidianSearch");
|
|
const resultsDiv = byId("obsidianSearchResults");
|
|
|
|
const performSearch = async () => {
|
|
const query = searchInput.value.trim();
|
|
if (!query) {
|
|
resultsDiv.innerHTML = "<p style='color: #999;'>Enter a search term</p>";
|
|
return;
|
|
}
|
|
|
|
resultsDiv.innerHTML = "<p>Searching...</p>";
|
|
|
|
try {
|
|
const results = await ObsidianBridge.searchNotes(query);
|
|
|
|
if (results.length === 0) {
|
|
resultsDiv.innerHTML = "<p style='color: #999;'>No matching notes found</p>";
|
|
return;
|
|
}
|
|
|
|
resultsDiv.innerHTML = results
|
|
.map(
|
|
(note, index) => `
|
|
<div class="search-result" data-index="${index}" style="
|
|
padding: 8px;
|
|
margin: 4px 0;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
background: white;
|
|
" onmouseover="this.style.background='#e8e8e8'" onmouseout="this.style.background='white'">
|
|
<div style="font-weight: bold;">${note.name}</div>
|
|
<div style="font-size: 0.85em; color: #666;">${note.path}</div>
|
|
</div>
|
|
`
|
|
)
|
|
.join("");
|
|
|
|
// Add click handlers
|
|
document.querySelectorAll(".search-result").forEach((el, index) => {
|
|
el.addEventListener("click", async () => {
|
|
$("#alert").dialog("close");
|
|
try {
|
|
const note = results[index];
|
|
const content = await ObsidianBridge.getNote(note.path);
|
|
resolve({
|
|
path: note.path,
|
|
name: note.name,
|
|
content,
|
|
frontmatter: note.frontmatter
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
resultsDiv.innerHTML = `<p style='color: red;'>Search failed: ${error.message}</p>`;
|
|
}
|
|
};
|
|
|
|
const showBrowse = async () => {
|
|
resultsDiv.innerHTML = "<p>Loading file list...</p>";
|
|
|
|
try {
|
|
// Use fast path-only listing (doesn't read file contents)
|
|
const allNotes = await ObsidianBridge.listAllNotePaths();
|
|
|
|
if (allNotes.length === 0) {
|
|
resultsDiv.innerHTML = "<p style='color: #999;'>No notes in vault</p>";
|
|
return;
|
|
}
|
|
|
|
INFO && console.log(`Displaying ${allNotes.length} notes in folder tree`);
|
|
|
|
// Build folder tree
|
|
const tree = buildFolderTree(allNotes);
|
|
resultsDiv.innerHTML = renderFolderTree(tree, allNotes);
|
|
|
|
// Add click handlers to files
|
|
document.querySelectorAll(".tree-file").forEach(el => {
|
|
el.addEventListener("click", async () => {
|
|
const index = parseInt(el.dataset.index);
|
|
$("#alert").dialog("close");
|
|
try {
|
|
const note = allNotes[index];
|
|
// Read the file content only when clicked
|
|
const content = await ObsidianBridge.getNote(note.path);
|
|
const {frontmatter} = ObsidianBridge.parseFrontmatter(content);
|
|
|
|
resolve({
|
|
path: note.path,
|
|
name: note.name,
|
|
content,
|
|
frontmatter
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add click handlers to folder toggles
|
|
document.querySelectorAll(".tree-folder-toggle").forEach(el => {
|
|
el.addEventListener("click", e => {
|
|
e.stopPropagation();
|
|
const folder = el.parentElement.nextElementSibling;
|
|
const isCollapsed = folder.style.display === "none";
|
|
folder.style.display = isCollapsed ? "block" : "none";
|
|
el.textContent = isCollapsed ? "▼" : "▶";
|
|
});
|
|
});
|
|
} catch (error) {
|
|
resultsDiv.innerHTML = `<p style='color: red;'>Failed to load notes: ${error.message}</p>`;
|
|
}
|
|
};
|
|
|
|
searchBtn.addEventListener("click", performSearch);
|
|
browseBtn.addEventListener("click", showBrowse);
|
|
searchInput.addEventListener("keypress", e => {
|
|
if (e.key === "Enter") performSearch();
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildFolderTree(notes) {
|
|
const root = {folders: {}, files: []};
|
|
|
|
INFO && console.log(`buildFolderTree: Processing ${notes.length} notes`);
|
|
|
|
notes.forEach((note, index) => {
|
|
const parts = note.path.split("/");
|
|
const fileName = parts[parts.length - 1];
|
|
|
|
DEBUG && console.log(`Processing note ${index}: ${note.path} (${parts.length} parts)`);
|
|
|
|
if (parts.length === 1) {
|
|
// Root level file
|
|
root.files.push({name: fileName, index, path: note.path});
|
|
DEBUG && console.log(` -> Added to root files: ${fileName}`);
|
|
} else {
|
|
// Navigate/create folder structure
|
|
let current = root;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const folderName = parts[i];
|
|
if (!current.folders[folderName]) {
|
|
current.folders[folderName] = {folders: {}, files: []};
|
|
DEBUG && console.log(` -> Created folder: ${folderName}`);
|
|
}
|
|
current = current.folders[folderName];
|
|
}
|
|
// Add file to final folder
|
|
current.files.push({name: fileName, index, path: note.path});
|
|
DEBUG && console.log(` -> Added to folder: ${fileName}`);
|
|
}
|
|
});
|
|
|
|
INFO && console.log("Folder tree structure:", root);
|
|
|
|
return root;
|
|
}
|
|
|
|
function renderFolderTree(node, allNotes, indent = 0) {
|
|
let html = "";
|
|
const indentPx = indent * 20;
|
|
|
|
// Render folders
|
|
for (const [folderName, folderData] of Object.entries(node.folders || {})) {
|
|
html += `
|
|
<div style="margin-left: ${indentPx}px;">
|
|
<div style="padding: 4px; cursor: pointer; user-select: none;">
|
|
<span class="tree-folder-toggle" style="display: inline-block; width: 16px; font-size: 12px;">▼</span>
|
|
<span style="font-weight: bold;">📁 ${folderName}</span>
|
|
</div>
|
|
<div class="tree-folder-content" style="display: block;">
|
|
${renderFolderTree(folderData, allNotes, indent + 1)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Render files in current folder
|
|
html += renderFiles(node.files || [], indent);
|
|
|
|
return html;
|
|
}
|
|
|
|
function renderFiles(files, indent) {
|
|
const indentPx = indent * 20;
|
|
return files
|
|
.map(
|
|
file => `
|
|
<div class="tree-file" data-index="${file.index}" style="
|
|
margin-left: ${indentPx}px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
" onmouseover="this.style.background='#e8e8e8'" onmouseout="this.style.background='transparent'">
|
|
<span style="font-size: 12px;">📄</span> ${file.name.replace(".md", "")}
|
|
</div>
|
|
`
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function getElementData(elementId, elementType) {
|
|
// Extract element data based on type
|
|
if (elementType === "burg") {
|
|
const burgId = parseInt(elementId.replace("burg", ""));
|
|
const burg = pack.burgs[burgId];
|
|
|
|
// Enhance with state and province names
|
|
const stateId = burg.state;
|
|
const provinceId = burg.province;
|
|
|
|
return {
|
|
...burg,
|
|
state: stateId && pack.states[stateId] ? pack.states[stateId].name : null,
|
|
province: provinceId && pack.provinces[provinceId] ? pack.provinces[provinceId].name : null
|
|
};
|
|
} else if (elementType === "marker") {
|
|
const markerId = parseInt(elementId.replace("marker", ""));
|
|
const marker = pack.markers[markerId];
|
|
|
|
// Enhance with state and province if marker has a cell
|
|
if (marker.cell) {
|
|
const cell = pack.cells;
|
|
const stateId = cell.state[marker.cell];
|
|
const provinceId = cell.province[marker.cell];
|
|
|
|
return {
|
|
...marker,
|
|
state: stateId && pack.states[stateId] ? pack.states[stateId].name : null,
|
|
province: provinceId && pack.provinces[provinceId] ? pack.provinces[provinceId].name : null
|
|
};
|
|
}
|
|
|
|
return marker;
|
|
} else {
|
|
// Generic element
|
|
const el = document.getElementById(elementId);
|
|
return {
|
|
id: elementId,
|
|
name: elementId,
|
|
x: parseFloat(el?.getAttribute("cx") || 0),
|
|
y: parseFloat(el?.getAttribute("cy") || 0)
|
|
};
|
|
}
|
|
}
|
|
|
|
function showMarkdownEditor(noteData, elementType, elementId, coordinates) {
|
|
const {path, name, content, frontmatter, isNew} = noteData;
|
|
|
|
// Extract frontmatter and body
|
|
const {content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
|
|
|
// Set up the dialog
|
|
byId("obsidianNotePath").textContent = path;
|
|
byId("obsidianNoteName").value = name;
|
|
byId("obsidianMarkdownEditor").value = content;
|
|
byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent);
|
|
|
|
// Store current note data and FMG element info
|
|
showMarkdownEditor.currentNote = noteData;
|
|
showMarkdownEditor.originalContent = content;
|
|
showMarkdownEditor.elementId = elementId;
|
|
showMarkdownEditor.elementType = elementType;
|
|
showMarkdownEditor.coordinates = coordinates;
|
|
|
|
$("#obsidianNotesEditor").dialog({
|
|
title: `Obsidian Note: ${name}`,
|
|
width: Math.min(svgWidth * 0.9, 1200),
|
|
height: svgHeight * 0.85,
|
|
position: {my: "center", at: "center", of: "svg"},
|
|
close: () => {
|
|
showMarkdownEditor.currentNote = null;
|
|
showMarkdownEditor.originalContent = null;
|
|
}
|
|
});
|
|
|
|
// Update preview on edit
|
|
updateMarkdownPreview();
|
|
|
|
if (isNew) {
|
|
tip("New note created in Obsidian vault", true, "success", 3000);
|
|
}
|
|
}
|
|
|
|
function updateMarkdownPreview() {
|
|
const content = byId("obsidianMarkdownEditor").value;
|
|
const {content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
|
byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent);
|
|
}
|
|
|
|
function renderMarkdown(markdown) {
|
|
// Simple Markdown renderer (will be replaced with marked.js)
|
|
let html = markdown;
|
|
|
|
// Headers
|
|
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
|
|
// Bold
|
|
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
html = html.replace(/\_\_(.*?)\_\_/g, "<strong>$1</strong>");
|
|
|
|
// Italic
|
|
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
|
html = html.replace(/\_(.*?)\_/g, "<em>$1</em>");
|
|
|
|
// Links
|
|
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
|
|
// Wikilinks [[Page]]
|
|
html = html.replace(/\[\[([^\]]+)\]\]/g, '<span class="wikilink">$1</span>');
|
|
|
|
// Lists
|
|
html = html.replace(/^\* (.*)$/gim, "<li>$1</li>");
|
|
html = html.replace(/^\- (.*)$/gim, "<li>$1</li>");
|
|
html = html.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>");
|
|
|
|
// Paragraphs
|
|
html = html.replace(/\n\n/g, "</p><p>");
|
|
html = "<p>" + html + "</p>";
|
|
|
|
// Clean up
|
|
html = html.replace(/<p><\/p>/g, "");
|
|
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
|
|
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
|
|
|
|
return html;
|
|
}
|
|
|
|
async function saveObsidianNote() {
|
|
if (!showMarkdownEditor.currentNote) {
|
|
tip("No note loaded", false, "error");
|
|
return;
|
|
}
|
|
|
|
let content = byId("obsidianMarkdownEditor").value;
|
|
const {path} = showMarkdownEditor.currentNote;
|
|
const elementId = showMarkdownEditor.elementId;
|
|
const coordinates = showMarkdownEditor.coordinates;
|
|
|
|
// Update/add frontmatter with FMG ID and coordinates
|
|
if (elementId && coordinates) {
|
|
content = updateFrontmatterWithFmgData(content, elementId, coordinates);
|
|
}
|
|
|
|
try {
|
|
await ObsidianBridge.updateNote(path, content);
|
|
|
|
// Update the FMG ID index if this note has an fmg-id
|
|
if (elementId) {
|
|
const {frontmatter} = ObsidianBridge.parseFrontmatter(content);
|
|
const fmgId = frontmatter["fmg-id"] || frontmatter.fmgId;
|
|
if (fmgId) {
|
|
// Add to index using internal method
|
|
ObsidianBridge.addToFmgIdIndex(fmgId, path);
|
|
}
|
|
}
|
|
|
|
showMarkdownEditor.originalContent = content;
|
|
// Update the editor to show the new frontmatter
|
|
byId("obsidianMarkdownEditor").value = content;
|
|
tip("Note saved to Obsidian vault (linked to FMG element)", true, "success", 3000);
|
|
} catch (error) {
|
|
ERROR && console.error("Failed to save note:", error);
|
|
tip("Failed to save note: " + error.message, true, "error", 5000);
|
|
}
|
|
}
|
|
|
|
function updateFrontmatterWithFmgData(content, elementId, coordinates) {
|
|
const {x, y} = coordinates;
|
|
const {frontmatter, content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
|
|
|
// Update frontmatter with FMG data
|
|
frontmatter["fmg-id"] = elementId;
|
|
frontmatter["x"] = Math.round(x * 100) / 100;
|
|
frontmatter["y"] = Math.round(y * 100) / 100;
|
|
|
|
// Rebuild frontmatter
|
|
let frontmatterLines = ["---"];
|
|
for (const [key, value] of Object.entries(frontmatter)) {
|
|
if (typeof value === "object" && value !== null) {
|
|
// Handle nested objects
|
|
frontmatterLines.push(`${key}:`);
|
|
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
frontmatterLines.push(` ${nestedKey}: ${nestedValue}`);
|
|
}
|
|
} else {
|
|
frontmatterLines.push(`${key}: ${value}`);
|
|
}
|
|
}
|
|
frontmatterLines.push("---");
|
|
|
|
return frontmatterLines.join("\n") + "\n" + bodyContent;
|
|
}
|
|
|
|
function openInObsidian() {
|
|
if (!showMarkdownEditor.currentNote) return;
|
|
|
|
const {path} = showMarkdownEditor.currentNote;
|
|
const vaultName = ObsidianBridge.config.vaultName || "vault";
|
|
const obsidianUrl = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(path)}`;
|
|
|
|
window.open(obsidianUrl, "_blank");
|
|
tip("Opening in Obsidian app...", false, "success", 2000);
|
|
}
|
|
|
|
function togglePreviewMode() {
|
|
const editor = byId("obsidianMarkdownEditor");
|
|
const preview = byId("obsidianMarkdownPreview");
|
|
const isPreviewMode = editor.style.display === "none";
|
|
|
|
if (isPreviewMode) {
|
|
editor.style.display = "block";
|
|
preview.style.display = "none";
|
|
byId("togglePreview").textContent = "👁 Preview";
|
|
} else {
|
|
updateMarkdownPreview();
|
|
editor.style.display = "none";
|
|
preview.style.display = "block";
|
|
byId("togglePreview").textContent = "✏ Edit";
|
|
}
|
|
}
|