v0.2.84
This commit is contained in:
1 parent
6329ac33d1
commit
90e3c399ea
22 files changed
+729
-46
No files matched your search
+173
-1
@@ -2424,7 +2424,179 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
});
|
||||
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await adapter.requestAccess(password); setAccessToken(adapter.access.kind, adapter.access.key, result.granted); accessToken = getAccessToken(adapter.access.kind, adapter.access.key); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; await loadNoteInfo(); void syncFavoriteButton(); loadFiles(); connect(); toast.success("Editing access has been unlocked.", { title: "Note unlocked" }); } catch (error) { document.querySelector("#password-error").textContent = error.message; toast.danger(error.message, { title: "Could not unlock note" }); } });
|
||||
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { if (info?.protected && !resourceUnlocked) { if (!passwordDialog.open) passwordDialog.showModal(); document.querySelector("#open-password")?.focus(); return; } historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Loading…</p>'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`; }).join("") : '<p class="empty">No history yet.</p>'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast.success("The selected revision is now the current version.", { title: "Version restored" }); }); } } catch (e) { list.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`; toast.danger(e.message, { title: "Could not load version history" }); } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
|
||||
const historyPanel = document.querySelector("#history-panel");
|
||||
const historyList = document.querySelector("#history-list");
|
||||
let historyRevisions = [];
|
||||
let historyView = "list";
|
||||
let historyArchiveYear = null;
|
||||
let historyArchiveDay = null;
|
||||
|
||||
function parseHistoryDate(value) {
|
||||
const raw = String(value ?? "").trim();
|
||||
let normalized = raw;
|
||||
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T") + ":00";
|
||||
else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T");
|
||||
else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(normalized)) normalized = normalized.replace(" ", "T") + "Z";
|
||||
const date = new Date(normalized);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function historyRevisionCard(revision, compact = false) {
|
||||
const snippet = escapeHtml(revision.content.trim().split("\n").slice(0, compact ? 2 : 3).join(" · ").slice(0, compact ? 110 : 150) || "Empty note");
|
||||
const author = revision.author || "Unknown author";
|
||||
return `<article class="revision${compact ? " revision--compact" : ""}" data-history-revision="${revision.id}"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(revision.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button type="button" data-preview="${revision.id}">Preview</button><button type="button" data-revision="${revision.id}">Restore</button><div class="revision__preview" hidden></div></div></article>`;
|
||||
}
|
||||
|
||||
function renderHistoryListView() {
|
||||
historyList.classList.remove("history-list--archive");
|
||||
historyList.innerHTML = historyRevisions.length
|
||||
? historyRevisions.map(revision => historyRevisionCard(revision)).join("")
|
||||
: '<p class="empty">No history yet.</p>';
|
||||
}
|
||||
|
||||
function dayKey(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function renderHistoryArchiveView() {
|
||||
historyList.classList.add("history-list--archive");
|
||||
const dated = historyRevisions
|
||||
.map(revision => ({ revision, date: parseHistoryDate(revision.created_at) }))
|
||||
.filter(item => item.date);
|
||||
if (!dated.length) {
|
||||
historyList.innerHTML = historyRevisions.length
|
||||
? '<p class="empty">Archive view needs valid revision timestamps.</p>'
|
||||
: '<p class="empty">No history yet.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const years = [...new Set(dated.map(item => item.date.getFullYear()))].sort((a, b) => b - a);
|
||||
if (!historyArchiveYear || !years.includes(historyArchiveYear)) historyArchiveYear = years[0];
|
||||
const yearItems = dated.filter(item => item.date.getFullYear() === historyArchiveYear);
|
||||
const monthCounts = Array.from({ length: 12 }, () => 0);
|
||||
const byDay = new Map();
|
||||
yearItems.forEach(item => {
|
||||
monthCounts[item.date.getMonth()] += 1;
|
||||
const key = dayKey(item.date);
|
||||
if (!byDay.has(key)) byDay.set(key, []);
|
||||
byDay.get(key).push(item.revision);
|
||||
});
|
||||
const maxMonthCount = Math.max(1, ...monthCounts);
|
||||
const months = Array.from({ length: 12 }, (_, month) => month);
|
||||
const monthNames = new Intl.DateTimeFormat(undefined, { month: "short" });
|
||||
const monthNamesLong = new Intl.DateTimeFormat(undefined, { month: "long" });
|
||||
const yearButtons = years.map(year => `<button type="button" data-history-year="${year}" class="${year === historyArchiveYear ? "active" : ""}" aria-pressed="${year === historyArchiveYear}">${year}</button>`).join("");
|
||||
const histogram = months.map(month => {
|
||||
const count = monthCounts[month];
|
||||
const height = count ? Math.max(12, Math.round((count / maxMonthCount) * 72)) : 3;
|
||||
return `<div class="history-archive__bar-cell" title="${escapeHtml(monthNamesLong.format(new Date(historyArchiveYear, month, 1)))}: ${count} revisions"><span class="history-archive__bar" style="--history-bar-height:${height}px"></span><small>${escapeHtml(monthNames.format(new Date(historyArchiveYear, month, 1)))}</small></div>`;
|
||||
}).join("");
|
||||
|
||||
const calendars = months.filter(month => monthCounts[month] > 0).map(month => {
|
||||
const first = new Date(historyArchiveYear, month, 1);
|
||||
const daysInMonth = new Date(historyArchiveYear, month + 1, 0).getDate();
|
||||
const mondayOffset = (first.getDay() + 6) % 7;
|
||||
const blanks = Array.from({ length: mondayOffset }, () => '<span class="history-calendar__blank"></span>').join("");
|
||||
let days = "";
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const key = `${historyArchiveYear}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
const revisions = byDay.get(key) || [];
|
||||
const active = historyArchiveDay === key;
|
||||
days += revisions.length
|
||||
? `<button type="button" class="history-calendar__day${active ? " active" : ""}" data-history-day="${key}" title="${revisions.length} revision${revisions.length === 1 ? "" : "s"}"><span>${day}</span><small>${revisions.length}</small></button>`
|
||||
: `<span class="history-calendar__day history-calendar__day--empty"><span>${day}</span></span>`;
|
||||
}
|
||||
return `<section class="history-calendar__month"><div class="history-calendar__month-head"><strong>${escapeHtml(monthNamesLong.format(first))}</strong><span>${monthCounts[month]} revisions</span></div><div class="history-calendar__weekdays"><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span></div><div class="history-calendar__days">${blanks}${days}</div></section>`;
|
||||
}).join("");
|
||||
|
||||
const selected = historyArchiveDay && byDay.get(historyArchiveDay);
|
||||
const selectedHtml = selected?.length
|
||||
? `<section class="history-archive__selected"><div class="history-archive__selected-head"><strong>${escapeHtml(historyArchiveDay)}</strong><span>${selected.length} saved version${selected.length === 1 ? "" : "s"}</span></div>${selected.map(revision => historyRevisionCard(revision, true)).join("")}</section>`
|
||||
: '<p class="history-archive__hint">Select a highlighted day to inspect saved versions.</p>';
|
||||
|
||||
historyList.innerHTML = `<div class="history-archive"><div class="history-archive__years" aria-label="Archive year">${yearButtons}</div><div class="history-archive__histogram" aria-label="Revision activity by month">${histogram}</div><div class="history-calendar">${calendars}</div>${selectedHtml}</div>`;
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
document.querySelectorAll("[data-history-view]").forEach(button => {
|
||||
const active = button.dataset.historyView === historyView;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
});
|
||||
if (historyView === "archive") renderHistoryArchiveView();
|
||||
else renderHistoryListView();
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-history-view]").forEach(button => button.addEventListener("click", () => {
|
||||
historyView = button.dataset.historyView === "archive" ? "archive" : "list";
|
||||
renderHistory();
|
||||
}));
|
||||
|
||||
historyList.addEventListener("click", async event => {
|
||||
const yearButton = event.target.closest("[data-history-year]");
|
||||
if (yearButton) {
|
||||
historyArchiveYear = Number(yearButton.dataset.historyYear);
|
||||
historyArchiveDay = null;
|
||||
renderHistoryArchiveView();
|
||||
return;
|
||||
}
|
||||
const dayButton = event.target.closest("[data-history-day]");
|
||||
if (dayButton) {
|
||||
historyArchiveDay = dayButton.dataset.historyDay;
|
||||
renderHistoryArchiveView();
|
||||
return;
|
||||
}
|
||||
const previewButton = event.target.closest("[data-preview]");
|
||||
if (previewButton) {
|
||||
const revision = historyRevisions.find(item => String(item.id) === previewButton.dataset.preview);
|
||||
const previewNode = previewButton.closest(".revision")?.querySelector(".revision__preview");
|
||||
if (!revision || !previewNode) return;
|
||||
previewNode.hidden = !previewNode.hidden;
|
||||
previewNode.textContent = revision.content;
|
||||
return;
|
||||
}
|
||||
const restoreButton = event.target.closest("[data-revision]");
|
||||
if (!restoreButton) return;
|
||||
restoreButton.disabled = true;
|
||||
try {
|
||||
await adapter.restoreRevision(Number(restoreButton.dataset.revision), accessToken);
|
||||
toast.success("The selected revision is now the current version.", { title: "Version restored" });
|
||||
historyRevisions = await adapter.loadHistory(accessToken);
|
||||
renderHistory();
|
||||
} catch (error) {
|
||||
toast.danger(error.message, { title: "Could not restore version" });
|
||||
restoreButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector("#history-button").addEventListener("click", async () => {
|
||||
if (info?.protected && !resourceUnlocked) {
|
||||
if (!passwordDialog.open) passwordDialog.showModal();
|
||||
document.querySelector("#open-password")?.focus();
|
||||
return;
|
||||
}
|
||||
historyPanel.classList.add("open");
|
||||
historyPanel.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("history-open");
|
||||
historyList.innerHTML = '<p class="empty">Loading…</p>';
|
||||
try {
|
||||
historyRevisions = await adapter.loadHistory(accessToken);
|
||||
historyArchiveYear = null;
|
||||
historyArchiveDay = null;
|
||||
renderHistory();
|
||||
} catch (e) {
|
||||
historyList.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`;
|
||||
toast.danger(e.message, { title: "Could not load version history" });
|
||||
}
|
||||
});
|
||||
document.querySelector("#close-history").addEventListener("click", () => {
|
||||
historyPanel.classList.remove("open");
|
||||
historyPanel.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("history-open");
|
||||
});
|
||||
|
||||
const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast.danger(error.message, { title: "Could not delete note" }); } });
|
||||
initialize();
|
||||
|
||||
Reference in new issue
Block a user