This commit is contained in:
Mateusz Gruszczyński
2026-09-20 13:56:53 +02:00
parent a6efa52f78
commit 0a269cf6fb
11 changed files with 437 additions and 22 deletions
+49 -14
View File
@@ -17,10 +17,8 @@ import { CollaborationRevisionGapError, CollaborationSession } from "@rustpad/co
import { copyText } from "@rustpad/clipboard";
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
import { applyFormat, bindFormatShortcuts, bindIndentationShortcuts } from "@rustpad/editor-format";
import { bindEmojiPicker } from "@rustpad/emoji-picker";
import { updateImageAliasInLineBySource } from "@rustpad/image-alias";
import { previewEditingHost } from "@rustpad/preview-edit";
import { bindPdfExport } from "@rustpad/pdf-export";
import { createRenderQueue } from "@rustpad/render-queue";
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
@@ -270,12 +268,7 @@ export function startNoteEditor(adapter) {
documentTitle.textContent = info.title;
}
globalColor = info.global_color || ""; noteColor = info.note_color || "";
if (getAuthToken()) {
const colors = await adapter.loadColor(accountHeaders());
globalColor = colors.global_color || ""; noteColor = colors.note_color || "";
} else {
noteColor = readGuestColor();
}
if (!getAuthToken()) noteColor = readGuestColor();
updateMarkdownFiles(info.files || []);
if (info.personal_editor_settings) {
compactToggle.checked = info.compact_view !== false;
@@ -1633,7 +1626,7 @@ export function startNoteEditor(adapter) {
});
socket.connect();
}
bindIdentityDialog({ dialog: identityDialog, onIdentity: async (value, session) => { nickname = value; accountSession = session || await validateCurrentSession(); accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); await syncFavoriteButton(); if (info.protected && info.access_level === "none") passwordDialog.showModal(); else { loadFiles(); connect(); } } });
bindIdentityDialog({ dialog: identityDialog, onIdentity: async (value, session) => { nickname = value; accountSession = session || await validateCurrentSession(); accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && info.access_level === "none") passwordDialog.showModal(); else { loadFiles(); connect(); } } });
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
async function showSystemNotFound() {
try {
@@ -1667,7 +1660,6 @@ export function startNoteEditor(adapter) {
adapter.configureView?.(info);
applyUi({ write: true, replace: true });
updateCurrentUser();
await syncFavoriteButton();
if (info.protected && info.access_level === "none") passwordDialog.showModal();
else { loadFiles(); connect(); }
} catch (e) {
@@ -2074,14 +2066,57 @@ export function startNoteEditor(adapter) {
});
bindFormatShortcuts(editor);
bindIndentationShortcuts(editor, { size: 2 });
bindPdfExport({
const pdfExportArgs = {
button: document.querySelector("#export-pdf"),
dialog: document.querySelector("#pdf-export-dialog"),
form: document.querySelector("#pdf-export-form"),
preview,
title: () => document.querySelector("#document-title")?.textContent?.trim() || document.title,
};
let pdfExportBound = false;
let pdfExportLoad = null;
pdfExportArgs.button?.addEventListener("click", async event => {
if (pdfExportBound) return;
event.stopImmediatePropagation();
pdfExportLoad ||= import("@rustpad/pdf-export").then(({ bindPdfExport }) => {
bindPdfExport(pdfExportArgs);
pdfExportBound = true;
});
try {
await pdfExportLoad;
if (!pdfExportArgs.dialog.open) pdfExportArgs.dialog.showModal();
} catch (error) {
pdfExportLoad = null;
console.error("Failed to load PDF export", error);
toast.danger(t("editor.pdfExportFailed", {}, "Could not prepare the PDF."), {
title: t("editor.exportPdf", {}, "Export PDF"),
});
}
});
const emojiPickerArgs = {
editor,
details: document.querySelector("#emoji-picker"),
search: document.querySelector("#emoji-search"),
categories: document.querySelector("#emoji-categories"),
grid: document.querySelector("#emoji-grid"),
empty: document.querySelector("#emoji-empty"),
};
let emojiPickerLoad = null;
const loadEmojiPicker = async () => {
if (emojiPickerLoad) return emojiPickerLoad;
emojiPickerLoad = import("@rustpad/emoji-picker").then(({ bindEmojiPicker }) => {
bindEmojiPicker(emojiPickerArgs);
}).catch(error => {
emojiPickerLoad = null;
throw error;
});
return emojiPickerLoad;
};
emojiPickerArgs.details?.addEventListener("toggle", () => {
if (!emojiPickerArgs.details.open) return;
void loadEmojiPicker().catch(error => console.error("Failed to load emoji picker", error));
});
bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") });
document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal());
document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close());
@@ -2259,7 +2294,7 @@ export function startNoteEditor(adapter) {
password = "";
setPagePasswordInput.value = "";
await loadNoteInfo();
await syncFavoriteButton();
void syncFavoriteButton();
resourceUnlocked = false;
socket?.stop();
loadFiles();
@@ -2357,7 +2392,7 @@ export function startNoteEditor(adapter) {
document.querySelector("#open-password")?.focus();
}
});
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(); await 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" }); } });
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 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" }); } });