Files
doxa/doxa-notes/desktop_app_dev/build/assets/js/script.js
T
2026-08-07 09:48:59 +02:00

1622 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
"use strict";
// Detect Node (fs) availability (Electron / Node) and provide a browser fallback
let nodeFS = false;
let fsModule = null;
let pathModule = null;
let dataFilePath = null;
const browserDataUrl = "assets/js/data/doxa.json"; // relative to index.html
try {
fsModule = require("fs");
pathModule = require("path");
nodeFS = true;
dataFilePath = pathModule.resolve(__dirname, "data", "doxa.json");
} catch (err) {
nodeFS = false;
}
// In-memory state
window.notes = window.notes || [];
window.currentNoteIndex = window.currentNoteIndex || null;
let modalCurrentIndex = null;
function log() {
if (console && console.log) console.log.apply(console, arguments);
}
// Theme handling: store preference in localStorage and toggle body.dark
const THEME_KEY = "doxa_theme";
let systemMediaQuery = null;
let systemMediaListener = null;
function applyTheme(theme) {
try {
// remove previous system listener if any
if (systemMediaQuery && systemMediaListener) {
try {
if (systemMediaQuery.removeEventListener)
systemMediaQuery.removeEventListener("change", systemMediaListener);
else systemMediaQuery.removeListener(systemMediaListener);
} catch (e) {}
systemMediaQuery = null;
systemMediaListener = null;
}
if (theme === "dark") {
document.body.classList.add("dark");
} else if (theme === "light") {
document.body.classList.remove("dark");
} else {
// system: apply based on current system setting and listen for changes
try {
const mql = window.matchMedia("(prefers-color-scheme: dark)");
if (mql.matches) document.body.classList.add("dark");
else document.body.classList.remove("dark");
systemMediaQuery = mql;
systemMediaListener = function (e) {
if (e.matches) document.body.classList.add("dark");
else document.body.classList.remove("dark");
const btn = document.getElementById("themeToggle");
if (btn) {
const currentSys = e.matches ? "tmavý" : "světlý";
btn.setAttribute(
"aria-label",
`Respektovat systém (aktuálně: ${currentSys})`,
);
}
};
if (mql.addEventListener)
mql.addEventListener("change", systemMediaListener);
else mql.addListener(systemMediaListener);
} catch (e) {
// fallback
document.body.classList.remove("dark");
}
}
} catch (e) {
/* ignore */
}
const btn = document.getElementById("themeToggle");
if (btn) {
let icon = "🌓";
let aria = "Respektovat systém";
if (theme === "dark") {
icon = "🌙";
aria = "Tmavý režim (kliknutím přepnete)";
} else if (theme === "light") {
icon = "☀️";
aria = "Světlý režim (kliknutím přepnete)";
} else {
const cur = document.body.classList.contains("dark")
? "tmavý"
: "světlý";
icon = "🌓";
aria = `Respektovat systém (aktuálně: ${cur})`;
}
btn.textContent = icon;
btn.setAttribute("aria-label", aria);
}
}
function detectSystemTheme() {
try {
return window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
} catch (e) {
return "light";
}
}
function getStoredTheme() {
try {
return localStorage.getItem(THEME_KEY);
} catch (e) {
return null;
}
}
function setStoredTheme(value) {
try {
localStorage.setItem(THEME_KEY, value);
} catch (e) {}
}
function initTheme() {
const stored = getStoredTheme();
const theme = stored !== null ? stored : "system";
applyTheme(theme);
}
function toggleTheme() {
const stored = getStoredTheme() || "system";
const order = ["light", "dark", "system"];
let idx = order.indexOf(stored);
if (idx === -1) idx = 2;
const next = order[(idx + 1) % order.length];
setStoredTheme(next);
applyTheme(next);
}
// end theme handling
// --- Markdown editor helpers (toolbar + preview) ---
let mdEngine = null;
function ensureMarkdown() {
if (mdEngine) return mdEngine;
if (window.markdownit) {
try {
mdEngine = window.markdownit({
html: false,
linkify: true,
typographer: true,
});
} catch (e) {
mdEngine = null;
}
return mdEngine;
}
// Fallback simple markdown renderer (safe, offline)
function escapeHtml(str) {
if (str == null) return "";
return String(str)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function escapeAttr(str) {
return escapeHtml(str).replace(/\"/g, "&quot;");
}
function inlineTokenize(s) {
const tokens = [];
function push(html) {
const id = "%%T" + tokens.length + "%%";
tokens.push(html);
return id;
}
// images
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (m, alt, url) =>
push(
'<img src="' + escapeAttr(url) + '" alt="' + escapeHtml(alt) + '">',
),
);
// links
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, text, url) =>
push('<a href="' + escapeAttr(url) + '">' + escapeHtml(text) + "</a>"),
);
// inline code
s = s.replace(/`([^`]+)`/g, (m, code) =>
push("<code>" + escapeHtml(code) + "</code>"),
);
// bold
s = s.replace(/\*\*(.+?)\*\*/g, (m, t) =>
push("<strong>" + escapeHtml(t) + "</strong>"),
);
// italic
s = s.replace(/\*(.+?)\*/g, (m, t) =>
push("<em>" + escapeHtml(t) + "</em>"),
);
// strike
s = s.replace(/~~(.+?)~~/g, (m, t) =>
push("<del>" + escapeHtml(t) + "</del>"),
);
// escape remaining text
let escaped = escapeHtml(s);
// replace placeholders
tokens.forEach((html, i) => {
escaped = escaped.replace("%%T" + i + "%%", html);
});
return escaped;
}
function simpleMarkdownRender(src) {
if (!src) return "";
const lines = String(src).replace(/\r\n/g, "\n").split("\n");
let i = 0,
out = "";
const n = lines.length;
while (i < n) {
let line = lines[i];
// code block
if (/^```/.test(line)) {
const lang = line.slice(3).trim();
let code = "";
i++;
while (i < n && !/^```/.test(lines[i])) {
code += lines[i] + "\n";
i++;
}
i++; // skip closing
out += "<pre><code>" + escapeHtml(code) + "</code></pre>\n";
continue;
}
// heading
const h = line.match(/^\s*(#{1,6})\s+(.*)$/);
if (h) {
out +=
"<h" +
h[1].length +
">" +
inlineTokenize(h[2]) +
"</h" +
h[1].length +
">\n";
i++;
continue;
}
// hr
if (/^\s*-{3,}\s*$/.test(line)) {
out += "<hr>";
i++;
continue;
}
// table detection: line with | and next line with - or :
if (
line.indexOf("|") !== -1 &&
i + 1 < n &&
/[-:\s|]+/.test(lines[i + 1]) &&
lines[i + 1].indexOf("-") !== -1
) {
const headerParts = line
.split("|")
.map((s) => s.trim())
.filter((s) => s.length > 0);
const j = i + 2;
const rows = [];
let k = j;
while (k < n && lines[k].indexOf("|") !== -1) {
rows.push(lines[k]);
k++;
}
out +=
"<table><thead><tr>" +
headerParts
.map((hp) => "<th>" + inlineTokenize(hp) + "</th>")
.join("") +
"</tr></thead>";
if (rows.length) {
out +=
"<tbody>" +
rows
.map((r) => {
const cells = r
.split("|")
.map((c) => c.trim())
.filter((_, idx) => idx > 0 || true);
return (
"<tr>" +
cells
.map((c) => "<td>" + inlineTokenize(c) + "</td>")
.join("") +
"</tr>"
);
})
.join("") +
"</tbody>";
}
out += "</table>\n";
i = k;
continue;
}
// unordered list
if (/^\s*[-*]\s+/.test(line)) {
out += "<ul>";
while (i < n && /^\s*([-*])\s+/.test(lines[i])) {
const m = lines[i].replace(/^\s*[-*]\s+/, "");
out += "<li>" + inlineTokenize(m) + "</li>";
i++;
}
out += "</ul>\n";
continue;
}
// ordered list
if (/^\s*\d+\.\s+/.test(line)) {
out += "<ol>";
while (i < n && /^\s*\d+\.\s+/.test(lines[i])) {
const m = lines[i].replace(/^\s*\d+\.\s+/, "");
out += "<li>" + inlineTokenize(m) + "</li>";
i++;
}
out += "</ol>\n";
continue;
}
// blockquote
if (/^\s*>\s?/.test(line)) {
let quote = "";
while (i < n && /^\s*>\s?/.test(lines[i])) {
quote += lines[i].replace(/^\s*>\s?/, "") + "\n";
i++;
}
out +=
"<blockquote>" + inlineTokenize(quote.trim()) + "</blockquote>\n";
continue;
}
// paragraph (collect until blank)
if (/^\s*$/.test(line)) {
i++;
continue;
}
let para = line;
i++;
while (
i < n &&
!/^\s*$/.test(lines[i]) &&
!/^(#{1,6})\s+/.test(lines[i]) &&
!/^```/.test(lines[i]) &&
lines[i].indexOf("|") === -1 &&
!/^\s*[-*]\s+/.test(lines[i]) &&
!/^\s*\d+\.\s+/.test(lines[i]) &&
!/^\s*>\s?/.test(lines[i])
) {
para += "\n" + lines[i];
i++;
}
out += "<p>" + inlineTokenize(para.trim()) + "</p>\n";
}
return out;
}
mdEngine = { render: simpleMarkdownRender };
return mdEngine;
}
function sanitizeHtml(html) {
if (window.DOMPurify && window.DOMPurify.sanitize)
return window.DOMPurify.sanitize(html);
return html;
}
function renderPreviewFor(textareaEl, previewEl) {
if (!textareaEl || !previewEl) return;
const md = ensureMarkdown();
if (!md) {
previewEl.innerText = "Preview není dostupný (chybí knihovna).";
return;
}
const raw = textareaEl.value || "";
let html = "";
try {
html = md.render(raw);
} catch (e) {
html = "<pre>" + String(raw) + "</pre>";
}
html = sanitizeHtml(html);
previewEl.innerHTML = html;
// syntax highlight if available
if (window.hljs && window.hljs.highlightElement) {
previewEl.querySelectorAll("pre code").forEach((block) => {
try {
window.hljs.highlightElement(block);
} catch (e) {}
});
}
}
function scheduleRender(textareaEl, previewEl, delay = 200) {
if (!previewEl) return;
try {
if (previewEl._renderTimer) clearTimeout(previewEl._renderTimer);
} catch (e) {}
previewEl._renderTimer = setTimeout(() => {
try {
renderPreviewFor(textareaEl, previewEl);
} catch (e) {}
previewEl._renderTimer = null;
}, delay);
}
// Live preview storage key and helpers
const LIVE_PREVIEW_KEY = "doxa_livepreview";
function getStoredLive() {
try {
return localStorage.getItem(LIVE_PREVIEW_KEY) === "1";
} catch (e) {
return false;
}
}
function setStoredLive(v) {
try {
localStorage.setItem(LIVE_PREVIEW_KEY, v ? "1" : "0");
} catch (e) {}
}
function applyLiveStateGlobally() {
const on = getStoredLive();
const mainLiveBtn = document.getElementById("livePreviewToggle");
const mainPreview = document.getElementById("notePreview");
const mainTextarea = document.getElementById("noteInput");
if (mainLiveBtn) {
if (on) mainLiveBtn.classList.add("active");
else mainLiveBtn.classList.remove("active");
}
if (on && mainPreview && mainTextarea) {
mainPreview.classList.remove("hidden");
mainPreview.setAttribute("aria-hidden", "false");
try {
scheduleRender(mainTextarea, mainPreview, 0);
} catch (e) {}
}
// modal (if present)
const modalLiveBtn = document.getElementById("livePreviewToggleModal");
const modalPreview = document.getElementById("modalNotePreview");
const modalTextarea = document.getElementById("modalNoteContent");
if (modalLiveBtn) {
if (on) modalLiveBtn.classList.add("active");
else modalLiveBtn.classList.remove("active");
}
if (on && modalPreview && modalTextarea) {
modalPreview.classList.remove("hidden");
modalPreview.setAttribute("aria-hidden", "false");
try {
scheduleRender(modalTextarea, modalPreview, 0);
} catch (e) {}
}
}
const AUTOSAVE_DELAY = 1000;
let autosaveTimer = null;
function scheduleAutosave() {
try {
if (autosaveTimer) clearTimeout(autosaveTimer);
} catch (e) {}
autosaveTimer = setTimeout(() => {
try {
saveNotesToFile(false);
} catch (err) {
/* ignore */
}
const statusEl = document.getElementById("saveStatus");
if (statusEl) {
statusEl.textContent = "Uloženo";
try {
if (window._saveStatusTimeout)
clearTimeout(window._saveStatusTimeout);
} catch (e) {}
window._saveStatusTimeout = setTimeout(() => {
statusEl.textContent = "";
}, 1500);
}
}, AUTOSAVE_DELAY);
}
function wrapSelection(textarea, before, after) {
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
const selected = value.slice(start, end) || "";
const newValue =
value.slice(0, start) +
before +
selected +
(after !== undefined ? after : before) +
value.slice(end);
textarea.value = newValue;
textarea.focus();
const cursorStart = start + before.length;
textarea.selectionStart = cursorStart;
textarea.selectionEnd = cursorStart + selected.length;
// trigger input event so autosave and model update run
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function insertCodeBlock(textarea) {
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
const selected = value.slice(start, end) || "";
if (!selected) {
const insert = "```\n\n```";
textarea.value = value.slice(0, start) + insert + value.slice(end);
textarea.focus();
const cursor = start + 4; // after ```\n
textarea.selectionStart = cursor;
textarea.selectionEnd = cursor;
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
} else {
const newValue =
value.slice(0, start) + "```\n" + selected + "\n```" + value.slice(end);
textarea.value = newValue;
textarea.focus();
textarea.selectionStart = start + 4;
textarea.selectionEnd = start + 4 + selected.length;
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
}
function insertLink(textarea) {
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
const selected = value.slice(start, end) || "odkaz";
const url = prompt("Zadejte URL (např. https://example.com):", "https://");
if (!url) return;
const newValue =
value.slice(0, start) +
"[" +
selected +
"](" +
url +
")" +
value.slice(end);
textarea.value = newValue;
textarea.focus();
const cursorStart = start + 1;
textarea.selectionStart = cursorStart;
textarea.selectionEnd = cursorStart + selected.length;
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function getLineInfo(textarea) {
const value = textarea.value;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const lineStart = value.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
let nextNewline = value.indexOf("\n", end);
if (nextNewline === -1) nextNewline = value.length;
const segment = value.slice(lineStart, nextNewline);
const lines = segment.split("\n");
return { value, start, end, lineStart, lineEnd: nextNewline, lines };
}
function insertHeading(textarea, level) {
if (!textarea) return;
const info = getLineInfo(textarea);
const lines = info.lines;
const prefix = "#".repeat(level) + " ";
const specificRegex = new RegExp("^\\s*" + "#".repeat(level) + "\\s+");
const anyHeadingRegex = /^\s*#{1,6}\s+/;
const allHaveSpecific = lines.every((l) => specificRegex.test(l));
let newLines;
if (allHaveSpecific)
newLines = lines.map((l) => l.replace(specificRegex, ""));
else newLines = lines.map((l) => prefix + l.replace(anyHeadingRegex, ""));
const newSegment = newLines.join("\n");
textarea.value =
info.value.slice(0, info.lineStart) +
newSegment +
info.value.slice(info.lineEnd);
textarea.selectionStart = info.lineStart;
textarea.selectionEnd = info.lineStart + newSegment.length;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function toggleUnorderedList(textarea) {
if (!textarea) return;
const info = getLineInfo(textarea);
const lines = info.lines;
const allHave = lines.every((l) => /^\s*([-*])\s+/.test(l));
let newLines;
if (allHave) newLines = lines.map((l) => l.replace(/^\s*([-*])\s+/, ""));
else
newLines = lines.map((l) =>
l.trim() === "" ? "- " : "- " + l.replace(/^\s*/, ""),
);
const newSegment = newLines.join("\n");
textarea.value =
info.value.slice(0, info.lineStart) +
newSegment +
info.value.slice(info.lineEnd);
textarea.selectionStart = info.lineStart;
textarea.selectionEnd = info.lineStart + newSegment.length;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function toggleOrderedList(textarea) {
if (!textarea) return;
const info = getLineInfo(textarea);
const lines = info.lines;
const allHave = lines.every((l) => /^\s*\d+\.\s+/.test(l));
let newLines;
if (allHave) newLines = lines.map((l) => l.replace(/^\s*\d+\.\s+/, ""));
else
newLines = lines.map((l, i) =>
l.trim() === "" ? i + 1 + ". " : i + 1 + ". " + l.replace(/^\s*/, ""),
);
const newSegment = newLines.join("\n");
textarea.value =
info.value.slice(0, info.lineStart) +
newSegment +
info.value.slice(info.lineEnd);
textarea.selectionStart = info.lineStart;
textarea.selectionEnd = info.lineStart + newSegment.length;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function toggleBlockquote(textarea) {
if (!textarea) return;
const info = getLineInfo(textarea);
const lines = info.lines;
const allHave = lines.every((l) => /^\s*>\s?/.test(l));
let newLines;
if (allHave) newLines = lines.map((l) => l.replace(/^\s*>\s?/, ""));
else
newLines = lines.map((l) =>
l.trim() === "" ? "> " : "> " + l.replace(/^\s*/, ""),
);
const newSegment = newLines.join("\n");
textarea.value =
info.value.slice(0, info.lineStart) +
newSegment +
info.value.slice(info.lineEnd);
textarea.selectionStart = info.lineStart;
textarea.selectionEnd = info.lineStart + newSegment.length;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function insertHr(textarea) {
if (!textarea) return;
const pos = textarea.selectionStart;
const value = textarea.value;
const insert = "\n---\n";
textarea.value = value.slice(0, pos) + insert + value.slice(pos);
textarea.selectionStart = pos + insert.length;
textarea.selectionEnd = textarea.selectionStart;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function insertImage(textarea) {
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
const selected = value.slice(start, end) || "";
const url = prompt("Adresa obrázku (URL):", "https://");
if (!url) return;
const alt = selected || prompt("Alt text (krátký popis):", "");
const insert = "![" + (alt || "") + "](" + url + ")";
textarea.value = value.slice(0, start) + insert + value.slice(end);
textarea.selectionStart = start + insert.length;
textarea.selectionEnd = textarea.selectionStart;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function toggleTaskList(textarea) {
if (!textarea) return;
const info = getLineInfo(textarea);
const lines = info.lines;
const allHave = lines.every((l) => /^\s*-\s*\[[ xX]\]\s+/.test(l));
let newLines;
if (allHave)
newLines = lines.map((l) => l.replace(/^\s*-\s*\[[ xX]\]\s+/, ""));
else
newLines = lines.map((l) =>
l.trim() === "" ? "- [ ] " : "- [ ] " + l.replace(/^\s*/, ""),
);
const newSegment = newLines.join("\n");
textarea.value =
info.value.slice(0, info.lineStart) +
newSegment +
info.value.slice(info.lineEnd);
textarea.selectionStart = info.lineStart;
textarea.selectionEnd = info.lineStart + newSegment.length;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function insertTable(textarea) {
if (!textarea) return;
const colsStr = prompt("Počet sloupců (např. 2):", "2");
if (!colsStr) return;
const rowsStr = prompt("Počet řádků (bez hlavičky, např. 2):", "2");
if (!rowsStr) return;
const cols = Math.max(1, parseInt(colsStr, 10) || 2);
const rows = Math.max(0, parseInt(rowsStr, 10) || 2);
// build header
const headers = [];
const aligns = [];
for (let c = 1; c <= cols; c++) {
headers.push("Header " + c);
aligns.push("---");
}
const headerLine = "| " + headers.join(" | ") + " |\n";
const alignLine = "| " + aligns.join(" | ") + " |\n";
let body = "";
for (let r = 0; r < rows; r++) {
const cells = [];
for (let c = 0; c < cols; c++) cells.push("");
body += "| " + cells.join(" | ") + " |\n";
}
const insert = headerLine + alignLine + body;
const pos = textarea.selectionStart || textarea.value.length;
textarea.value =
textarea.value.slice(0, pos) + insert + textarea.value.slice(pos);
textarea.selectionStart = pos + insert.length;
textarea.selectionEnd = textarea.selectionStart;
textarea.focus();
try {
textarea.dispatchEvent(new Event("input", { bubbles: true }));
} catch (e) {
try {
textarea.dispatchEvent(new Event("input"));
} catch (e) {}
}
}
function togglePreview(textareaId, previewId) {
const textarea = document.getElementById(textareaId);
const preview = document.getElementById(previewId);
if (!preview) return;
const isHidden = preview.classList.contains("hidden");
if (isHidden) {
// show + render
scheduleRender(textarea, preview, 0);
preview.classList.remove("hidden");
preview.setAttribute("aria-hidden", "false");
} else {
preview.classList.add("hidden");
preview.setAttribute("aria-hidden", "true");
}
}
function attachToolbar(toolbarEl, textareaEl, previewEl) {
if (!toolbarEl || !textareaEl) return;
toolbarEl.querySelectorAll("button[data-action]").forEach((btn) => {
btn.addEventListener("click", (e) => {
const action = btn.getAttribute("data-action");
e.preventDefault();
switch (action) {
case "bold":
wrapSelection(textareaEl, "**", "**");
break;
case "italic":
wrapSelection(textareaEl, "*", "*");
break;
case "code":
wrapSelection(textareaEl, "`", "`");
break;
case "codeblock":
insertCodeBlock(textareaEl);
break;
case "link":
insertLink(textareaEl);
break;
case "h1":
insertHeading(textareaEl, 1);
break;
case "h2":
insertHeading(textareaEl, 2);
break;
case "h3":
insertHeading(textareaEl, 3);
break;
case "ul":
toggleUnorderedList(textareaEl);
break;
case "ol":
toggleOrderedList(textareaEl);
break;
case "quote":
toggleBlockquote(textareaEl);
break;
case "hr":
insertHr(textareaEl);
break;
case "image":
insertImage(textareaEl);
break;
case "strike":
wrapSelection(textareaEl, "~~", "~~");
break;
case "task":
toggleTaskList(textareaEl);
break;
case "table":
insertTable(textareaEl);
break;
}
});
});
// preview toggle button (if present)
const previewBtn =
toolbarEl.querySelector("#togglePreviewMain") ||
toolbarEl.querySelector("#togglePreviewModal");
if (previewBtn) {
previewBtn.addEventListener("click", (e) => {
e.preventDefault();
togglePreview(textareaEl.id, previewEl.id);
});
}
}
// --- end markdown editor helpers ---
function loadNotes() {
if (nodeFS) {
try {
// Ensure file exists
if (!fsModule.existsSync(dataFilePath)) {
fsModule.mkdirSync(pathModule.dirname(dataFilePath), {
recursive: true,
});
fsModule.writeFileSync(
dataFilePath,
JSON.stringify({ notes: [] }, null, 2),
"utf8",
);
}
const raw = fsModule.readFileSync(dataFilePath, "utf8");
const data = raw ? JSON.parse(raw) : {};
window.notes = data.notes || [];
if (!window.notes || window.notes.length === 0)
window.currentNoteIndex = null;
} catch (err) {
console.error("Chyba při čtení datového souboru:", err);
window.notes = window.notes || [];
}
updateNotesList();
if (window.currentNoteIndex != null) switchNotes(window.currentNoteIndex);
return;
}
// Browser fallback: try localStorage first, then static JSON file
try {
const saved = localStorage.getItem("doxa_notes_v1");
if (saved) {
const parsed = JSON.parse(saved);
window.notes = parsed.notes || [];
updateNotesList();
if (window.currentNoteIndex != null)
switchNotes(window.currentNoteIndex);
return;
}
} catch (err) {
console.warn("Chyba při načítání z localStorage:", err);
}
fetch(browserDataUrl)
.then((resp) => {
if (!resp.ok) {
window.notes = [];
updateNotesList();
return;
}
return resp
.json()
.then((data) => {
window.notes = data.notes || [];
updateNotesList();
})
.catch((err) => {
console.error("Chyba při parsování JSON ze souboru:", err);
window.notes = [];
updateNotesList();
});
})
.catch((err) => {
console.warn("Nelze načíst statický soubor:", err);
window.notes = [];
updateNotesList();
});
}
function saveNotesToFile(triggerDownload = true) {
const payload = { notes: window.notes || [] };
const json = JSON.stringify(payload, null, 2);
if (nodeFS) {
try {
const dir = pathModule.dirname(dataFilePath);
if (!fsModule.existsSync(dir)) {
fsModule.mkdirSync(dir, { recursive: true });
}
fsModule.writeFileSync(dataFilePath, json, "utf8");
log("Data uložena do", dataFilePath);
if (triggerDownload) alert("Poznámky byly uloženy.");
} catch (err) {
alert("Chyba při ukládání souboru: " + (err.message || err));
}
return;
}
// Browser fallback: save to localStorage
try {
localStorage.setItem("doxa_notes_v1", json);
} catch (err) {
console.warn("Nepodařilo se uložit do localStorage:", err);
}
if (triggerDownload) {
const dataStr =
"data:text/json;charset=utf-8," + encodeURIComponent(json);
const downloadAnchor = document.createElement("a");
downloadAnchor.setAttribute("href", dataStr);
downloadAnchor.setAttribute("download", "doxa.json");
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
downloadAnchor.remove();
}
}
function reorderNotes(fromIndex, toIndex) {
if (fromIndex === toIndex) return;
if (!Array.isArray(window.notes)) return;
// Keep track of selected note id to restore selection after reorder
const selectedId = window.notes[window.currentNoteIndex]
? window.notes[window.currentNoteIndex].id
: null;
// Remove item from array
const item = window.notes.splice(fromIndex, 1)[0];
// Adjust insert position because we removed an earlier element
let insertAt = toIndex;
if (toIndex > fromIndex) insertAt = toIndex - 1;
if (insertAt < 0) insertAt = 0;
if (insertAt > window.notes.length) insertAt = window.notes.length;
window.notes.splice(insertAt, 0, item);
// restore selection by id (robust across reorders)
if (selectedId != null) {
const newIndex = window.notes.findIndex((n) => n.id === selectedId);
window.currentNoteIndex = newIndex === -1 ? null : newIndex;
}
saveNotesToFile(false);
updateNotesList();
}
function moveNoteUp(index) {
if (index <= 0) return;
reorderNotes(index, index - 1);
}
function moveNoteDown(index) {
if (index < 0 || index >= window.notes.length - 1) return;
// toIndex = index + 2 will place after the next element (because of removal adjustment)
reorderNotes(index, index + 2);
}
function deleteNote(index) {
if (!window.notes || index < 0 || index >= window.notes.length) return;
const note = window.notes[index];
if (!confirm(`Smazat poznámku "${note.name}"?`)) return;
const selectedId = window.notes[window.currentNoteIndex]
? window.notes[window.currentNoteIndex].id
: null;
window.notes.splice(index, 1);
// If selected note deleted, try to select neighbor
if (selectedId != null && selectedId === note.id) {
if (window.notes.length === 0) {
window.currentNoteIndex = null;
document.getElementById("noteTitle").value = "";
document.getElementById("noteInput").value = "";
} else {
const newIndex = Math.min(index, window.notes.length - 1);
window.currentNoteIndex = newIndex;
switchNotes(window.currentNoteIndex);
}
} else if (
window.currentNoteIndex != null &&
window.currentNoteIndex > index
) {
// adjust index of current selection
window.currentNoteIndex -= 1;
}
saveNotesToFile(false);
updateNotesList();
}
function ensureModal() {
if (document.getElementById("editModalOverlay")) return;
const overlay = document.createElement("div");
overlay.id = "editModalOverlay";
overlay.className = "modal-overlay hidden";
overlay.innerHTML = `
<div class="modal" role="dialog" aria-modal="true">
<button class="modal-close" aria-label="Zavřít">✖</button>
<h2>Upravit poznámku</h2>
<input id="modalNoteTitle" placeholder="Název poznámky" />
<div class="editor-toolbar modal-toolbar" id="modalEditorToolbar">
<button type="button" data-action="h1">H1</button>
<button type="button" data-action="h2">H2</button>
<button type="button" data-action="h3">H3</button>
<button type="button" data-action="bold"><strong>B</strong></button>
<button type="button" data-action="italic"><em>I</em></button>
<button type="button" data-action="strike">S</button>
<button type="button" data-action="ul">•</button>
<button type="button" data-action="ol">1.</button>
<button type="button" data-action="task">☐</button>
<button type="button" data-action="quote">❝</button>
<button type="button" data-action="hr">—</button>
<button type="button" data-action="code"><code>kód</code></button>
<button type="button" data-action="codeblock">&lt;/&gt;</button>
<button type="button" data-action="link">🔗</button>
<button type="button" data-action="image">🖼</button>
<button type="button" data-action="table">▦</button>
<button type="button" id="livePreviewToggleModal" title="Živý náhled">Live</button>
<button type="button" id="togglePreviewModal">Náhled</button>
</div>
<textarea id="modalNoteContent" placeholder="Obsah poznámky"></textarea>
<div id="modalNotePreview" class="preview hidden" aria-hidden="true"></div>
<div class="modal-actions">
<button id="modalSaveBtn">Uložit</button>
<button id="modalCancelBtn">Zrušit</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// attach modal toolbar handlers (editor in modal)
const modalToolbar = overlay.querySelector("#modalEditorToolbar");
const modalTextarea = overlay.querySelector("#modalNoteContent");
const modalPreview = overlay.querySelector("#modalNotePreview");
if (modalToolbar && modalTextarea)
attachToolbar(modalToolbar, modalTextarea, modalPreview);
// modal live toggle
const modalLiveBtn = overlay.querySelector("#livePreviewToggleModal");
if (modalLiveBtn) {
modalLiveBtn.addEventListener("click", () => {
const cur = getStoredLive();
setStoredLive(!cur);
applyLiveStateGlobally();
});
if (getStoredLive()) modalLiveBtn.classList.add("active");
}
// modal title autosave
const modalTitleInput = overlay.querySelector("#modalNoteTitle");
if (modalTitleInput) {
modalTitleInput.addEventListener("input", () => {
if (
modalCurrentIndex != null &&
Array.isArray(window.notes) &&
window.notes[modalCurrentIndex]
) {
const idx = modalCurrentIndex;
window.notes[idx].name = modalTitleInput.value;
window.notes[idx].modified_at = new Date().toISOString();
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
});
}
if (modalTextarea && modalPreview) {
modalTextarea.addEventListener("input", () => {
// update model for modal current note
if (
modalCurrentIndex != null &&
Array.isArray(window.notes) &&
window.notes[modalCurrentIndex]
) {
const idx = modalCurrentIndex;
window.notes[idx].content = modalTextarea.value;
window.notes[idx].modified_at = new Date().toISOString();
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
if (!modalPreview.classList.contains("hidden"))
scheduleRender(modalTextarea, modalPreview);
});
modalTextarea.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
const k = (e.key || "").toLowerCase();
if (k === "b") {
e.preventDefault();
wrapSelection(modalTextarea, "**", "**");
}
if (k === "i") {
e.preventDefault();
wrapSelection(modalTextarea, "*", "*");
}
if (k === "`") {
e.preventDefault();
wrapSelection(modalTextarea, "`", "`");
}
}
});
}
// events
overlay.querySelector(".modal-close").addEventListener("click", closeModal);
overlay
.querySelector("#modalCancelBtn")
.addEventListener("click", closeModal);
overlay.querySelector("#modalSaveBtn").addEventListener("click", () => {
const title = document.getElementById("modalNoteTitle").value.trim();
const content = document.getElementById("modalNoteContent").value;
if (!title) return alert("Zadejte název poznámky.");
if (modalCurrentIndex == null) return closeModal();
const note = window.notes[modalCurrentIndex];
note.name = title;
note.content = content;
note.modified_at = new Date().toISOString();
saveNotesToFile(false);
updateNotesList();
switchNotes(modalCurrentIndex);
closeModal();
});
// click outside modal to close
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeModal();
});
}
function openEditModal(index) {
if (!window.notes || index < 0 || index >= window.notes.length) return;
ensureModal();
modalCurrentIndex = index;
const overlay = document.getElementById("editModalOverlay");
const note = window.notes[index];
document.getElementById("modalNoteTitle").value = note.name || "";
document.getElementById("modalNoteContent").value = note.content || "";
overlay.classList.remove("hidden");
// focus title
setTimeout(() => document.getElementById("modalNoteTitle").focus(), 50);
}
function closeModal() {
modalCurrentIndex = null;
const overlay = document.getElementById("editModalOverlay");
if (overlay) overlay.classList.add("hidden");
}
function updateNotesList() {
const notesContainer = document.getElementById("notesContainer");
if (!notesContainer) return;
notesContainer.innerHTML = "";
if (!window.notes || window.notes.length === 0) {
const empty = document.createElement("div");
empty.textContent =
'Žádné poznámky. Klikněte na "➕ Nová poznámka" pro přidání.';
empty.className = "no-notes";
notesContainer.appendChild(empty);
return;
}
window.notes.forEach((note, index) => {
const el = document.createElement("div");
el.className = "note-item";
el.setAttribute("draggable", "true");
el.dataset.index = index;
// left clickable area (switches to note)
const left = document.createElement("div");
left.className = "note-left";
left.addEventListener("click", () => switchNotes(index));
const title = document.createElement("div");
title.className = "note-title";
title.textContent = note.name || "Poznámka " + (index + 1);
const preview = document.createElement("div");
preview.className = "note-preview";
preview.textContent = (note.content || "")
.replace(/\n/g, " ")
.slice(0, 80);
left.appendChild(title);
left.appendChild(preview);
// controls
const controls = document.createElement("div");
controls.className = "note-controls";
const upBtn = document.createElement("button");
upBtn.className = "note-btn move-up";
upBtn.title = "Přesunout nahoru";
upBtn.textContent = "↑";
upBtn.addEventListener("click", (e) => {
e.stopPropagation();
moveNoteUp(index);
});
const downBtn = document.createElement("button");
downBtn.className = "note-btn move-down";
downBtn.title = "Přesunout dolů";
downBtn.textContent = "↓";
downBtn.addEventListener("click", (e) => {
e.stopPropagation();
moveNoteDown(index);
});
const editBtn = document.createElement("button");
editBtn.className = "note-btn edit";
editBtn.title = "Upravit (modal)";
editBtn.textContent = "✎";
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
openEditModal(index);
});
const delBtn = document.createElement("button");
delBtn.className = "note-btn delete";
delBtn.title = "Smazat poznámku";
delBtn.textContent = "🗑";
delBtn.addEventListener("click", (e) => {
e.stopPropagation();
deleteNote(index);
});
controls.appendChild(upBtn);
controls.appendChild(downBtn);
controls.appendChild(editBtn);
controls.appendChild(delBtn);
el.appendChild(left);
el.appendChild(controls);
// mark active
if (index === window.currentNoteIndex) el.classList.add("active");
// drag & drop handlers
el.addEventListener("dragstart", (ev) => {
ev.dataTransfer.setData("text/plain", String(index));
ev.dataTransfer.effectAllowed = "move";
el.classList.add("dragging");
});
el.addEventListener("dragend", () => {
el.classList.remove("dragging");
// cleanup any drag-over classes
document
.querySelectorAll(".note-item.drag-over")
.forEach((n) => n.classList.remove("drag-over", "drag-after"));
});
// show whether drop will be before or after based on mouse position
el.addEventListener("dragover", (ev) => {
ev.preventDefault();
const rect = el.getBoundingClientRect();
const offset = ev.clientY - rect.top;
const after = offset > rect.height / 2;
el.classList.add("drag-over");
if (after) el.classList.add("drag-after");
else el.classList.remove("drag-after");
ev.dataTransfer.dropEffect = "move";
});
el.addEventListener("dragleave", () => {
el.classList.remove("drag-over", "drag-after");
});
el.addEventListener("drop", (ev) => {
ev.preventDefault();
el.classList.remove("drag-over", "drag-after");
const from = parseInt(ev.dataTransfer.getData("text/plain"), 10);
if (Number.isNaN(from)) return;
const rect = el.getBoundingClientRect();
const offset = ev.clientY - rect.top;
const insertAfter = offset > rect.height / 2;
let to = index + (insertAfter ? 1 : 0);
// bounds
if (to < 0) to = 0;
if (to > window.notes.length) to = window.notes.length;
reorderNotes(from, to);
});
notesContainer.appendChild(el);
});
}
function switchNotes(index) {
if (!window.notes || index < 0 || index >= window.notes.length) return;
window.currentNoteIndex = index;
const note = window.notes[index];
const titleEl = document.getElementById("noteTitle");
const input = document.getElementById("noteInput");
if (titleEl) titleEl.value = note.name || "";
if (input) input.value = note.content || "";
updateNotesList();
}
function addNewNote() {
const defaultName =
"Nová poznámka " +
(window.notes && window.notes.length ? window.notes.length + 1 : 1);
const name = prompt("Název nové poznámky:", defaultName);
if (name === null) return; // user cancelled
const newNote = {
id: Date.now(),
name: name.trim() || defaultName,
content: "",
created_at: new Date().toISOString(),
};
window.notes = window.notes || [];
window.notes.push(newNote);
window.currentNoteIndex = window.notes.length - 1;
saveNotesToFile(false); // persist but don't trigger download
updateNotesList();
switchNotes(window.currentNoteIndex);
}
function importujPoznamky() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json,application/json";
input.addEventListener("change", (ev) => {
const file = ev.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const parsed = JSON.parse(e.target.result);
if (Array.isArray(parsed)) {
window.notes = parsed;
} else if (Array.isArray(parsed.notes)) {
window.notes = parsed.notes;
} else {
alert(
'Neplatný formát JSON. Očekávám pole nebo objekt s polem "notes".',
);
return;
}
window.currentNoteIndex = window.notes.length ? 0 : null;
saveNotesToFile(false); // persist (fs/localStorage) but don't trigger download
updateNotesList();
if (window.currentNoteIndex !== null)
switchNotes(window.currentNoteIndex);
} catch (err) {
alert("Chyba při čtení JSON: " + err.message);
}
};
reader.readAsText(file);
});
input.click();
}
function ulozPoznamku() {
// compatibility with old button
saveNotesToFile(true);
}
function saveCurrentNote() {
const titleEl = document.getElementById("noteTitle");
const input = document.getElementById("noteInput");
const title = titleEl ? titleEl.value.trim() : "";
const content = input ? input.value : "";
if (!title) {
alert("Zadejte název poznámky.");
return;
}
if (window.currentNoteIndex == null) {
// create new
const newNote = {
id: Date.now(),
name: title,
content,
created_at: new Date().toISOString(),
};
window.notes.push(newNote);
window.currentNoteIndex = window.notes.length - 1;
} else {
// update existing
const note = window.notes[window.currentNoteIndex];
note.name = title;
note.content = content;
note.modified_at = new Date().toISOString();
}
saveNotesToFile(false);
updateNotesList();
switchNotes(window.currentNoteIndex);
}
// Export functions to global scope so HTML onclick handlers work
window.importujPoznamky = importujPoznamky;
window.addNewNote = addNewNote;
window.switchNotes = switchNotes;
window.ulozPoznamku = ulozPoznamku;
window.saveNotesToFile = saveNotesToFile;
window.saveCurrentNote = saveCurrentNote;
// Additional exported helpers
window.deleteNote = deleteNote;
window.moveNoteUp = moveNoteUp;
window.moveNoteDown = moveNoteDown;
window.openEditModal = openEditModal;
window.toggleTheme = toggleTheme;
window.applyTheme = applyTheme;
document.addEventListener("DOMContentLoaded", () => {
const newNoteBtn = document.getElementById("newNoteButton");
if (newNoteBtn) newNoteBtn.addEventListener("click", addNewNote);
const themeBtn = document.getElementById("themeToggle");
if (themeBtn) themeBtn.addEventListener("click", toggleTheme);
// attach main editor toolbar if present
const toolbar = document.getElementById("editorToolbar");
const textarea = document.getElementById("noteInput");
const preview = document.getElementById("notePreview");
if (toolbar && textarea) attachToolbar(toolbar, textarea, preview);
// live preview toggle
const liveBtn = document.getElementById("livePreviewToggle");
if (liveBtn) {
liveBtn.addEventListener("click", () => {
const cur = getStoredLive();
setStoredLive(!cur);
applyLiveStateGlobally();
});
}
// apply initial state to main & modal if present
applyLiveStateGlobally();
if (textarea) {
// input handling: update preview and autosave current note
textarea.addEventListener("input", () => {
// update model for selected note
if (
window.currentNoteIndex != null &&
Array.isArray(window.notes) &&
window.notes[window.currentNoteIndex]
) {
const idx = window.currentNoteIndex;
window.notes[idx].content = textarea.value;
window.notes[idx].modified_at = new Date().toISOString();
// reflect small changes in the list
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
// live preview render if visible or live mode enabled
const liveMode = getStoredLive();
if (preview && (!preview.classList.contains("hidden") || liveMode)) {
if (preview.classList.contains("hidden")) {
// show preview when live mode enabled
preview.classList.remove("hidden");
preview.setAttribute("aria-hidden", "false");
}
scheduleRender(textarea, preview);
}
});
textarea.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
const k = (e.key || "").toLowerCase();
if (k === "b") {
e.preventDefault();
wrapSelection(textarea, "**", "**");
}
if (k === "i") {
e.preventDefault();
wrapSelection(textarea, "*", "*");
}
if (k === "`") {
e.preventDefault();
wrapSelection(textarea, "`", "`");
}
}
});
}
// title input autosave
const titleInput = document.getElementById("noteTitle");
if (titleInput) {
titleInput.addEventListener("input", () => {
if (
window.currentNoteIndex != null &&
Array.isArray(window.notes) &&
window.notes[window.currentNoteIndex]
) {
const idx = window.currentNoteIndex;
window.notes[idx].name = titleInput.value;
window.notes[idx].modified_at = new Date().toISOString();
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
});
}
initTheme();
loadNotes();
});
})();