mirror of
https://codeberg.org/Kankys/doxa.git
synced 2026-11-11 14:44:36 +01:00
2116 lines
70 KiB
JavaScript
2116 lines
70 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",
|
|
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, """)
|
|
.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 = t('previewUnavailable');
|
|
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 (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 = `
|
|
<div class="modal" role="dialog" aria-modal="true">
|
|
<button class="modal-close" data-i18n-key="modalClose" data-i18n-attr="aria-label" aria-label="Zavřít">✖</button>
|
|
<h2 data-i18n-key="modalTitle">Upravit poznámku</h2>
|
|
<input id="modalNoteTitle" data-i18n-key="noteTitlePlaceholder" data-i18n-attr="placeholder" placeholder="Název poznámky" />
|
|
|
|
<div class="editor-toolbar modal-toolbar" id="modalEditorToolbar">
|
|
<button type="button" data-action="h1" data-i18n-key="toolbarH1" data-i18n-attr="title" title="Nadpis H1">H1</button>
|
|
<button type="button" data-action="h2" data-i18n-key="toolbarH2" data-i18n-attr="title" title="Nadpis H2">H2</button>
|
|
<button type="button" data-action="h3" data-i18n-key="toolbarH3" data-i18n-attr="title" title="Nadpis H3">H3</button>
|
|
|
|
<button type="button" data-action="bold" data-i18n-key="toolbarBold" data-i18n-attr="title" title="Tučné (Ctrl+B)"><strong>B</strong></button>
|
|
<button type="button" data-action="italic" data-i18n-key="toolbarItalic" data-i18n-attr="title" title="Kurzíva (Ctrl+I)"><em>I</em></button>
|
|
<button type="button" data-action="strike" data-i18n-key="toolbarStrike" data-i18n-attr="title" title="Přeškrtnutí">S</button>
|
|
|
|
<button type="button" data-action="ul" data-i18n-key="toolbarUl" data-i18n-attr="title" title="Odrážkový seznam">•</button>
|
|
<button type="button" data-action="ol" data-i18n-key="toolbarOl" data-i18n-attr="title" title="Číslovaný seznam">1.</button>
|
|
<button type="button" data-action="task" data-i18n-key="toolbarTask" data-i18n-attr="title" title="Úkol (checkbox)">☐</button>
|
|
|
|
<button type="button" data-action="quote" data-i18n-key="toolbarQuote" data-i18n-attr="title" title="Citace">❝</button>
|
|
<button type="button" data-action="hr" data-i18n-key="toolbarHr" data-i18n-attr="title" title="Vodorovná čára">—</button>
|
|
|
|
<button type="button" data-action="code" data-i18n-key="toolbarCode" data-i18n-attr="title" title="Inline kód"><code>kód</code></button>
|
|
<button type="button" data-action="codeblock" data-i18n-key="toolbarCodeblock" data-i18n-attr="title" title="Blok kódu"></></button>
|
|
|
|
<button type="button" data-action="link" data-i18n-key="toolbarLink" data-i18n-attr="title" title="Vložit odkaz">🔗</button>
|
|
<button type="button" data-action="image" data-i18n-key="toolbarImage" data-i18n-attr="title" title="Vložit obrázek">🖼</button>
|
|
<button type="button" data-action="table" data-i18n-key="toolbarTable" data-i18n-attr="title" title="Vložit tabulku">▦</button>
|
|
<button type="button" id="togglePreviewModal" data-i18n-key="toolbarPreview" data-i18n-attr="title" title="Náhled">Náhled</button>
|
|
</div>
|
|
|
|
<textarea id="modalNoteContent" data-i18n-key="noteInputPlaceholder" data-i18n-attr="placeholder" placeholder="Obsah poznámky"></textarea>
|
|
<div id="modalNotePreview" class="preview hidden" aria-hidden="true"></div>
|
|
|
|
<div class="modal-actions">
|
|
<button id="modalSaveBtn" data-i18n-key="modalSave">Uložit</button>
|
|
<button id="modalCancelBtn" data-i18n-key="modalCancel">Zrušit</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `<option value="all" data-i18n-key="filterAllFolders">${t('filterAllFolders')}</option>` + folders.map(f => `<option value="${f}">${f}</option>`).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 => `<option value="${f}"></option>`).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) =>
|
|
`<div class="slash-command-item ${idx === 0 ? 'active' : ''}" data-index="${idx}">
|
|
${t(cmd.i18nKey)}
|
|
</div>`
|
|
).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();
|
|
});
|
|
})(); |