public pages links
This commit is contained in:
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.49"
|
version = "0.2.50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.49"
|
version = "0.2.50"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
+98
-3
@@ -26,6 +26,7 @@ const fullWidthStorageKey = "rustpad:public-full-width";
|
|||||||
const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error");
|
const passwordDialog = document.querySelector("#public-password-dialog"), passwordForm = document.querySelector("#public-password-form"), passwordInput = document.querySelector("#public-password"), passwordError = document.querySelector("#public-password-error");
|
||||||
let pagePassword = "";
|
let pagePassword = "";
|
||||||
let mermaidRenderVersion = 0;
|
let mermaidRenderVersion = 0;
|
||||||
|
let publicAnchorSettleCleanup = null;
|
||||||
function pageHeaders() { return pagePassword ? { "X-RustPad-Page-Password": pagePassword } : {}; }
|
function pageHeaders() { return pagePassword ? { "X-RustPad-Page-Password": pagePassword } : {}; }
|
||||||
async function renderMermaid() {
|
async function renderMermaid() {
|
||||||
const renderVersion = ++mermaidRenderVersion;
|
const renderVersion = ++mermaidRenderVersion;
|
||||||
@@ -75,9 +76,74 @@ function scrollToPublicAnchor(hash, behavior = "auto") {
|
|||||||
target.scrollIntoView({ behavior, block: "start" });
|
target.scrollIntoView({ behavior, block: "start" });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
function settleInitialPublicAnchor(hash) {
|
||||||
|
publicAnchorSettleCleanup?.();
|
||||||
|
publicAnchorSettleCleanup = null;
|
||||||
|
if (!hash) return;
|
||||||
|
|
||||||
|
let stopped = false;
|
||||||
|
let frame = 0;
|
||||||
|
let observer = null;
|
||||||
|
const cleanups = [];
|
||||||
|
const cleanup = () => {
|
||||||
|
if (stopped) return;
|
||||||
|
stopped = true;
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
observer?.disconnect();
|
||||||
|
cleanups.splice(0).forEach(remove => remove());
|
||||||
|
if (publicAnchorSettleCleanup === cleanup) publicAnchorSettleCleanup = null;
|
||||||
|
};
|
||||||
|
const correct = () => {
|
||||||
|
frame = 0;
|
||||||
|
if (stopped || location.hash !== hash) return;
|
||||||
|
scrollToPublicAnchor(hash);
|
||||||
|
};
|
||||||
|
const scheduleCorrection = () => {
|
||||||
|
if (stopped || frame) return;
|
||||||
|
frame = requestAnimationFrame(correct);
|
||||||
|
};
|
||||||
|
const stopOnUserInput = () => cleanup();
|
||||||
|
|
||||||
|
["wheel", "touchstart", "pointerdown", "keydown"].forEach(type => {
|
||||||
|
window.addEventListener(type, stopOnUserInput, { capture: true, passive: true });
|
||||||
|
cleanups.push(() => window.removeEventListener(type, stopOnUserInput, true));
|
||||||
|
});
|
||||||
|
|
||||||
|
content.querySelectorAll("img, video, iframe").forEach(asset => {
|
||||||
|
["load", "error", "loadedmetadata"].forEach(type => {
|
||||||
|
asset.addEventListener(type, scheduleCorrection, { once: true });
|
||||||
|
cleanups.push(() => asset.removeEventListener(type, scheduleCorrection));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (document.readyState !== "complete") {
|
||||||
|
window.addEventListener("load", scheduleCorrection, { once: true });
|
||||||
|
cleanups.push(() => window.removeEventListener("load", scheduleCorrection));
|
||||||
|
}
|
||||||
|
document.fonts?.ready?.then(scheduleCorrection).catch(() => {});
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === "function") {
|
||||||
|
observer = new ResizeObserver(scheduleCorrection);
|
||||||
|
observer.observe(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
publicAnchorSettleCleanup = cleanup;
|
||||||
|
scheduleCorrection();
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(scheduleCorrection));
|
||||||
|
const timeout = window.setTimeout(cleanup, 6000);
|
||||||
|
cleanups.push(() => clearTimeout(timeout));
|
||||||
|
}
|
||||||
|
function isBlankPublicSourceLine(target) {
|
||||||
|
if (!target.matches("div.preview-source-line")) return false;
|
||||||
|
return ![...target.childNodes].some(node => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) return Boolean(node.textContent?.trim());
|
||||||
|
if (!(node instanceof Element)) return false;
|
||||||
|
return !node.matches("br, .public-permalink");
|
||||||
|
});
|
||||||
|
}
|
||||||
function installPublicPermalinks() {
|
function installPublicPermalinks() {
|
||||||
content.querySelectorAll(".preview-source-line[data-source-line]").forEach(target => {
|
content.querySelectorAll(".preview-source-line[data-source-line]").forEach(target => {
|
||||||
if (target.matches("hr") || target.querySelector(":scope > .public-permalink")) return;
|
if (target.matches("hr") || isBlankPublicSourceLine(target) || target.querySelector(":scope > .public-permalink")) return;
|
||||||
const line = Number(target.dataset.sourceLine);
|
const line = Number(target.dataset.sourceLine);
|
||||||
if (!Number.isSafeInteger(line) || line < 1) return;
|
if (!Number.isSafeInteger(line) || line < 1) return;
|
||||||
const heading = target.matches("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]");
|
const heading = target.matches("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]");
|
||||||
@@ -106,7 +172,36 @@ function restoreFullWidth() {
|
|||||||
try { enabled = localStorage.getItem(fullWidthStorageKey) === "1"; } catch { }
|
try { enabled = localStorage.getItem(fullWidthStorageKey) === "1"; } catch { }
|
||||||
setFullWidth(enabled, { persist: false });
|
setFullWidth(enabled, { persist: false });
|
||||||
}
|
}
|
||||||
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); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); installPublicPermalinks(); alignPreviewLineNumbers(content); 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);
|
||||||
|
lockPublicContent(page.allow_task_updates);
|
||||||
|
await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]);
|
||||||
|
installPublicPermalinks();
|
||||||
|
alignPreviewLineNumbers(content);
|
||||||
|
settleInitialPublicAnchor(location.hash);
|
||||||
|
} catch (error) {
|
||||||
|
publicAnchorSettleCleanup?.();
|
||||||
|
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", async event => {
|
content.addEventListener("click", async event => {
|
||||||
const link = event.target.closest('a[href^="#"]');
|
const link = event.target.closest('a[href^="#"]');
|
||||||
if (!link || !content.contains(link)) return;
|
if (!link || !content.contains(link)) return;
|
||||||
@@ -123,7 +218,7 @@ content.addEventListener("click", async event => {
|
|||||||
toast(link.dataset.linkKind === "heading" ? "Heading link copied" : `Link to line ${link.dataset.line} copied`);
|
toast(link.dataset.linkKind === "heading" ? "Heading link copied" : `Link to line ${link.dataset.line} copied`);
|
||||||
} catch (error) { toast(error.message); }
|
} catch (error) { toast(error.message); }
|
||||||
});
|
});
|
||||||
window.addEventListener("hashchange", () => scrollToPublicAnchor(location.hash, "smooth"));
|
window.addEventListener("hashchange", () => { publicAnchorSettleCleanup?.(); scrollToPublicAnchor(location.hash, "smooth"); });
|
||||||
content.addEventListener("change", async event => { const box = event.target.closest(".task-checkbox"); if (!box || box.disabled) return; const previous = !box.checked; box.disabled = true; try { const page = await api(`/api/public/${encodeURIComponent(token)}/tasks`, { method: "POST", headers: pageHeaders(), body: JSON.stringify({ source_line: Number(box.dataset.sourceLine), checked: box.checked }) }); document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`; toast("Task saved"); } catch (error) { box.checked = previous; toast(error.message); } finally { box.disabled = false; } });
|
content.addEventListener("change", async event => { const box = event.target.closest(".task-checkbox"); if (!box || box.disabled) return; const previous = !box.checked; box.disabled = true; try { const page = await api(`/api/public/${encodeURIComponent(token)}/tasks`, { method: "POST", headers: pageHeaders(), body: JSON.stringify({ source_line: Number(box.dataset.sourceLine), checked: box.checked }) }); document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`; toast("Task saved"); } catch (error) { box.checked = previous; toast(error.message); } finally { box.disabled = false; } });
|
||||||
lineNumbersToggle.addEventListener("change", () => { document.body.classList.toggle("hide-preview-line-numbers", !lineNumbersToggle.checked); });
|
lineNumbersToggle.addEventListener("change", () => { document.body.classList.toggle("hide-preview-line-numbers", !lineNumbersToggle.checked); });
|
||||||
fullWidthToggle.addEventListener("change", () => setFullWidth(fullWidthToggle.checked));
|
fullWidthToggle.addEventListener("change", () => setFullWidth(fullWidthToggle.checked));
|
||||||
|
|||||||
Reference in New Issue
Block a user