mirror of
https://codeberg.org/Kankys/doxa.git
synced 2026-11-11 22:44:36 +01:00
1622 lines
51 KiB
JavaScript
1622 lines
51 KiB
JavaScript
(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, "<")
|
||
.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(
|
||
'<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 = "";
|
||
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"></></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();
|
||
});
|
||
})();
|