new finctions and fixes
This commit is contained in:
@@ -28,6 +28,25 @@ function safeUrl(value) {
|
||||
}
|
||||
|
||||
const emoji = EMOJI_SHORTCODES;
|
||||
let markdownFiles = new Map();
|
||||
|
||||
export function setMarkdownFiles(files) {
|
||||
markdownFiles = new Map((Array.isArray(files) ? files : [])
|
||||
.filter(file => file && file.filename && file.url)
|
||||
.map(file => [String(file.filename), {
|
||||
url: String(file.url),
|
||||
mimeType: String(file.mime_type || ""),
|
||||
}]));
|
||||
}
|
||||
|
||||
export function unresolvedMarkdownFileAliases(value) {
|
||||
const missing = new Set();
|
||||
const source = String(value || "").replace(/`[^`]*`/g, "");
|
||||
for (const match of source.matchAll(/\[(?:file|image|img)=([^,\]\s]+)(?:,[^\]]*)?\]/gi)) {
|
||||
if (!markdownFiles.has(match[1])) missing.add(match[1]);
|
||||
}
|
||||
return [...missing];
|
||||
}
|
||||
|
||||
function inline(value) {
|
||||
const tokens = [];
|
||||
@@ -39,6 +58,16 @@ function inline(value) {
|
||||
let html = escapeHtml(value);
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, (_, code) => stash(`<code>${code}</code>`));
|
||||
html = html.replace(/\[(file|image|img)=([^,\]\s]+)(?:,([^\]]*))?\]/gi, (match, kind, filename, label) => {
|
||||
const file = markdownFiles.get(filename);
|
||||
if (!file) return match;
|
||||
const text = String(label || filename).trim() || filename;
|
||||
if (kind.toLowerCase() === "file") {
|
||||
return stash(`<a href="${safeUrl(file.url)}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-file-alias="file" data-file-name="${filename}">${text}</a>`);
|
||||
}
|
||||
if (!file.mimeType.startsWith("image/")) return match;
|
||||
return stash(`<img src="${safeUrl(file.url)}" alt="${text}" loading="lazy" decoding="async" referrerpolicy="no-referrer" draggable="false" contenteditable="false" data-file-alias="image" data-file-name="${filename}">`);
|
||||
});
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => {
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
return stash(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async" referrerpolicy="no-referrer" draggable="false" contenteditable="false"${titleAttr}>`);
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createPadAdapter() {
|
||||
|
||||
return {
|
||||
access: { kind: "pad", key: slug },
|
||||
addressSelector: "#pad-url",
|
||||
addressSelector: "#document-url",
|
||||
title: info => `${info.title} · RustPad`,
|
||||
loadInfo: headers => api(base, { headers }),
|
||||
loadColor: headers => api(`${base}/editor-color`, { headers }),
|
||||
@@ -62,7 +62,7 @@ export function createWorkspaceNoteAdapter() {
|
||||
|
||||
return {
|
||||
access: { kind: "workspace", key: workspaceSlug },
|
||||
addressSelector: "#note-url",
|
||||
addressSelector: "#document-url",
|
||||
title: info => `${info.title} · ${info.workspace_title}`,
|
||||
loadInfo: headers => api(base, { headers }),
|
||||
loadColor: headers => api(`${base}/editor-color`, { headers }),
|
||||
|
||||
+158
-25
@@ -15,7 +15,7 @@ import { copyText } from "@rustpad/clipboard";
|
||||
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
|
||||
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
|
||||
import { bindEmojiPicker } from "@rustpad/emoji-picker";
|
||||
import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
|
||||
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
|
||||
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
|
||||
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
||||
import { bindNoteFiles } from "@rustpad/note-files";
|
||||
@@ -27,24 +27,69 @@ export function startNoteEditor(adapter) {
|
||||
const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
||||
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread");
|
||||
let unreadChat = 0;
|
||||
const compactToggle = document.querySelector("#compact-toggle"), lineLinksToggle = document.querySelector("#line-links-toggle"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), saveEditorSettingsButton = document.querySelector("#save-editor-settings"), publicPageEnabled = document.querySelector("#public-page-enabled"), publicTaskUpdates = document.querySelector("#public-task-updates"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), mobileColorPicker = document.querySelector("#mobile-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
|
||||
const compactToggle = document.querySelector("#compact-toggle"), lineLinksToggle = document.querySelector("#line-links-toggle"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), publicPageEnabled = document.querySelector("#public-page-enabled"), publicTaskUpdates = document.querySelector("#public-task-updates"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), mobileColorPicker = document.querySelector("#mobile-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
|
||||
const mobileFontFamily = document.querySelector("#mobile-font-family"), mobileFontSize = document.querySelector("#mobile-font-size"), mobileLineToggle = document.querySelector("#mobile-line-numbers-toggle"), mobilePreviewLineToggle = document.querySelector("#mobile-preview-line-numbers-toggle"), mobileCompactToggle = document.querySelector("#mobile-compact-toggle"), mobileLineLinksToggle = document.querySelector("#mobile-line-links-toggle");
|
||||
const shareToken = new URLSearchParams(location.search).get("share"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
|
||||
const notePreferenceKey = name => `rustpad:${name}:${adapter.access.kind}:${adapter.access.key}`;
|
||||
const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`;
|
||||
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
|
||||
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false;
|
||||
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
|
||||
const singlePaneQuery = window.matchMedia("(max-width: 760px)");
|
||||
let compactView = uiState.view === "preview" ? "preview" : "edit";
|
||||
const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem("rustpad:line-numbers") !== "off";
|
||||
previewLineToggle.checked = localStorage.getItem("rustpad:preview-line-numbers") === "on";
|
||||
compactToggle.checked = localStorage.getItem("rustpad:compact") !== "off";
|
||||
lineLinksToggle.checked = localStorage.getItem("rustpad:line-links") === "on";
|
||||
fontFamily.value = localStorage.getItem("rustpad:font-family") || "mono";
|
||||
fontSize.value = localStorage.getItem("rustpad:font-size") || "14";
|
||||
let refreshFilesForAliases = () => { };
|
||||
let aliasRefreshTimer = 0;
|
||||
let lastUnresolvedAliasKey = "";
|
||||
let markdownFileSignature = "";
|
||||
function updateMarkdownFiles(files, { rerender = false } = {}) {
|
||||
const normalized = (Array.isArray(files) ? files : []).map(file => ({
|
||||
filename: String(file?.filename || ""),
|
||||
url: String(file?.url || ""),
|
||||
mime_type: String(file?.mime_type || ""),
|
||||
})).sort((left, right) => left.filename.localeCompare(right.filename));
|
||||
const nextSignature = JSON.stringify(normalized);
|
||||
const changed = nextSignature !== markdownFileSignature;
|
||||
markdownFileSignature = nextSignature;
|
||||
setMarkdownFiles(normalized);
|
||||
if (rerender && changed) render();
|
||||
}
|
||||
function scheduleAliasFileRefresh(content) {
|
||||
const key = unresolvedMarkdownFileAliases(content).sort().join("\u0000");
|
||||
if (!key) {
|
||||
lastUnresolvedAliasKey = "";
|
||||
return;
|
||||
}
|
||||
if (key === lastUnresolvedAliasKey) return;
|
||||
lastUnresolvedAliasKey = key;
|
||||
clearTimeout(aliasRefreshTimer);
|
||||
aliasRefreshTimer = window.setTimeout(() => refreshFilesForAliases(), 200);
|
||||
}
|
||||
const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem(notePreferenceKey("line-numbers")) !== "off";
|
||||
previewLineToggle.checked = localStorage.getItem(notePreferenceKey("preview-line-numbers")) === "on";
|
||||
compactToggle.checked = localStorage.getItem(notePreferenceKey("compact")) !== "off";
|
||||
lineLinksToggle.checked = localStorage.getItem(notePreferenceKey("line-links")) === "on";
|
||||
fontFamily.value = localStorage.getItem(notePreferenceKey("font-family")) || "mono";
|
||||
fontSize.value = localStorage.getItem(notePreferenceKey("font-size")) || "14";
|
||||
authorshipColorsToggle.checked = authorshipColorsEnabled;
|
||||
function syncMobileEditorControls() {
|
||||
if (mobileFontFamily) mobileFontFamily.value = fontFamily.value;
|
||||
if (mobileFontSize) mobileFontSize.value = fontSize.value;
|
||||
if (mobileLineToggle) mobileLineToggle.checked = lineToggle.checked;
|
||||
if (mobilePreviewLineToggle) mobilePreviewLineToggle.checked = previewLineToggle.checked;
|
||||
if (mobileCompactToggle) mobileCompactToggle.checked = compactToggle.checked;
|
||||
if (mobileLineLinksToggle) mobileLineLinksToggle.checked = lineLinksToggle.checked;
|
||||
}
|
||||
syncMobileEditorControls();
|
||||
function updateAuthorshipControls() {
|
||||
const canManage = info?.can_manage_authorship === true;
|
||||
authorshipColorsToggle.checked = authorshipColorsEnabled;
|
||||
authorshipColorsToggle.disabled = !canManage;
|
||||
authorshipColorsLabel.textContent = authorshipColorsEnabled ? "Colors on" : "Colors off";
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode));
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => {
|
||||
button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode);
|
||||
button.disabled = !canManage;
|
||||
});
|
||||
const controls = document.querySelector(".authorship-controls");
|
||||
if (controls) controls.title = canManage ? "Global authorship settings" : "Only the owner can change authorship settings";
|
||||
}
|
||||
function defaultColorFor(name) { let h = 0; for (const c of name || "?") h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 70% 62%)`; }
|
||||
function ownerParts(owner) { const raw = String(owner || ""); const split = raw.lastIndexOf("\u001f"); return split < 0 ? { name: raw, color: "" } : { name: raw.slice(0, split), color: raw.slice(split + 1) }; }
|
||||
@@ -67,7 +112,13 @@ export function startNoteEditor(adapter) {
|
||||
}, replacement, contentLength);
|
||||
}
|
||||
function updateCurrentUser() { const color = currentUserColor() || defaultColorFor(nickname); const pickerColor = /^#[0-9a-f]{6}$/i.test(color) ? color : "#7c6cff"; const overridden = Boolean(noteUserColor()); currentUser.querySelector(".user-chip__name").textContent = nickname; currentUser.style.setProperty("--owner", color); currentUser.title = overridden ? "Note color override" : "Global profile color"; userColorPicker.value = pickerColor; if (mobileColorPicker) mobileColorPicker.value = pickerColor; useGlobalColorButton.hidden = !overridden; document.querySelector(".mobile-editor-bubble")?.style.setProperty("--owner", color); }
|
||||
function sessionHeaders() { const token = accessToken || getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||
function sessionHeaders() {
|
||||
const userToken = getAuthToken();
|
||||
const resourceToken = accessToken || userToken;
|
||||
const headers = resourceToken ? { Authorization: `Bearer ${resourceToken}` } : {};
|
||||
if (userToken && userToken !== resourceToken) headers["X-RustPad-User-Token"] = userToken;
|
||||
return headers;
|
||||
}
|
||||
function accountHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||
async function loadNoteInfo() {
|
||||
info = await adapter.loadInfo(sessionHeaders());
|
||||
@@ -78,10 +129,19 @@ export function startNoteEditor(adapter) {
|
||||
} else {
|
||||
noteColor = readGuestColor();
|
||||
}
|
||||
updateMarkdownFiles(info.files || []);
|
||||
if (info.personal_editor_settings) {
|
||||
compactToggle.checked = info.compact_view !== false;
|
||||
lineToggle.checked = info.editor_line_numbers !== false;
|
||||
previewLineToggle.checked = info.preview_line_numbers === true;
|
||||
lineLinksToggle.checked = info.line_links === true;
|
||||
if (["mono", "system", "serif", "arial", "georgia"].includes(info.font_family)) fontFamily.value = info.font_family;
|
||||
if (["14", "16", "18", "20", "22"].includes(String(info.font_size))) fontSize.value = String(info.font_size);
|
||||
}
|
||||
authorshipMode = info.authorship_mode === "full" ? "full" : "simple";
|
||||
authorshipColorsEnabled = info.colors_enabled !== false;
|
||||
updateAuthorshipControls();
|
||||
if (saveEditorSettingsButton) saveEditorSettingsButton.disabled = !info.can_save_editor_settings;
|
||||
syncMobileEditorControls();
|
||||
updateCurrentUser(); return info;
|
||||
}
|
||||
function updatePresence(users) { const entries = Array.isArray(users) ? users : []; presenceUsers = entries.map(entry => typeof entry === "string" ? { name: entry, color: "" } : entry || {}); roomCount.textContent = `${entries.length} ${entries.length === 1 ? "user" : "users"}`; roomUsers.replaceChildren(...presenceUsers.map(user => { const li = document.createElement("li"), dot = document.createElement("span"), label = document.createElement("span"); li.className = "room-user"; dot.className = "room-user__dot"; dot.style.setProperty("--owner", /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(user.name)); label.textContent = user.name || "Guest"; li.title = label.textContent; li.append(dot, label); return li; })); if (!entries.length) { const li = document.createElement("li"); li.textContent = "No active users"; roomUsers.append(li); } renderGutter(); }
|
||||
@@ -202,10 +262,17 @@ export function startNoteEditor(adapter) {
|
||||
if (tag === "code") return { open: "`", close: "`", atomic: null };
|
||||
if (tag === "sub") return { open: "~", close: "~", atomic: null };
|
||||
if (tag === "sup" && !current.classList.contains("footnote-ref")) return { open: "^", close: "^", atomic: null };
|
||||
const fileAlias = current.getAttribute("data-file-alias");
|
||||
const fileName = current.getAttribute("data-file-name");
|
||||
if (tag === "a" && fileAlias === "file" && fileName) return { open: `[file=${fileName},`, close: "]", atomic: null };
|
||||
if (tag === "a") return { open: "[", close: `](${current.getAttribute("href") || "#"})`, atomic: null };
|
||||
if (tag === "img") {
|
||||
const src = current.getAttribute("src") || "";
|
||||
const alt = current.getAttribute("alt") || "";
|
||||
if (fileAlias === "image" && fileName) {
|
||||
const safeAlt = alt.replace(/\]/g, ")").replace(/[\r\n]+/g, " ");
|
||||
return { open: "", close: "", atomic: `[image=${fileName},${safeAlt}]` };
|
||||
}
|
||||
const src = current.getAttribute("src") || "";
|
||||
const title = current.getAttribute("title");
|
||||
return { open: "", close: "", atomic: `}"` : ""})` };
|
||||
}
|
||||
@@ -478,7 +545,7 @@ export function startNoteEditor(adapter) {
|
||||
const ratio = editorRange > 0 ? editor.scrollTop / editorRange : 0;
|
||||
preview.scrollTop = ratio * previewRange;
|
||||
}
|
||||
function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}">${escapeHtml(line) || "<br>"}</div>`).join(""); document.querySelector("#preview-label").textContent = "Text preview · editable"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); requestAnimationFrame(syncPreviewScroll); }
|
||||
function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); scheduleAliasFileRefresh(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}">${escapeHtml(line) || "<br>"}</div>`).join(""); document.querySelector("#preview-label").textContent = "Text preview · editable"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); requestAnimationFrame(syncPreviewScroll); }
|
||||
function activeView() {
|
||||
return singlePaneQuery.matches ? compactView : uiState.view;
|
||||
}
|
||||
@@ -549,9 +616,11 @@ export function startNoteEditor(adapter) {
|
||||
const { loadFiles } = bindNoteFiles({
|
||||
editor, toast, getAccessToken: () => accessToken, canDelete: () => Boolean(info?.can_delete_files),
|
||||
endpoints: adapter.fileEndpoints,
|
||||
onFilesChanged: files => updateMarkdownFiles(files, { rerender: true }),
|
||||
});
|
||||
refreshFilesForAliases = () => loadFiles();
|
||||
function connect() { socket?.stop(); socket = adapter.createSocket({ password, accessToken, nickname, color: currentUserColor() || null, sessionToken: getAuthToken(), guestId: getGuestId(), onStatus: s => setStatus(s === "online" ? "online" : s === "offline" ? "offline" : null, s === "online" ? "Connected" : s === "offline" ? "Reconnecting…" : "Connecting…"), onAuthenticated: m => { resourceUnlocked = true; if (passwordDialog.open) passwordDialog.close(); const readOnly = m.access_level === "read_only"; editor.readOnly = readOnly; accessLevel.textContent = readOnly ? "Access: read only" : "Access: full"; applyRemote(m.content, m.owner_map); if (!readOnly) editor.focus(); }, onDocument: m => { applyRemote(m.content, m.owner_map); document.querySelector("#save-state").textContent = `${m.author ? `${m.author} · ` : ""}${new Date(m.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`; }, onPresence: updatePresence, onLatency: updateLatency, onChat: appendChatMessage, onError: m => { const friendly = /read-only access/i.test(m) ? "This note is read only. Enter the password or ask the owner to grant write access." : m; document.querySelector("#password-error").textContent = friendly; if (/read-only access/i.test(m)) { toast(friendly); accessLevel.textContent = "Access: read only"; editor.readOnly = true; return; } if (/nickname|session|account/i.test(m)) { if (!identityDialog.open) identityDialog.showModal(); } else if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); } }); socket.connect(); }
|
||||
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key) || getAuthToken(); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
|
||||
async function showSystemNotFound() {
|
||||
try {
|
||||
@@ -579,7 +648,7 @@ export function startNoteEditor(adapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key);
|
||||
accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key) || getAuthToken();
|
||||
await loadNoteInfo();
|
||||
document.title = adapter.title(info);
|
||||
publicPageEnabled.checked = Boolean(info.public_page_enabled); publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected); updatePageControls();
|
||||
@@ -631,7 +700,24 @@ export function startNoteEditor(adapter) {
|
||||
compactLayoutQuery.addEventListener("change", event => {
|
||||
if (!event.matches) setHeaderMenuOpen(false);
|
||||
});
|
||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); }); lineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:line-numbers", lineToggle.checked ? "on" : "off"); renderGutter(); }); previewLineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:preview-line-numbers", previewLineToggle.checked ? "on" : "off"); renderGutter(); alignPreviewLineNumbers(preview); }); compactToggle.addEventListener("change", () => { localStorage.setItem("rustpad:compact", compactToggle.checked ? "on" : "off"); applyUi(); }); lineLinksToggle.addEventListener("change", () => { localStorage.setItem("rustpad:line-links", lineLinksToggle.checked ? "on" : "off"); renderGutter(); }); fontFamily.addEventListener("change", () => { localStorage.setItem("rustpad:font-family", fontFamily.value); applyUi(); }); fontSize.addEventListener("change", () => { localStorage.setItem("rustpad:font-size", fontSize.value); applyUi(); });
|
||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); });
|
||||
lineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("line-numbers"), lineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
previewLineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("preview-line-numbers"), previewLineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); alignPreviewLineNumbers(preview); scheduleEditorSettingsSave({ personal: true }); });
|
||||
compactToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("compact"), compactToggle.checked ? "on" : "off"); syncMobileEditorControls(); applyUi(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
lineLinksToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("line-links"), lineLinksToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
fontFamily.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("font-family"), fontFamily.value); syncMobileEditorControls(); applyUi(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
fontSize.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("font-size"), fontSize.value); syncMobileEditorControls(); applyUi(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
const mirrorMobileControl = (mobileControl, desktopControl) => mobileControl?.addEventListener("change", () => {
|
||||
if (desktopControl instanceof HTMLInputElement && desktopControl.type === "checkbox") desktopControl.checked = mobileControl.checked;
|
||||
else desktopControl.value = mobileControl.value;
|
||||
desktopControl.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
mirrorMobileControl(mobileFontFamily, fontFamily);
|
||||
mirrorMobileControl(mobileFontSize, fontSize);
|
||||
mirrorMobileControl(mobileLineToggle, lineToggle);
|
||||
mirrorMobileControl(mobilePreviewLineToggle, previewLineToggle);
|
||||
mirrorMobileControl(mobileCompactToggle, compactToggle);
|
||||
mirrorMobileControl(mobileLineLinksToggle, lineLinksToggle);
|
||||
document.querySelector("#mobile-files-button")?.addEventListener("click", () => document.querySelector("#files-button")?.click());
|
||||
const compactBubbleQuery = matchMedia("(max-width: 1499px)");
|
||||
const roomPopover = roomDetails.querySelector(".room-popover");
|
||||
@@ -788,27 +874,66 @@ export function startNoteEditor(adapter) {
|
||||
});
|
||||
}
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.addEventListener("click", () => {
|
||||
if (!info?.can_manage_authorship) return;
|
||||
authorshipMode = button.dataset.authorshipMode === "full" ? "full" : "simple";
|
||||
updateAuthorshipControls();
|
||||
syncMobileEditorControls();
|
||||
renderGutter();
|
||||
scheduleEditorSettingsSave({ authorship: true });
|
||||
}));
|
||||
authorshipColorsToggle?.addEventListener("change", () => {
|
||||
if (!info?.can_manage_authorship) return;
|
||||
authorshipColorsEnabled = authorshipColorsToggle.checked;
|
||||
updateAuthorshipControls();
|
||||
syncMobileEditorControls();
|
||||
renderGutter();
|
||||
scheduleEditorSettingsSave({ authorship: true });
|
||||
});
|
||||
saveEditorSettingsButton?.addEventListener("click", async () => {
|
||||
if (!info?.can_save_editor_settings) return;
|
||||
saveEditorSettingsButton.disabled = true;
|
||||
function personalEditorSettingsPayload() {
|
||||
return {
|
||||
compact_view: compactToggle.checked,
|
||||
editor_line_numbers: lineToggle.checked,
|
||||
preview_line_numbers: previewLineToggle.checked,
|
||||
line_links: lineLinksToggle.checked,
|
||||
font_family: fontFamily.value,
|
||||
font_size: Number(fontSize.value),
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleEditorSettingsSave({ personal = false, authorship = false } = {}) {
|
||||
if (personal) pendingPersonalSettingsSave = true;
|
||||
if (authorship && info?.can_manage_authorship) pendingAuthorshipSettingsSave = true;
|
||||
if (!info?.can_save_editor_settings || (!pendingPersonalSettingsSave && !pendingAuthorshipSettingsSave)) return;
|
||||
clearTimeout(editorSettingsSaveTimer);
|
||||
editorSettingsSaveTimer = window.setTimeout(flushEditorSettingsSave, 250);
|
||||
}
|
||||
|
||||
async function flushEditorSettingsSave() {
|
||||
if (editorSettingsSaveInFlight || !info?.can_save_editor_settings) return;
|
||||
const savePersonal = pendingPersonalSettingsSave;
|
||||
const saveAuthorship = pendingAuthorshipSettingsSave && info.can_manage_authorship;
|
||||
if (!savePersonal && !saveAuthorship) return;
|
||||
pendingPersonalSettingsSave = false;
|
||||
pendingAuthorshipSettingsSave = false;
|
||||
editorSettingsSaveInFlight = true;
|
||||
const settings = savePersonal ? personalEditorSettingsPayload() : {};
|
||||
if (saveAuthorship) {
|
||||
settings.authorship_mode = authorshipMode;
|
||||
settings.colors_enabled = authorshipColorsEnabled;
|
||||
}
|
||||
try {
|
||||
await adapter.saveEditorSettings(sessionHeaders(), { authorship_mode: authorshipMode, colors_enabled: authorshipColorsEnabled });
|
||||
toast("Editor settings saved for everyone");
|
||||
await adapter.saveEditorSettings(sessionHeaders(), settings);
|
||||
if (savePersonal) info.personal_editor_settings = true;
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
} finally {
|
||||
saveEditorSettingsButton.disabled = !info?.can_save_editor_settings;
|
||||
editorSettingsSaveInFlight = false;
|
||||
if (pendingPersonalSettingsSave || pendingAuthorshipSettingsSave) {
|
||||
clearTimeout(editorSettingsSaveTimer);
|
||||
editorSettingsSaveTimer = window.setTimeout(flushEditorSettingsSave, 250);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
window.addEventListener("popstate", () => { lastRevealedLineHash = ""; uiState = readEditorState(); applyUi(); requestAnimationFrame(revealLinkedLine); });
|
||||
window.addEventListener("hashchange", () => { lastRevealedLineHash = ""; renderGutter(); requestAnimationFrame(revealLinkedLine); });
|
||||
window.addEventListener("rustpad:urlchange", updateAddressLabel);
|
||||
@@ -826,9 +951,17 @@ export function startNoteEditor(adapter) {
|
||||
toast(error.message);
|
||||
}
|
||||
});
|
||||
document.querySelector("#copy-link").addEventListener("click", async () => {
|
||||
async function copyCurrentLink() {
|
||||
try { await copyText(currentShareUrl(uiState)); toast("Link copied"); }
|
||||
catch (error) { toast(error.message); }
|
||||
}
|
||||
document.querySelector("#copy-link").addEventListener("click", copyCurrentLink);
|
||||
const documentLinkCopy = document.querySelector("#document-link-copy");
|
||||
documentLinkCopy?.addEventListener("click", copyCurrentLink);
|
||||
documentLinkCopy?.addEventListener("keydown", event => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
copyCurrentLink();
|
||||
});
|
||||
|
||||
let pendingPreviewFormatRange = null;
|
||||
|
||||
+19
-7
@@ -31,7 +31,17 @@ function formatDate(value) {
|
||||
return Number.isNaN(date.getTime()) ? "" : date.toLocaleString("pl-PL");
|
||||
}
|
||||
|
||||
export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, toast }) {
|
||||
function aliasCode(filename, label, mimeType) {
|
||||
const kind = String(mimeType || "").startsWith("image/") ? "image" : "file";
|
||||
const safeLabel = String(label || filename).replace(/\]/g, ")").replace(/[\r\n]+/g, " ").trim() || filename;
|
||||
return `[${kind}=${filename},${safeLabel}]`;
|
||||
}
|
||||
|
||||
function markdownCode(url, label, mimeType) {
|
||||
return String(mimeType || "").startsWith("image/") ? `` : `[${label}](${url})`;
|
||||
}
|
||||
|
||||
export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, toast, onFilesChanged = () => {} }) {
|
||||
const dialog = document.querySelector("#files-dialog");
|
||||
const list = document.querySelector("#files-list");
|
||||
const input = document.querySelector("#file-input");
|
||||
@@ -50,12 +60,14 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="action-button action-button--secondary compact-button" data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="alias" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Alias</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>
|
||||
<button class="action-button action-button--primary compact-button" data-add-file-to-note data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Add to note</button>
|
||||
${canDelete() ? `<button class="action-button action-button--danger compact-button" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>` : ""}
|
||||
</div>
|
||||
<div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button class="action-button action-button--primary compact-button" data-copy-generated>Copy</button></div>
|
||||
</div>`).join("") : '<p class="dialog-copy">No files uploaded.</p>';
|
||||
onFilesChanged(files);
|
||||
if (open && !dialog.open) dialog.showModal();
|
||||
} catch (error) {
|
||||
if (open) toast(error.message);
|
||||
@@ -95,8 +107,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
});
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
const fileUrl = safeAppUrl(result.url);
|
||||
const text = file.type.startsWith("image/") ? `` : `[${file.name}](${fileUrl})`;
|
||||
const text = aliasCode(result.name, file.name, result.mime_type || file.type);
|
||||
editor.setRangeText(text, editor.selectionStart, editor.selectionEnd, "end");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
uploadToast.success();
|
||||
@@ -117,8 +128,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
list.addEventListener("click", async event => {
|
||||
const addButton = event.target.closest("[data-add-file-to-note]");
|
||||
if (addButton) {
|
||||
const relative = safeAppUrl(addButton.dataset.url);
|
||||
const text = addButton.dataset.mime?.startsWith("image/") ? `` : `[${addButton.dataset.name}](${relative})`;
|
||||
const text = aliasCode(addButton.dataset.name, addButton.dataset.name, addButton.dataset.mime);
|
||||
editor.setRangeText(text, editor.selectionStart, editor.selectionEnd, "end");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
toast("Added to note");
|
||||
@@ -130,9 +140,11 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
const output = panel.querySelector("textarea");
|
||||
const absolute = new URL(safeAppUrl(showButton.dataset.url), location.origin).href;
|
||||
let text = absolute;
|
||||
if (showButton.dataset.showFileCode === "markdown") {
|
||||
if (showButton.dataset.showFileCode === "alias") {
|
||||
text = aliasCode(showButton.dataset.name, showButton.dataset.name, showButton.dataset.mime);
|
||||
} else if (showButton.dataset.showFileCode === "markdown") {
|
||||
const relative = safeAppUrl(showButton.dataset.url);
|
||||
text = showButton.dataset.mime?.startsWith("image/") ? `` : `[${showButton.dataset.name}](${relative})`;
|
||||
text = markdownCode(relative, showButton.dataset.name, showButton.dataset.mime);
|
||||
}
|
||||
output.value = text; panel.hidden = false; output.focus(); output.select(); return;
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ installGlobalDiagnostics();
|
||||
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
|
||||
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown";
|
||||
import { toast } from "@rustpad/toast";
|
||||
|
||||
const token = location.pathname.split("/").filter(Boolean)[1];
|
||||
@@ -39,7 +39,7 @@ function scrollToPublicAnchor(hash, behavior = "auto") {
|
||||
target.scrollIntoView({ behavior, block: "start" });
|
||||
return true;
|
||||
}
|
||||
async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; content.innerHTML = renderMarkdown(page.content); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } }
|
||||
async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } }
|
||||
content.addEventListener("click", event => {
|
||||
const link = event.target.closest('.markdown-toc a[href^="#"]');
|
||||
if (!link) return;
|
||||
|
||||
Reference in New Issue
Block a user