"use strict";
// Modern Markdown Notes Editor with Obsidian Integration
function editObsidianNote(elementId, elementType, coordinates) {
const {x, y} = coordinates;
// Show loading dialog
showLoadingDialog();
// Try to find note by FMG ID first, then by coordinates
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");
});
}
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);
return note;
}
// 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 - load it
const match = matches[0];
const content = await ObsidianBridge.getNote(match.path);
return {
path: match.path,
name: match.name,
content,
frontmatter: match.frontmatter
};
}
// Multiple matches - show selection dialog
return await showNoteSelectionDialog(matches, elementId, elementType, coordinates);
}
function showLoadingDialog() {
alertMessage.innerHTML = `
Searching Obsidian vault for matching notes...
`;
$("#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) => `
${match.name}
Distance: ${match.distance.toFixed(1)} units
Coordinates: (${match.coordinates.x}, ${match.coordinates.y})
Path: ${match.path}
`
)
.join("");
alertMessage.innerHTML = `
Found ${matches.length} notes near this location. Select one:
${matchList}
`;
$("#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 += `State: ${element.state}
`;
}
if (element.province) {
contextInfo += `Province: ${element.province}
`;
}
// Pre-fill search with state or element name
const defaultSearch = element.state || element.name || "";
alertMessage.innerHTML = `
${element.name || elementId}
${contextInfo}
No matching notes found by coordinates.
`;
$("#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);
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 = "Enter a search term
";
return;
}
resultsDiv.innerHTML = "Searching...
";
try {
const results = await ObsidianBridge.searchNotes(query);
if (results.length === 0) {
resultsDiv.innerHTML = "No matching notes found
";
return;
}
resultsDiv.innerHTML = results
.map(
(note, index) => `
${note.name}
${note.path}
`
)
.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 = `Search failed: ${error.message}
`;
}
};
const showBrowse = async () => {
resultsDiv.innerHTML = "Loading all notes...
";
try {
const allNotes = await ObsidianBridge.listAllNotes();
if (allNotes.length === 0) {
resultsDiv.innerHTML = "No notes in vault
";
return;
}
resultsDiv.innerHTML = allNotes
.map(
(note, index) => `
${note.name}
${note.path}
`
)
.join("");
// Add click handlers
document.querySelectorAll(".browse-result").forEach((el, index) => {
el.addEventListener("click", async () => {
$("#alert").dialog("close");
try {
const note = allNotes[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 = `Failed to load notes: ${error.message}
`;
}
};
searchBtn.addEventListener("click", performSearch);
browseBtn.addEventListener("click", showBrowse);
searchInput.addEventListener("keypress", e => {
if (e.key === "Enter") performSearch();
});
});
}
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, "$1
");
html = html.replace(/^## (.*$)/gim, "$1
");
html = html.replace(/^# (.*$)/gim, "$1
");
// Bold
html = html.replace(/\*\*(.*?)\*\*/g, "$1");
html = html.replace(/\_\_(.*?)\_\_/g, "$1");
// Italic
html = html.replace(/\*(.*?)\*/g, "$1");
html = html.replace(/\_(.*?)\_/g, "$1");
// Links
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1');
// Wikilinks [[Page]]
html = html.replace(/\[\[([^\]]+)\]\]/g, '$1');
// Lists
html = html.replace(/^\* (.*)$/gim, "$1");
html = html.replace(/^\- (.*)$/gim, "$1");
html = html.replace(/(.*<\/li>)/s, "");
// Paragraphs
html = html.replace(/\n\n/g, "");
html = "
" + html + "
";
// Clean up
html = html.replace(/<\/p>/g, "");
html = html.replace(/
()/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);
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";
}
}