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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -7,7 +7,7 @@
* See LICENSE file in repository root for details.
*/
import { EMOJI_GROUPS } from "@rustpad/emoji-data";
import { EMOJI_GROUPS } from "@rustpad/emoji-groups";
import { t } from "@rustpad/i18n";
const RECENTS_KEY = "rustpad:recent-emojis";
@@ -153,4 +153,9 @@ export function bindEmojiPicker({ editor, details, search, categories, grid, emp
document.addEventListener("rustpad:languagechange", () => {
if (details.open) render();
});
if (details.open) {
render();
requestAnimationFrame(() => search.focus());
}
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
* See LICENSE file in repository root for details.
*/
import { EMOJI_SHORTCODES } from "@rustpad/emoji-data";
import { EMOJI_SHORTCODES } from "@rustpad/emoji-shortcodes";
import { t } from "@rustpad/i18n";
import { parseImageAlias } from "@rustpad/image-alias";
+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" }); } });