(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",
t('themeAriaSystem', { current: t(e.matches ? 'themeCurrentDark' : 'themeCurrentLight') })
);
}
};
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 = t('themeAriaSystem', { current: t(document.body.classList.contains("dark") ? 'themeCurrentDark' : 'themeCurrentLight') });
if (theme === "dark") {
icon = "🌙";
aria = t('themeAriaDark');
} else if (theme === "light") {
icon = "☀️";
aria = t('themeAriaLight');
} else {
const cur = document.body.classList.contains("dark")
? t('themeCurrentDark')
: t('themeCurrentLight');
icon = "🌓";
aria = t('themeAriaSystem', { current: 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, ">")
.replace(/\"/g, """)
.replace(/'/g, "'");
}
function escapeAttr(str) {
return escapeHtml(str).replace(/\"/g, """);
}
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(
' ',
),
);
// links
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, text, url) =>
push('' + escapeHtml(text) + " "),
);
// inline code
s = s.replace(/`([^`]+)`/g, (m, code) =>
push("" + escapeHtml(code) + ""),
);
// bold
s = s.replace(/\*\*(.+?)\*\*/g, (m, t) =>
push("" + escapeHtml(t) + " "),
);
// italic
s = s.replace(/\*(.+?)\*/g, (m, t) =>
push("" + escapeHtml(t) + " "),
);
// strike
s = s.replace(/~~(.+?)~~/g, (m, t) =>
push("" + escapeHtml(t) + ""),
);
// 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 += "
" + escapeHtml(code) + " \n";
continue;
}
// heading
const h = line.match(/^\s*(#{1,6})\s+(.*)$/);
if (h) {
out +=
"" +
inlineTokenize(h[2]) +
" \n";
i++;
continue;
}
// hr
if (/^\s*-{3,}\s*$/.test(line)) {
out += " ";
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 +=
"" +
headerParts
.map((hp) => "" + inlineTokenize(hp) + " ")
.join("") +
" ";
if (rows.length) {
out +=
"" +
rows
.map((r) => {
const cells = r
.split("|")
.map((c) => c.trim())
.filter((_, idx) => idx > 0 || true);
return (
"" +
cells
.map((c) => "" + inlineTokenize(c) + " ")
.join("") +
" "
);
})
.join("") +
" ";
}
out += "
\n";
i = k;
continue;
}
// unordered list
if (/^\s*[-*]\s+/.test(line)) {
out += "";
while (i < n && /^\s*([-*])\s+/.test(lines[i])) {
const m = lines[i].replace(/^\s*[-*]\s+/, "");
out += "" + inlineTokenize(m) + " ";
i++;
}
out += " \n";
continue;
}
// ordered list
if (/^\s*\d+\.\s+/.test(line)) {
out += "";
while (i < n && /^\s*\d+\.\s+/.test(lines[i])) {
const m = lines[i].replace(/^\s*\d+\.\s+/, "");
out += "" + inlineTokenize(m) + " ";
i++;
}
out += " \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 +=
"" + inlineTokenize(quote.trim()) + " \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 += "" + inlineTokenize(para.trim()) + "
\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 = t('previewUnavailable');
return;
}
const raw = textareaEl.value || "";
let html = "";
try {
html = md.render(raw);
} catch (e) {
html = "" + String(raw) + " ";
}
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 (mainPreview) {
mainPreview.classList.add("hidden");
mainPreview.setAttribute("aria-hidden", "true");
}
if (on && mainPreview && mainTextarea) {
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) {
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 = t('statusSaved');
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(t('promptUrl'), "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) {}
}
}
// --- end markdown editor helpers ---
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) {}
}
}
// --- Ostatní helpers pro markdown zůstávají ---
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(t('promptImageUrl'), "https://");
if (!url) return;
const alt = selected || prompt(t('promptImageAlt'), "");
const insert = "";
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(t('promptTableCols'), "2");
if (!colsStr) return;
const rowsStr = prompt(t('promptTableRows'), "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(t('tableHeaderDefault', { index: 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);
const toggleBtn = document.getElementById("togglePreviewMain") ||
document.getElementById("togglePreviewModal");
if (!preview) return;
const isHidden = preview.classList.contains("hidden");
if (isHidden) {
scheduleRender(textarea, preview, 0);
preview.classList.remove("hidden");
preview.setAttribute("aria-hidden", "false");
textarea.classList.add("hidden");
if (toggleBtn) toggleBtn.classList.add("active");
} else {
preview.classList.add("hidden");
preview.setAttribute("aria-hidden", "true");
textarea.classList.remove("hidden");
if (toggleBtn) toggleBtn.classList.remove("active");
}
}
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;
}
});
});
const previewBtn =
toolbarEl.querySelector("#togglePreviewMain") ||
toolbarEl.querySelector("#togglePreviewModal");
if (previewBtn) {
previewBtn.addEventListener("click", (e) => {
e.preventDefault();
togglePreview(textareaEl.id, previewEl.id);
});
}
}
function loadNotes() {
if (nodeFS) {
try {
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 || [];
}
// OPRAVA/SANACE: Zajištění přítomnosti pole tags a folder u všech načtených poznámek
if (Array.isArray(window.notes)) {
window.notes.forEach(note => {
if (!note.tags) note.tags = [];
if (!note.folder) note.folder = "";
});
}
updateNotesList();
if (window.currentNoteIndex != null) switchNotes(window.currentNoteIndex);
return;
}
// Browser fallback
try {
const saved = localStorage.getItem("doxa_notes_v1");
if (saved) {
const parsed = JSON.parse(saved);
window.notes = parsed.notes || [];
// OPRAVA/SANACE v LocalStorage: tags + folder
if (Array.isArray(window.notes)) {
window.notes.forEach(note => {
if (!note.tags) note.tags = [];
if (!note.folder) note.folder = "";
});
}
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 || [];
// OPRAVA/SANACE ve statickém JSON souboru: tags + folder
if (Array.isArray(window.notes)) {
window.notes.forEach(note => {
if (!note.tags) note.tags = [];
if (!note.folder) note.folder = "";
});
}
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(t('notesSavedAlert'));
} catch (err) {
alert(t('saveErrorAlert', { message: (err.message || err) }));
}
return;
}
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;
const selectedId = window.notes[window.currentNoteIndex]
? window.notes[window.currentNoteIndex].id
: null;
const item = window.notes.splice(fromIndex, 1)[0];
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);
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;
reorderNotes(index, index + 2);
}
function deleteNote(index) {
if (!window.notes || index < 0 || index >= window.notes.length) return;
const note = window.notes[index];
if (!confirm(t('confirmDeleteNote', { noteName: note.name }))) return;
const selectedId = window.notes[window.currentNoteIndex]
? window.notes[window.currentNoteIndex].id
: null;
window.notes.splice(index, 1);
if (selectedId != null && selectedId === note.id) {
if (window.notes.length === 0) {
window.currentNoteIndex = null;
document.getElementById("noteTitle").value = "";
document.getElementById("noteInput").value = "";
if (document.getElementById("noteTags")) document.getElementById("noteTags").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
) {
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 = `
✖
Upravit poznámku
H1
H2
H3
B
I
S
•
1.
☐
❝
—
kód
</>
🔗
🖼
▦
Náhled
Uložit
Zrušit
`;
document.body.appendChild(overlay);
const modalToolbar = overlay.querySelector("#modalEditorToolbar");
const modalTextarea = overlay.querySelector("#modalNoteContent");
const modalPreview = overlay.querySelector("#modalNotePreview");
if (modalToolbar && modalTextarea)
attachToolbar(modalToolbar, modalTextarea, modalPreview);
const modalLiveBtn = overlay.querySelector("#livePreviewToggleModal");
if (modalLiveBtn) {
modalLiveBtn.addEventListener("click", () => {
const cur = getStoredLive();
setStoredLive(!cur);
applyLiveStateGlobally();
});
if (getStoredLive()) modalLiveBtn.classList.add("active");
}
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", () => {
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, "`", "`");
}
}
});
}
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(t('alertNoTitle'));
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();
});
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");
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 = "";
const filterText = document.getElementById("searchTagInput")?.value.trim().toLowerCase() || "";
const folderFilter = document.getElementById("folderFilter")?.value || "all";
// naplnit select se složkami (unikátní, nenulové) a aktualizovat datalist pro pole složky
try {
const select = document.getElementById("folderFilter");
const datalist = document.getElementById("foldersList");
if (select) {
const folders = Array.from(new Set((window.notes || []).map(n => (n.folder || "").trim()).filter(f => f)));
const current = select.value || "all";
select.innerHTML = `${t('filterAllFolders')} ` + folders.map(f => `${f} `).join("");
if (folders.indexOf(current) !== -1) select.value = current;
else select.value = current === "all" ? "all" : current;
// update datalist options
if (datalist) {
datalist.innerHTML = folders.map(f => ` `).join("");
}
}
} catch (e) {}
if (!window.notes || window.notes.length === 0) {
const empty = document.createElement("div");
empty.textContent = t('noNotes');
empty.className = "no-notes";
notesContainer.appendChild(empty);
return;
}
let renderedCount = 0;
window.notes.forEach((note, index) => {
// filtrovat podle hledaného textu (název, obsah, tagy, složka)
if (filterText) {
const ft = filterText;
const inTitle = note.name && note.name.toLowerCase().includes(ft);
const inContent = note.content && note.content.toLowerCase().includes(ft);
const inTags = note.tags && note.tags.some(t => t.toLowerCase().includes(ft));
const inFolderName = note.folder && note.folder.toLowerCase().includes(ft);
if (!(inTitle || inContent || inTags || inFolderName)) return;
}
// filtr podle složky (select)
if (folderFilter && folderFilter !== "all") {
const nf = (note.folder || "").trim();
if (nf !== folderFilter) return;
}
renderedCount++;
const el = document.createElement("div");
el.className = "note-item";
el.setAttribute("draggable", "true");
el.dataset.index = index;
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 || t('noteNameDefault', { index: index + 1 });
const preview = document.createElement("div");
preview.className = "note-preview";
let previewText = (note.content || "").replace(/\n/g, " ").slice(0, 80);
preview.textContent = previewText;
left.appendChild(title);
left.appendChild(preview);
if (note.tags && note.tags.length > 0) {
const tagsDiv = document.createElement("div");
tagsDiv.className = "note-preview-tags";
tagsDiv.style.fontSize = "0.75rem";
tagsDiv.style.color = "var(--muted)";
tagsDiv.style.marginTop = "4px";
tagsDiv.style.fontFamily = "monospace";
tagsDiv.textContent = "🏷️ " + note.tags.join(", ");
left.appendChild(tagsDiv);
}
if (note.folder && note.folder.trim()) {
const folderDiv = document.createElement("div");
folderDiv.className = "note-preview-folder";
folderDiv.style.fontSize = "0.75rem";
folderDiv.style.color = "var(--muted)";
folderDiv.style.marginTop = "4px";
folderDiv.style.fontFamily = "monospace";
const iconSpan = document.createElement("span");
iconSpan.className = "folder-icon";
iconSpan.textContent = "📁";
const nameSpan = document.createElement("span");
nameSpan.className = "folder-name";
nameSpan.textContent = note.folder;
folderDiv.appendChild(iconSpan);
folderDiv.appendChild(nameSpan);
left.appendChild(folderDiv);
}
const controls = document.createElement("div");
controls.className = "note-controls";
const upBtn = document.createElement("button");
upBtn.className = "note-btn move-up";
upBtn.title = t('noteMoveUp');
upBtn.textContent = "↑";
upBtn.addEventListener("click", (e) => {
e.stopPropagation();
moveNoteUp(index);
});
const downBtn = document.createElement("button");
downBtn.className = "note-btn move-down";
downBtn.title = t('noteMoveDown');
downBtn.textContent = "↓";
downBtn.addEventListener("click", (e) => {
e.stopPropagation();
moveNoteDown(index);
});
const editBtn = document.createElement("button");
editBtn.className = "note-btn edit";
editBtn.title = t('noteEditModal');
editBtn.textContent = "✎";
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
openEditModal(index);
});
const delBtn = document.createElement("button");
delBtn.className = "note-btn delete";
delBtn.title = t('noteDelete');
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);
if (index === window.currentNoteIndex) el.classList.add("active");
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");
document
.querySelectorAll(".note-item.drag-over")
.forEach((n) => n.classList.remove("drag-over", "drag-after"));
});
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);
if (to < 0) to = 0;
if (to > window.notes.length) to = window.notes.length;
reorderNotes(from, to);
});
notesContainer.appendChild(el);
});
if (renderedCount === 0 && filterText) {
const emptyFilter = document.createElement("div");
emptyFilter.textContent = t('noNotesForFilter');
emptyFilter.className = "no-notes";
notesContainer.appendChild(emptyFilter);
}
}
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");
const tagsInput = document.getElementById("noteTags");
const preview = document.getElementById("notePreview");
const toggleBtn = document.getElementById("togglePreviewMain");
if (titleEl) titleEl.value = note.name || "";
if (input) input.value = note.content || "";
if (tagsInput) {
tagsInput.value = note.tags ? note.tags.join(", ") : "";
}
const noteFolderInput = document.getElementById("noteFolder");
if (noteFolderInput) noteFolderInput.value = note.folder || "";
if (preview) {
preview.classList.add("hidden");
preview.setAttribute("aria-hidden", "true");
}
if (input) input.classList.remove("hidden");
if (toggleBtn) toggleBtn.classList.remove("active");
updateNotesList();
}
function addNewNote() {
const defaultName = t('noteNameDefault', { index: (window.notes?.length || 0) + 1 });
const name = prompt(t('promptNewNote'), defaultName);
if (name === null) return;
const newNote = {
id: Date.now(),
name: name.trim() || defaultName,
content: "",
tags: [],
folder: "",
created_at: new Date().toISOString(),
};
window.notes = window.notes || [];
window.notes.push(newNote);
window.currentNoteIndex = window.notes.length - 1;
saveNotesToFile(false);
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(t('importInvalidJson'));
return;
}
// OPRAVA/SANACE při importu: Každá poznámka musí mít pole tags a folder
if (Array.isArray(window.notes)) {
window.notes.forEach(note => {
if (!note.tags) note.tags = [];
if (!note.folder) note.folder = "";
});
}
window.currentNoteIndex = window.notes.length ? 0 : null;
saveNotesToFile(false);
updateNotesList();
if (window.currentNoteIndex !== null)
switchNotes(window.currentNoteIndex);
} catch (err) {
alert(t('importReadError', { message: err.message }));
}
};
reader.readAsText(file);
});
input.click();
}
function ulozPoznamku() {
saveNotesToFile(true);
}
function saveCurrentNote() {
const titleEl = document.getElementById("noteTitle");
const input = document.getElementById("noteInput");
const tagsInput = document.getElementById("noteTags");
const title = titleEl ? titleEl.value.trim() : "";
const content = input ? input.value : "";
// OPRAVA: Zaručíme vytvoření validního pole [] i v případě absence textu
const tags = tagsInput && tagsInput.value.trim() !== ""
? tagsInput.value.split(",").map(t => t.trim()).filter(t => t.length > 0)
: [];
// Rozpoznání záložky složky z obsahu: řádek začínající "folder: Název"
let detectedFolder = "";
try {
const m = content.match(/(^|\n)\s*folder:\s*(.+)/i);
if (m && m[2]) detectedFolder = m[2].trim();
} catch (e) {}
// Pokud je explicitní pole noteFolder vyplněné, má přednost
const explicitFolderInput = document.getElementById("noteFolder");
const explicitFolder = explicitFolderInput && explicitFolderInput.value.trim() ? explicitFolderInput.value.trim() : null;
if (!title) {
alert(t('alertNoTitle'));
return;
}
if (window.currentNoteIndex == null) {
const newNote = {
id: Date.now(),
name: title,
content,
tags,
folder: explicitFolder !== null ? explicitFolder : detectedFolder || "",
created_at: new Date().toISOString(),
};
window.notes.push(newNote);
window.currentNoteIndex = window.notes.length - 1;
} else {
const note = window.notes[window.currentNoteIndex];
note.name = title;
note.content = content;
note.tags = tags;
// explicitFolder má prioritu před detekcí v obsahu
if (explicitFolder !== null) note.folder = explicitFolder;
else if (detectedFolder) note.folder = detectedFolder;
note.modified_at = new Date().toISOString();
}
saveNotesToFile(false);
updateNotesList();
switchNotes(window.currentNoteIndex);
}
function exportToMarkdown() {
const titleEl = document.getElementById("noteTitle");
const contentEl = document.getElementById("noteInput");
const title = titleEl ? titleEl.value.trim() : "Poznámka";
const content = contentEl ? contentEl.value : "";
if (!title && !content) {
alert(t('exportEmptyAlert'));
return;
}
const markdown = title ? `# ${title}\n\n${content}` : content;
const filename = title ? `${title.replace(/[^a-zA-Z0-9-_]/g, "_")}.md` : "export.md";
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
const downloadAnchor = document.createElement("a");
downloadAnchor.setAttribute("href", URL.createObjectURL(blob));
downloadAnchor.setAttribute("download", filename);
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
downloadAnchor.remove();
}
// Slash command functionality
const SLASH_COMMANDS = [
{ cmd: "/date", i18nKey: "slashDate", insert: () => {
const lang = localStorage.getItem('doxa_lang') || 'cs';
const locale = lang === 'cs' ? 'cs-CZ' : 'en-US';
return new Date().toLocaleDateString(locale);
}
},
{ cmd: "/time", i18nKey: "slashTime", insert: () => {
const lang = localStorage.getItem('doxa_lang') || 'cs';
const locale = lang === 'cs' ? 'cs-CZ' : 'en-US';
return new Date().toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
}
},
{ cmd: "/code", i18nKey: "slashCode", insert: () => "`kód`" },
{ cmd: "/codeblock", i18nKey: "slashCodeblock", insert: () => "```\nkód\n```" },
{ cmd: "/link", i18nKey: "slashLink", insert: () => "text" },
{ cmd: "/image", i18nKey: "slashImage", insert: () => "!alt" },
{ cmd: "/table", i18nKey: "slashTable", insert: () => "| Sloupec 1 | Sloupec 2 |\n|-----------|----------|\n| Data 1 | Data 2 |" },
{ cmd: "/quote", i18nKey: "slashQuote", insert: () => "> Citace" },
{ cmd: "/hr", i18nKey: "slashHr", insert: () => "---" },
{ cmd: "/task", i18nKey: "slashTask", insert: () => "- [ ] Úkol" },
{ cmd: "/list", i18nKey: "slashList", insert: () => "- Položka" },
{ cmd: "/h1", i18nKey: "slashH1", insert: () => "# Nadpis" },
{ cmd: "/h2", i18nKey: "slashH2", insert: () => "## Nadpis" },
{ cmd: "/h3", i18nKey: "slashH3", insert: () => "### Nadpis" },
];
let slashCommandState = {
active: false,
selectedIndex: 0,
filteredCommands: [],
startPos: 0,
query: "",
};
function hideSlashMenu() {
const menu = document.getElementById("slashCommandMenu");
if (menu) {
menu.classList.add("hidden");
menu.innerHTML = "";
}
slashCommandState.active = false;
}
function showSlashMenu(textarea, query) {
const menu = document.getElementById("slashCommandMenu");
if (!menu) return;
const filtered = SLASH_COMMANDS.filter(cmd =>
cmd.cmd.toLowerCase().includes(query.toLowerCase()) ||
t(cmd.i18nKey).toLowerCase().includes(query.toLowerCase())
);
slashCommandState.filteredCommands = filtered;
slashCommandState.selectedIndex = 0;
slashCommandState.query = query;
if (filtered.length === 0) {
hideSlashMenu();
return;
}
menu.innerHTML = filtered.map((cmd, idx) =>
`
${t(cmd.i18nKey)}
`
).join("");
// Umístit menu pod kurzorem v textareu
const textarea_rect = textarea.getBoundingClientRect();
const scrollX = window.scrollX || window.pageXOffset;
const scrollY = window.scrollY || window.pageYOffset;
// Přibližná výška jedného řádku textu
const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight);
const lineCount = textarea.value.substring(0, textarea.selectionStart).split('\n').length;
// Pozice kurzoru přibližně
let top = textarea_rect.top + scrollY + (lineCount * lineHeight) + 4;
let left = textarea_rect.left + scrollX;
menu.style.position = "fixed";
menu.style.top = top + "px";
menu.style.left = left + "px";
menu.style.zIndex = "1000";
// Ujistit se, že menu nezůstane mimo viditelnou oblast
menu.classList.remove("hidden");
setTimeout(() => {
const menuRect = menu.getBoundingClientRect();
// Pokud je menu mimo dolní okraj, posunout nahoru
if (menuRect.bottom > window.innerHeight) {
top = window.innerHeight - menuRect.height - 10 + scrollY;
menu.style.top = Math.max(scrollY, top) + "px";
}
// Pokud je menu mimo pravý okraj, posunout doleva
if (menuRect.right > window.innerWidth) {
left = window.innerWidth - menuRect.width - 10 + scrollX;
menu.style.left = Math.max(scrollX, left) + "px";
}
}, 0);
slashCommandState.active = true;
menu.querySelectorAll(".slash-command-item").forEach((item, idx) => {
item.addEventListener("click", () => selectSlashCommand(textarea, idx));
item.addEventListener("mouseover", () => {
document.querySelectorAll(".slash-command-item").forEach(i => i.classList.remove("active"));
item.classList.add("active");
slashCommandState.selectedIndex = idx;
});
});
}
function selectSlashCommand(textarea, index) {
const cmd = slashCommandState.filteredCommands[index];
if (!cmd) return;
const text = textarea.value;
const pos = textarea.selectionStart;
const lastSlashPos = text.lastIndexOf("/", pos - 1);
if (lastSlashPos === -1) return;
const before = text.substring(0, lastSlashPos);
const after = text.substring(pos);
const insertText = cmd.insert();
textarea.value = before + insertText + after;
textarea.selectionStart = textarea.selectionEnd = before.length + insertText.length;
textarea.focus();
if (window.currentNoteIndex != null && Array.isArray(window.notes)) {
window.notes[window.currentNoteIndex].content = textarea.value;
scheduleAutosave();
}
hideSlashMenu();
}
function initSlashCommands(textarea) {
if (!textarea) return;
// Slash commands are handled in the main input and keydown listeners
}
function updateSlashMenuSelection() {
document.querySelectorAll(".slash-command-item").forEach((item, idx) => {
if (idx === slashCommandState.selectedIndex) {
item.classList.add("active");
item.scrollIntoView({ block: "nearest" });
} else {
item.classList.remove("active");
}
});
}
// Export functions to global scope
window.importujPoznamky = importujPoznamky;
window.addNewNote = addNewNote;
window.switchNotes = switchNotes;
window.ulozPoznamku = ulozPoznamku;
window.saveNotesToFile = saveNotesToFile;
window.saveCurrentNote = saveCurrentNote;
window.exportToMarkdown = exportToMarkdown;
window.deleteNote = deleteNote;
window.moveNoteUp = moveNoteUp;
window.moveNoteDown = moveNoteDown;
window.openEditModal = openEditModal;
window.toggleTheme = toggleTheme;
window.applyTheme = applyTheme;
// Expose a function to re-render UI parts that depend on translated text
function updateDynamicUIText() {
applyTheme(getStoredTheme() || "system");
updateNotesList();
// If slash menu is open, redraw it
if (slashCommandState.active) showSlashMenu(document.getElementById("noteInput"), slashCommandState.query);
}
window.updateDynamicUIText = updateDynamicUIText;
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);
const toolbar = document.getElementById("editorToolbar");
const textarea = document.getElementById("noteInput");
const preview = document.getElementById("notePreview");
if (preview) {
preview.classList.add("hidden");
preview.setAttribute("aria-hidden", "true");
}
if (toolbar && textarea) attachToolbar(toolbar, textarea, preview);
if (textarea) {
textarea.addEventListener("input", () => {
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();
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
if (preview && !preview.classList.contains("hidden")) {
scheduleRender(textarea, preview);
}
// Handle slash commands
const text = textarea.value;
const pos = textarea.selectionStart;
const lastSlashPos = text.lastIndexOf("/", pos - 1);
if (lastSlashPos === -1 || text[lastSlashPos] === "\n") {
hideSlashMenu();
} else {
const query = text.substring(lastSlashPos, pos);
const beforeSlash = lastSlashPos === 0 || text[lastSlashPos - 1] === "\n";
if (query.length > 0 && beforeSlash) {
showSlashMenu(textarea, query);
} else {
hideSlashMenu();
}
}
});
textarea.addEventListener("keydown", (e) => {
// Handle slash command navigation
if (slashCommandState.active) {
if (e.key === "Escape") {
e.preventDefault();
hideSlashMenu();
return;
} else if (e.key === "ArrowDown") {
e.preventDefault();
slashCommandState.selectedIndex = (slashCommandState.selectedIndex + 1) % slashCommandState.filteredCommands.length;
updateSlashMenuSelection();
return;
} else if (e.key === "ArrowUp") {
e.preventDefault();
slashCommandState.selectedIndex = (slashCommandState.selectedIndex - 1 + slashCommandState.filteredCommands.length) % slashCommandState.filteredCommands.length;
updateSlashMenuSelection();
return;
} else if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
selectSlashCommand(textarea, slashCommandState.selectedIndex);
return;
}
}
// Handle keyboard shortcuts
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, "`", "`");
}
}
});
}
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();
}
});
}
const tagsInput = document.getElementById("noteTags");
if (tagsInput) {
// create suggestions container
let tagSug = document.createElement("div");
tagSug.className = "suggestions hidden";
tagSug.id = "tagSuggestions";
tagSug.style.minWidth = "220px";
tagsInput.parentNode?.appendChild(tagSug);
let tagSelected = -1;
function getUniqueTags() {
const all = (window.notes || []).flatMap(n => (n.tags || []));
return Array.from(new Set(all)).filter(t => t && t.trim()).sort((a,b)=>a.localeCompare(b));
}
function showTagSuggestionsFor(prefix) {
const list = getUniqueTags().filter(t => t.toLowerCase().startsWith(prefix.toLowerCase()) );
const container = document.getElementById("tagSuggestions");
if (!container) return;
container.innerHTML = "";
tagSelected = -1;
if (!list.length) { container.classList.add("hidden"); return; }
list.forEach((t, i)=>{
const it = document.createElement("div");
it.className = "suggestion-item";
it.textContent = t;
it.dataset.value = t;
it.addEventListener("mousedown", (ev)=>{
ev.preventDefault(); // prevent blur
applyTagSuggestion(t);
});
container.appendChild(it);
});
container.classList.remove("hidden");
}
function hideTagSuggestions() {
const c = document.getElementById("tagSuggestions");
if (c) c.classList.add("hidden");
tagSelected = -1;
}
function applyTagSuggestion(value) {
const el = tagsInput;
const cur = el.value;
const parts = cur.split(",");
const last = parts.pop() || "";
const prefix = last.includes(" ") ? last : last; // keep simple
parts.push(" " + value.trim());
const newVal = parts.map(p=>p.trim()).filter(p=>p).join(", ");
el.value = newVal;
el.dispatchEvent(new Event("input", { bubbles: true }));
hideTagSuggestions();
el.focus();
}
tagsInput.addEventListener("input", (e)=>{
const v = tagsInput.value;
const parts = v.split(",");
const last = parts.pop() || "";
const q = last.trim();
if (q.length === 0) { hideTagSuggestions(); return; }
showTagSuggestionsFor(q);
// update stored tags on change (existing behavior)
if (
window.currentNoteIndex != null &&
Array.isArray(window.notes) &&
window.notes[window.currentNoteIndex]
) {
const idx = window.currentNoteIndex;
window.notes[idx].tags = tagsInput.value
.split(",")
.map(t => t.trim())
.filter(t => t.length > 0);
window.notes[idx].modified_at = new Date().toISOString();
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
});
tagsInput.addEventListener("keydown", (e)=>{
const container = document.getElementById("tagSuggestions");
if (!container || container.classList.contains("hidden")) return;
const items = Array.from(container.querySelectorAll('.suggestion-item'));
if (e.key === 'ArrowDown') {
e.preventDefault(); tagSelected = Math.min(items.length-1, tagSelected+1); updateTagActive(items); }
else if (e.key === 'ArrowUp') { e.preventDefault(); tagSelected = Math.max(0, tagSelected-1); updateTagActive(items); }
else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); if (tagSelected >=0 && items[tagSelected]) applyTagSuggestion(items[tagSelected].dataset.value); }
else if (e.key === 'Escape') { hideTagSuggestions(); }
});
function updateTagActive(items){
items.forEach((it, idx)=>{ it.classList.toggle('active', idx === tagSelected); if (idx === tagSelected) it.scrollIntoView({block:'nearest'}); });
}
tagsInput.addEventListener('blur', ()=>{ setTimeout(()=>hideTagSuggestions(), 150); });
}
// Složka (ručně)
const noteFolderInput = document.getElementById("noteFolder");
if (noteFolderInput) {
noteFolderInput.addEventListener("input", () => {
if (
window.currentNoteIndex != null &&
Array.isArray(window.notes) &&
window.notes[window.currentNoteIndex]
) {
const idx = window.currentNoteIndex;
window.notes[idx].folder = noteFolderInput.value.trim();
window.notes[idx].modified_at = new Date().toISOString();
try {
updateNotesList();
} catch (e) {}
scheduleAutosave();
}
});
}
const searchTagInput = document.getElementById("searchTagInput");
if (searchTagInput) {
searchTagInput.addEventListener("input", () => {
try {
updateNotesList();
} catch (e) {}
});
}
const folderFilterEl = document.getElementById("folderFilter");
if (folderFilterEl) {
folderFilterEl.addEventListener("change", () => {
try { updateNotesList(); } catch (e) {}
});
}
// Close slash menu when clicking outside textarea
document.addEventListener("click", (e) => {
if (e.target !== textarea) {
hideSlashMenu();
}
});
initTheme();
loadNotes();
});
})();