work
This commit is contained in:
+6
-10
@@ -2,18 +2,14 @@ export async function api(path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 12000);
|
||||
try {
|
||||
const response = await fetch(path, {
|
||||
...options,
|
||||
headers: { "content-type": "application/json", ...(options.headers || {}) },
|
||||
signal: controller.signal,
|
||||
});
|
||||
const headers = new Headers(options.headers || {});
|
||||
if (!(options.body instanceof FormData) && !headers.has("content-type")) headers.set("content-type", "application/json");
|
||||
const response = await fetch(path, { ...options, headers, signal: controller.signal });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || `Błąd ${response.status}`);
|
||||
if (!response.ok) throw new Error(data.error || `Error ${response.status}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") throw new Error("Przekroczono czas odpowiedzi serwera");
|
||||
if (error.name === "AbortError") throw new Error("Timed out");
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
} finally { clearTimeout(timeout); }
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ export async function copyText(text) {
|
||||
input.select();
|
||||
const copied = document.execCommand("copy");
|
||||
input.remove();
|
||||
if (!copied) throw new Error("Nie udało się skopiować linku");
|
||||
if (!copied) throw new Error("Failed to copy the link");
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function applyFormat(editor, format) {
|
||||
if (format === "bullet") prefix("- ");
|
||||
if (format === "number") prefix((index) => `${index + 1}. `);
|
||||
if (format === "quote") prefix("> ");
|
||||
if (format === "link") wrap("[", "](https://)", "opis linku");
|
||||
if (format === "link") wrap("[", "](https://)", "description");
|
||||
editor.focus();
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import { api } from "./api.js?v=0.6.0";
|
||||
import { api } from "@rustpad/api";
|
||||
|
||||
function slugify(value, fallback) {
|
||||
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback;
|
||||
@@ -21,7 +21,7 @@ function setBusy(button, busy, idleText, busyText) {
|
||||
button.textContent = busy ? busyText : idleText;
|
||||
}
|
||||
|
||||
bindPreview("#pad-name", "#pad-slug-preview", "#pad-name-count", "/p/", "notatka");
|
||||
bindPreview("#pad-name", "#pad-slug-preview", "#pad-name-count", "/p/", "note");
|
||||
bindPreview("#workspace-name", "#workspace-slug-preview", "#workspace-name-count", "/w/", "workspace");
|
||||
|
||||
document.querySelectorAll(".password-toggle").forEach((button) => {
|
||||
@@ -29,7 +29,7 @@ document.querySelectorAll(".password-toggle").forEach((button) => {
|
||||
const input = document.getElementById(button.dataset.target);
|
||||
const show = input.type === "password";
|
||||
input.type = show ? "text" : "password";
|
||||
button.textContent = show ? "Ukryj" : "Pokaż";
|
||||
button.textContent = show ? "Hide" : "Show";
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ document.querySelector("#pad-form").addEventListener("submit", async (event) =>
|
||||
const button = document.querySelector("#pad-button");
|
||||
const error = document.querySelector("#pad-error");
|
||||
error.textContent = "";
|
||||
setBusy(button, true, "Utwórz notatkę", "Tworzenie…");
|
||||
setBusy(button, true, "Create note", "Creating…");
|
||||
try {
|
||||
const payload = { name: name.value.trim() };
|
||||
if (password.value) payload.password = password.value;
|
||||
@@ -50,7 +50,7 @@ document.querySelector("#pad-form").addEventListener("submit", async (event) =>
|
||||
} catch (requestError) {
|
||||
error.textContent = requestError.message;
|
||||
} finally {
|
||||
setBusy(button, false, "Utwórz notatkę", "Tworzenie…");
|
||||
setBusy(button, false, "Create note", "Creating…");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ document.querySelector("#workspace-form").addEventListener("submit", async (even
|
||||
const button = document.querySelector("#workspace-button");
|
||||
const error = document.querySelector("#workspace-error");
|
||||
error.textContent = "";
|
||||
setBusy(button, true, "Utwórz workspace", "Tworzenie…");
|
||||
setBusy(button, true, "Create workspace", "Creating…");
|
||||
try {
|
||||
const payload = { name: name.value.trim() };
|
||||
if (password.value) payload.password = password.value;
|
||||
@@ -71,6 +71,6 @@ document.querySelector("#workspace-form").addEventListener("submit", async (even
|
||||
} catch (requestError) {
|
||||
error.textContent = requestError.message;
|
||||
} finally {
|
||||
setBusy(button, false, "Utwórz workspace", "Tworzenie…");
|
||||
setBusy(button, false, "Create workspace", "Creating…");
|
||||
}
|
||||
});
|
||||
|
||||
+36
-11
@@ -1,26 +1,51 @@
|
||||
function escapeHtml(value) {
|
||||
return value.replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
||||
return String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
||||
}
|
||||
|
||||
function safeUrl(value) {
|
||||
const url = String(value).trim();
|
||||
if (/^(https?:\/\/|\/|\.\/|\.\.\/|#)/i.test(url)) return escapeHtml(url);
|
||||
return "#";
|
||||
}
|
||||
|
||||
function inline(value) {
|
||||
return escapeHtml(value)
|
||||
const tokens = [];
|
||||
let html = escapeHtml(value);
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => {
|
||||
const token = `\u0000IMG${tokens.length}\u0000`;
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
tokens.push(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async"${titleAttr}>`);
|
||||
return token;
|
||||
});
|
||||
html = html
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
||||
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => {
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
return `<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer"${titleAttr}>${label}</a>`;
|
||||
});
|
||||
return html.replace(/\u0000IMG(\d+)\u0000/g, (_, index) => tokens[Number(index)] || "");
|
||||
}
|
||||
|
||||
export function renderMarkdown(source) {
|
||||
let html = "", inCode = false, list = null;
|
||||
let html = "", inCode = false, language = "", code = [], list = null;
|
||||
const closeList = () => { if (list) { html += `</${list}>`; list = null; } };
|
||||
for (const line of source.split("\n")) {
|
||||
if (line.startsWith("```")) { closeList(); html += inCode ? "</code></pre>" : "<pre><code>"; inCode = !inCode; continue; }
|
||||
if (inCode) { html += `${escapeHtml(line)}\n`; continue; }
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
const ul = line.match(/^\s*[-*+]\s+(.+)$/);
|
||||
const ol = line.match(/^\s*\d+\.\s+(.+)$/);
|
||||
const closeCode = () => {
|
||||
const body = escapeHtml(code.join("\n"));
|
||||
html += language.toLowerCase() === "mermaid"
|
||||
? `<div class="mermaid">${body}</div>`
|
||||
: `<pre><code class="language-${escapeHtml(language)}">${body}</code></pre>`;
|
||||
code = []; language = "";
|
||||
};
|
||||
for (const line of String(source).split("\n")) {
|
||||
if (line.startsWith("```")) { closeList(); if (inCode) closeCode(); else language = line.slice(3).trim(); inCode = !inCode; continue; }
|
||||
if (inCode) { code.push(line); continue; }
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/), ul = line.match(/^\s*[-*+]\s+(.+)$/), ol = line.match(/^\s*\d+\.\s+(.+)$/);
|
||||
if (heading) { closeList(); const n = heading[1].length; html += `<h${n}>${inline(heading[2])}</h${n}>`; }
|
||||
else if (ul || ol) { const type = ul ? "ul" : "ol"; if (list !== type) { closeList(); html += `<${type}>`; list = type; } html += `<li>${inline((ul || ol)[1])}</li>`; }
|
||||
else { closeList(); if (/^---+$/.test(line)) html += "<hr>"; else if (line.startsWith("> ")) html += `<blockquote>${inline(line.slice(2))}</blockquote>`; else if (line.trim()) html += `<p>${inline(line)}</p>`; else html += "<br>"; }
|
||||
}
|
||||
closeList(); if (inCode) html += "</code></pre>"; return html;
|
||||
closeList(); if (inCode) closeCode(); return html;
|
||||
}
|
||||
|
||||
+34
-51
@@ -1,53 +1,36 @@
|
||||
import { api } from "./api.js?v=0.6.0";
|
||||
import { copyText } from "./clipboard.js?v=0.6.0";
|
||||
import { applyFormat } from "./editor-format.js?v=0.6.0";
|
||||
import { renderMarkdown } from "./markdown.js?v=0.6.0";
|
||||
import { getPassword, setPassword } from "./session.js?v=0.6.0";
|
||||
import { NoteSocket } from "./socket.js?v=0.6.0";
|
||||
import { currentShareUrl, readEditorState, writeEditorState } from "./url-state.js?v=0.6.0";
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { applyFormat } from "@rustpad/editor-format";
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
import { getNickname, getPassword, setNickname, setPassword } from "@rustpad/session";
|
||||
import { NoteSocket } from "@rustpad/socket";
|
||||
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
|
||||
|
||||
const parts = location.pathname.split("/").filter(Boolean);
|
||||
const workspaceSlug = parts[1], noteSlug = parts[3];
|
||||
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace");
|
||||
const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog");
|
||||
let password = getPassword(workspaceSlug), info, socket, saveTimer, applyingRemote = false, uiState = readEditorState();
|
||||
|
||||
function toast(text) { const el = document.querySelector("#toast"); el.textContent = text; el.classList.add("visible"); setTimeout(() => el.classList.remove("visible"), 1600); }
|
||||
function setStatus(kind, text) { document.querySelector("#status-dot").className = `status__dot${kind ? ` is-${kind}` : ""}`; document.querySelector("#status-text").textContent = text; }
|
||||
function updateAddressLabel() { document.querySelector("#note-url").textContent = `${location.pathname}${location.search}`; }
|
||||
function render() {
|
||||
if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); document.querySelector("#preview-label").textContent = "Podgląd Markdown"; }
|
||||
else { preview.classList.add("preview--raw"); preview.textContent = editor.value; document.querySelector("#preview-label").textContent = "Tekst źródłowy"; }
|
||||
document.querySelector("#characters").textContent = `${editor.value.length} znaków`;
|
||||
const words = editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0; document.querySelector("#words").textContent = `${words} słów`;
|
||||
}
|
||||
function applyUi({ write = false, replace = false } = {}) {
|
||||
editorWorkspace.className = `workspace view-${uiState.view}`;
|
||||
document.querySelectorAll("[data-view]").forEach(button => { const active = button.dataset.view === uiState.view; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); });
|
||||
const markdown = uiState.mode === "markdown"; modeToggle.classList.toggle("active", markdown); modeToggle.setAttribute("aria-pressed", String(markdown)); modeToggle.textContent = markdown ? "Markdown" : "Tekst"; modeToggle.title = markdown ? "Kliknij, aby pokazać tekst bez interpretacji" : "Kliknij, aby interpretować Markdown";
|
||||
render(); if (write) writeEditorState(uiState, { replace }); updateAddressLabel();
|
||||
}
|
||||
function applyRemote(content) { if (content === editor.value) return; const start = editor.selectionStart, end = editor.selectionEnd; applyingRemote = true; editor.value = content; editor.setSelectionRange(Math.min(start, content.length), Math.min(end, content.length)); applyingRemote = false; render(); }
|
||||
function connect() {
|
||||
socket?.stop(); socket = new NoteSocket({ workspaceSlug, noteSlug, password,
|
||||
onStatus: state => setStatus(state === "online" ? "online" : state === "offline" ? "offline" : null, state === "online" ? "Połączono" : state === "offline" ? "Ponowne łączenie…" : "Łączenie…"),
|
||||
onAuthenticated: message => { if (passwordDialog.open) passwordDialog.close(); document.querySelector("#note-title").textContent = message.note_title; document.querySelector("#workspace-link").textContent = message.workspace_title; applyRemote(message.content); editor.focus(); },
|
||||
onDocument: message => { applyRemote(message.content); document.querySelector("#save-state").textContent = `Zapisano ${new Date(message.updated_at).toLocaleTimeString("pl-PL", { hour: "2-digit", minute: "2-digit" })}`; },
|
||||
onError: message => { document.querySelector("#password-error").textContent = message; if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); }
|
||||
}); socket.connect();
|
||||
}
|
||||
async function initialize() {
|
||||
try { info = await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`); document.querySelector("#note-title").textContent = info.title; document.querySelector("#workspace-link").textContent = info.workspace_title; document.querySelector("#workspace-link").href = `/w/${encodeURIComponent(workspaceSlug)}`; document.querySelector("#back-workspace").href = `/w/${encodeURIComponent(workspaceSlug)}`; document.title = `${info.title} · ${info.workspace_title}`; applyUi({ write: true, replace: true }); if (info.protected && !password) passwordDialog.showModal(); else connect(); } catch (e) { document.body.innerHTML = `<main class="error-page"><div><h1>Nie znaleziono notatki</h1><p>${e.message}</p><a href="/w/${encodeURIComponent(workspaceSlug)}">Wróć do workspace</a></div></main>`; }
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-view]").forEach(button => button.addEventListener("click", () => { uiState = { ...uiState, view: button.dataset.view }; applyUi({ write: true }); }));
|
||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); });
|
||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); });
|
||||
window.addEventListener("rustpad:urlchange", updateAddressLabel);
|
||||
document.querySelector("#copy-link").addEventListener("click", async () => { try { const url = currentShareUrl(uiState); await copyText(url); toast("Skopiowano link z widokiem"); } catch (e) { toast(e.message); } });
|
||||
document.querySelectorAll("[data-format]").forEach(button => button.addEventListener("click", () => applyFormat(editor, button.dataset.format)));
|
||||
editor.addEventListener("input", () => { render(); if (applyingRemote) return; clearTimeout(saveTimer); document.querySelector("#save-state").textContent = "Zapisywanie…"; saveTimer = setTimeout(() => socket?.update(editor.value), 250); });
|
||||
document.querySelector("#password-form").addEventListener("submit", event => { event.preventDefault(); password = document.querySelector("#open-password").value; setPassword(workspaceSlug, password); document.querySelector("#password-error").textContent = ""; connect(); });
|
||||
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Ładowanie…</p>'; try { const revisions = await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/history`, { method: "POST", body: JSON.stringify({ password: password || null }) }); list.innerHTML = revisions.length ? revisions.map(r => `<article class="revision"><time>${new Date(r.created_at.replace(" ", "T") + "Z").toLocaleString("pl-PL")}</time><button class="secondary-button" data-revision="${r.id}">Przywróć</button></article>`).join("") : '<p class="empty">Brak historii.</p>'; list.querySelectorAll("[data-revision]").forEach(button => button.addEventListener("click", async () => { await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/restore`, { method: "POST", body: JSON.stringify({ password: password || null, revision_id: Number(button.dataset.revision) }) }); toast("Przywrócono wersję"); })); } catch (e) { list.innerHTML = `<p class="error">${e.message}</p>`; } });
|
||||
document.querySelector("#close-history").addEventListener("click", () => { historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
|
||||
const parts=location.pathname.split("/").filter(Boolean), workspaceSlug=parts[1], noteSlug=parts[3];
|
||||
const editor=document.querySelector("#editor"), preview=document.querySelector("#preview"), editorWorkspace=document.querySelector("#editor-workspace"), gutter=document.querySelector("#line-gutter");
|
||||
const modeToggle=document.querySelector("#mode-toggle"), passwordDialog=document.querySelector("#password-dialog"), identityDialog=document.querySelector("#identity-dialog");
|
||||
let password=getPassword(workspaceSlug), nickname=getNickname(), info, socket, saveTimer, applyingRemote=false, uiState=readEditorState(), owners=[];
|
||||
const lineToggle=document.querySelector("#line-numbers-toggle"); lineToggle.checked=localStorage.getItem("rustpad:line-numbers")!=="off";
|
||||
function colorFor(name){let h=0;for(const c of name||"?")h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 70% 62%)`;}
|
||||
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
|
||||
function setStatus(kind,text){document.querySelector("#status-dot").className=`status__dot${kind?` is-${kind}`:""}`;document.querySelector("#status-text").textContent=text;}
|
||||
function updateAddressLabel(){document.querySelector("#note-url").textContent=`${location.pathname}${location.search}`;}
|
||||
async function renderMermaid(){const nodes=preview.querySelectorAll(".mermaid");if(!nodes.length)return;try{const {default:mermaid}=await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs");mermaid.initialize({startOnLoad:false,theme:"dark",securityLevel:"strict"});await mermaid.run({nodes:[...nodes]});}catch{nodes.forEach(n=>n.insertAdjacentHTML("beforebegin",'<p class="error">Failed to load Mermaid.</p>'));}}
|
||||
function renderGutter(){const lines=editor.value.split("\n");owners=owners.slice(0,lines.length);while(owners.length<lines.length)owners.push(owners.at(-1)||nickname||"");gutter.innerHTML=lines.map((_,i)=>`<div title="${escapeHtml(owners[i]||"no author")}" style="--owner:${colorFor(owners[i])}">${i+1}</div>`).join("");document.body.classList.toggle("hide-line-numbers",!lineToggle.checked);}
|
||||
function escapeHtml(v){return String(v).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));}
|
||||
function render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview";renderMermaid();}else{preview.classList.add("preview--raw");preview.textContent=editor.value;document.querySelector("#preview-label").textContent="Source text";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
|
||||
function applyUi({write=false,replace=false}={}){editorWorkspace.className=`workspace view-${uiState.view}`;document.querySelectorAll("[data-view]").forEach(b=>{const a=b.dataset.view===uiState.view;b.classList.toggle("active",a);b.setAttribute("aria-pressed",String(a));});const markdown=uiState.mode==="markdown";modeToggle.classList.toggle("active",markdown);modeToggle.textContent=markdown?"Markdown":"Text";render();if(write)writeEditorState(uiState,{replace});updateAddressLabel();}
|
||||
function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null)return;const start=editor.selectionStart,end=editor.selectionEnd;applyingRemote=true;editor.value=content;try{owners=JSON.parse(ownerMap||"[]");}catch{owners=[];}editor.setSelectionRange(Math.min(start,content.length),Math.min(end,content.length));applyingRemote=false;render();}
|
||||
function connect(){socket?.stop();socket=new NoteSocket({workspaceSlug,noteSlug,password,nickname,onStatus:s=>setStatus(s==="online"?"online":s==="offline"?"offline":null,s==="online"?"Connected":s==="offline"?"Reconnecting…":"Connecting…"),onAuthenticated:m=>{if(passwordDialog.open)passwordDialog.close();document.querySelector("#note-title").textContent=m.note_title;document.querySelector("#workspace-link").textContent=m.workspace_title;applyRemote(m.content,m.owner_map);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"})}`;},onError:m=>{document.querySelector("#password-error").textContent=m;if(info?.protected&&!passwordDialog.open)passwordDialog.showModal();}});socket.connect();}
|
||||
async function initialize(){try{info=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`);document.querySelector("#note-title").textContent=info.title;document.querySelector("#workspace-link").textContent=info.workspace_title;document.querySelector("#workspace-link").href=`/w/${encodeURIComponent(workspaceSlug)}`;document.querySelector("#back-workspace").href=`/w/${encodeURIComponent(workspaceSlug)}`;document.title=`${info.title} · ${info.workspace_title}`;applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else connect();}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
|
||||
document.querySelector("#identity-form").addEventListener("submit",e=>{e.preventDefault();nickname=document.querySelector("#nickname").value.trim();setNickname(nickname);identityDialog.close();document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else connect();});
|
||||
document.querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>{uiState={...uiState,view:b.dataset.view};applyUi({write:true});}));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();});
|
||||
window.addEventListener("popstate",()=>{uiState=readEditorState();applyUi();});window.addEventListener("rustpad:urlchange",updateAddressLabel);document.querySelector("#copy-link").addEventListener("click",async()=>{try{await copyText(currentShareUrl(uiState));toast("Link copied");}catch(e){toast(e.message);}});document.querySelectorAll("[data-format]").forEach(b=>b.addEventListener("click",()=>applyFormat(editor,b.dataset.format)));
|
||||
document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null})});const url=new URL(result.url,location.origin).href;await copyText(url);toast("Page link copied");window.open(url,"_blank","noopener");}catch(error){toast(error.message);}});
|
||||
editor.addEventListener("scroll",()=>{gutter.scrollTop=editor.scrollTop;});editor.addEventListener("input",()=>{const newLines=editor.value.split("\n").length;const cursorLine=editor.value.slice(0,editor.selectionStart).split("\n").length-1;while(owners.length<newLines)owners.push(nickname);owners=owners.slice(0,newLines);owners[cursorLine]=nickname;render();if(applyingRemote)return;clearTimeout(saveTimer);document.querySelector("#save-state").textContent="Saving…";saveTimer=setTimeout(()=>socket?.update(editor.value,JSON.stringify(owners)),250);});
|
||||
document.querySelector("#password-form").addEventListener("submit",e=>{e.preventDefault();password=document.querySelector("#open-password").value;setPassword(workspaceSlug,password);document.querySelector("#password-error").textContent="";connect();});
|
||||
const historyPanel=document.querySelector("#history-panel");document.querySelector("#history-button").addEventListener("click",async()=>{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 api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/history`,{method:"POST",body:JSON.stringify({password:password||null})});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>${new Date(r.created_at.replace(" ","T")+"Z").toLocaleString("en-US")}</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 api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/restore`,{method:"POST",body:JSON.stringify({password:password||null,revision_id:r.id})});toast("Version restored");});}}catch(e){list.innerHTML=`<p class="error">${escapeHtml(e.message)}</p>`;}});document.querySelector("#close-history").addEventListener("click",()=>{historyPanel.classList.remove("open");historyPanel.setAttribute("aria-hidden","true");document.body.classList.remove("history-open");});
|
||||
document.querySelector("#upload-button").addEventListener("click",()=>document.querySelector("#file-input").click());document.querySelector("#file-input").addEventListener("change",async e=>{const file=e.target.files[0];if(!file)return;const form=new FormData();form.append("password",password||"");form.append("file",file);try{const result=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/files`,{method:"POST",body:form,headers:{}});const image=file.type.startsWith("image/");const text=image?``:`[${file.name}](${result.url})`;editor.setRangeText(text,editor.selectionStart,editor.selectionEnd,"end");editor.dispatchEvent(new Event("input"));toast("File uploaded");}catch(err){toast(err.message);}e.target.value="";});
|
||||
window.addEventListener("error",event=>{setStatus("offline","Application error");console.error(event.error||event.message);});
|
||||
window.addEventListener("unhandledrejection",event=>{setStatus("offline","Application error");console.error(event.reason);});
|
||||
initialize();
|
||||
|
||||
+32
-171
@@ -1,173 +1,34 @@
|
||||
import { api } from "./api.js?v=0.6.0";
|
||||
import { copyText } from "./clipboard.js?v=0.6.0";
|
||||
import { applyFormat } from "./editor-format.js?v=0.6.0";
|
||||
import { renderMarkdown } from "./markdown.js?v=0.6.0";
|
||||
import { PadSocket } from "./socket.js?v=0.6.0";
|
||||
import { currentShareUrl, readEditorState, writeEditorState } from "./url-state.js?v=0.6.0";
|
||||
|
||||
const slug = location.pathname.split("/").filter(Boolean)[1];
|
||||
const passwordKey = `rustpad:pad:${slug}:password`;
|
||||
const editor = document.querySelector("#editor");
|
||||
const preview = document.querySelector("#preview");
|
||||
const editorWorkspace = document.querySelector("#editor-workspace");
|
||||
const modeToggle = document.querySelector("#mode-toggle");
|
||||
const passwordDialog = document.querySelector("#password-dialog");
|
||||
let password = sessionStorage.getItem(passwordKey) || "";
|
||||
let info;
|
||||
let socket;
|
||||
let saveTimer;
|
||||
let applyingRemote = false;
|
||||
let uiState = readEditorState();
|
||||
|
||||
function toast(text) {
|
||||
const element = document.querySelector("#toast");
|
||||
element.textContent = text;
|
||||
element.classList.add("visible");
|
||||
setTimeout(() => element.classList.remove("visible"), 1600);
|
||||
}
|
||||
|
||||
function setStatus(kind, text) {
|
||||
document.querySelector("#status-dot").className = `status__dot${kind ? ` is-${kind}` : ""}`;
|
||||
document.querySelector("#status-text").textContent = text;
|
||||
}
|
||||
|
||||
function updateAddressLabel() {
|
||||
document.querySelector("#pad-url").textContent = `${location.pathname}${location.search}`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (uiState.mode === "markdown") {
|
||||
preview.classList.remove("preview--raw");
|
||||
preview.innerHTML = renderMarkdown(editor.value);
|
||||
document.querySelector("#preview-label").textContent = "Podgląd Markdown";
|
||||
} else {
|
||||
preview.classList.add("preview--raw");
|
||||
preview.textContent = editor.value;
|
||||
document.querySelector("#preview-label").textContent = "Tekst źródłowy";
|
||||
}
|
||||
document.querySelector("#characters").textContent = `${editor.value.length} znaków`;
|
||||
const words = editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0;
|
||||
document.querySelector("#words").textContent = `${words} słów`;
|
||||
}
|
||||
|
||||
function applyUi({ write = false, replace = false } = {}) {
|
||||
editorWorkspace.className = `workspace view-${uiState.view}`;
|
||||
document.querySelectorAll("[data-view]").forEach((button) => {
|
||||
const active = button.dataset.view === uiState.view;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
});
|
||||
const markdown = uiState.mode === "markdown";
|
||||
modeToggle.classList.toggle("active", markdown);
|
||||
modeToggle.setAttribute("aria-pressed", String(markdown));
|
||||
modeToggle.textContent = markdown ? "Markdown" : "Tekst";
|
||||
modeToggle.title = markdown ? "Pokaż tekst bez interpretacji" : "Interpretuj Markdown";
|
||||
render();
|
||||
if (write) writeEditorState(uiState, { replace });
|
||||
updateAddressLabel();
|
||||
}
|
||||
|
||||
function applyRemote(content) {
|
||||
if (content === editor.value) return;
|
||||
const start = editor.selectionStart;
|
||||
const end = editor.selectionEnd;
|
||||
applyingRemote = true;
|
||||
editor.value = content;
|
||||
editor.setSelectionRange(Math.min(start, content.length), Math.min(end, content.length));
|
||||
applyingRemote = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
socket?.stop();
|
||||
socket = new PadSocket({
|
||||
slug,
|
||||
password,
|
||||
onStatus: (state) => setStatus(state === "online" ? "online" : state === "offline" ? "offline" : null, state === "online" ? "Połączono" : state === "offline" ? "Ponowne łączenie…" : "Łączenie…"),
|
||||
onAuthenticated: (message) => {
|
||||
if (passwordDialog.open) passwordDialog.close();
|
||||
document.querySelector("#pad-title").textContent = message.title;
|
||||
applyRemote(message.content);
|
||||
editor.focus();
|
||||
},
|
||||
onDocument: (message) => {
|
||||
applyRemote(message.content);
|
||||
document.querySelector("#save-state").textContent = `Zapisano ${new Date(message.updated_at).toLocaleTimeString("pl-PL", { hour: "2-digit", minute: "2-digit" })}`;
|
||||
},
|
||||
onError: (message) => {
|
||||
document.querySelector("#password-error").textContent = message;
|
||||
if (info?.protected && !passwordDialog.open) passwordDialog.showModal();
|
||||
},
|
||||
});
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
try {
|
||||
info = await api(`/api/pads/${encodeURIComponent(slug)}`);
|
||||
document.querySelector("#pad-title").textContent = info.title;
|
||||
document.title = `${info.title} · RustPad`;
|
||||
applyUi({ write: true, replace: true });
|
||||
if (info.protected && !password) passwordDialog.showModal();
|
||||
else connect();
|
||||
} catch (error) {
|
||||
location.replace("/");
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-view]").forEach((button) => button.addEventListener("click", () => {
|
||||
uiState = { ...uiState, view: button.dataset.view };
|
||||
applyUi({ write: true });
|
||||
}));
|
||||
modeToggle.addEventListener("click", () => {
|
||||
uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" };
|
||||
applyUi({ write: true });
|
||||
});
|
||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); });
|
||||
window.addEventListener("rustpad:urlchange", updateAddressLabel);
|
||||
document.querySelector("#copy-link").addEventListener("click", async () => {
|
||||
try {
|
||||
await copyText(currentShareUrl(uiState));
|
||||
toast("Skopiowano link z widokiem");
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
}
|
||||
});
|
||||
document.querySelectorAll("[data-format]").forEach((button) => button.addEventListener("click", () => applyFormat(editor, button.dataset.format)));
|
||||
editor.addEventListener("input", () => {
|
||||
render();
|
||||
if (applyingRemote) return;
|
||||
clearTimeout(saveTimer);
|
||||
document.querySelector("#save-state").textContent = "Zapisywanie…";
|
||||
saveTimer = setTimeout(() => socket?.update(editor.value), 250);
|
||||
});
|
||||
document.querySelector("#password-form").addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
password = document.querySelector("#open-password").value;
|
||||
sessionStorage.setItem(passwordKey, password);
|
||||
document.querySelector("#password-error").textContent = "";
|
||||
connect();
|
||||
});
|
||||
const historyPanel = document.querySelector("#history-panel");
|
||||
document.querySelector("#history-button").addEventListener("click", async () => {
|
||||
historyPanel.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("history-open");
|
||||
const list = document.querySelector("#history-list");
|
||||
list.innerHTML = '<p class="empty">Ładowanie…</p>';
|
||||
try {
|
||||
const revisions = await api(`/api/pads/${encodeURIComponent(slug)}/history`, { method: "POST", body: JSON.stringify({ password: password || null }) });
|
||||
list.innerHTML = revisions.length ? revisions.map((revision) => `<article class="revision"><time>${new Date(revision.created_at.replace(" ", "T") + "Z").toLocaleString("pl-PL")}</time><button class="secondary-button" data-revision="${revision.id}">Przywróć</button></article>`).join("") : '<p class="empty">Brak historii.</p>';
|
||||
list.querySelectorAll("[data-revision]").forEach((button) => button.addEventListener("click", async () => {
|
||||
await api(`/api/pads/${encodeURIComponent(slug)}/restore`, { method: "POST", body: JSON.stringify({ password: password || null, revision_id: Number(button.dataset.revision) }) });
|
||||
toast("Przywrócono wersję");
|
||||
}));
|
||||
} catch (error) {
|
||||
list.innerHTML = `<p class="error">${error.message}</p>`;
|
||||
}
|
||||
});
|
||||
document.querySelector("#close-history").addEventListener("click", () => {
|
||||
historyPanel.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("history-open");
|
||||
});
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { applyFormat } from "@rustpad/editor-format";
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
import { getNickname, setNickname } from "@rustpad/session";
|
||||
import { PadSocket } from "@rustpad/socket";
|
||||
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
|
||||
|
||||
const slug=location.pathname.split("/").filter(Boolean)[1];
|
||||
const editor=document.querySelector("#editor"), preview=document.querySelector("#preview"), editorWorkspace=document.querySelector("#editor-workspace"), gutter=document.querySelector("#line-gutter");
|
||||
const modeToggle=document.querySelector("#mode-toggle"), passwordDialog=document.querySelector("#password-dialog"), identityDialog=document.querySelector("#identity-dialog");
|
||||
let password=sessionStorage.getItem(`rustpad:pad:${slug}:password`)||"", nickname=getNickname(), info, socket, saveTimer, applyingRemote=false, uiState=readEditorState(), owners=[];
|
||||
const lineToggle=document.querySelector("#line-numbers-toggle"); lineToggle.checked=localStorage.getItem("rustpad:line-numbers")!=="off";
|
||||
function colorFor(name){let h=0;for(const c of name||"?")h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 70% 62%)`;}
|
||||
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
|
||||
function setStatus(kind,text){document.querySelector("#status-dot").className=`status__dot${kind?` is-${kind}`:""}`;document.querySelector("#status-text").textContent=text;}
|
||||
function updateAddressLabel(){document.querySelector("#pad-url").textContent=`${location.pathname}${location.search}`;}
|
||||
async function renderMermaid(){const nodes=preview.querySelectorAll(".mermaid");if(!nodes.length)return;try{const {default:mermaid}=await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs");mermaid.initialize({startOnLoad:false,theme:"dark",securityLevel:"strict"});await mermaid.run({nodes:[...nodes]});}catch{nodes.forEach(n=>n.insertAdjacentHTML("beforebegin",'<p class="error">Failed to load Mermaid.</p>'));}}
|
||||
function renderGutter(){const lines=editor.value.split("\n");owners=owners.slice(0,lines.length);while(owners.length<lines.length)owners.push(owners.at(-1)||nickname||"");gutter.innerHTML=lines.map((_,i)=>`<div title="${escapeHtml(owners[i]||"no author")}" style="--owner:${colorFor(owners[i])}">${i+1}</div>`).join("");document.body.classList.toggle("hide-line-numbers",!lineToggle.checked);}
|
||||
function escapeHtml(v){return String(v).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));}
|
||||
function render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview";renderMermaid();}else{preview.classList.add("preview--raw");preview.textContent=editor.value;document.querySelector("#preview-label").textContent="Source text";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
|
||||
function applyUi({write=false,replace=false}={}){editorWorkspace.className=`workspace view-${uiState.view}`;document.querySelectorAll("[data-view]").forEach(b=>{const a=b.dataset.view===uiState.view;b.classList.toggle("active",a);b.setAttribute("aria-pressed",String(a));});const markdown=uiState.mode==="markdown";modeToggle.classList.toggle("active",markdown);modeToggle.textContent=markdown?"Markdown":"Text";render();if(write)writeEditorState(uiState,{replace});updateAddressLabel();}
|
||||
function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null)return;const start=editor.selectionStart,end=editor.selectionEnd;applyingRemote=true;editor.value=content;try{owners=JSON.parse(ownerMap||"[]");}catch{owners=[];}editor.setSelectionRange(Math.min(start,content.length),Math.min(end,content.length));applyingRemote=false;render();}
|
||||
function connect(){socket?.stop();socket=new PadSocket({slug,password,nickname,onStatus:s=>setStatus(s==="online"?"online":s==="offline"?"offline":null,s==="online"?"Connected":s==="offline"?"Reconnecting…":"Connecting…"),onAuthenticated:m=>{if(passwordDialog.open)passwordDialog.close();document.querySelector("#pad-title").textContent=m.title;applyRemote(m.content,m.owner_map);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"})}`;},onError:m=>{document.querySelector("#password-error").textContent=m;if(info?.protected&&!passwordDialog.open)passwordDialog.showModal();}});socket.connect();}
|
||||
async function initialize(){try{info=await api(`/api/pads/${encodeURIComponent(slug)}`);document.querySelector("#pad-title").textContent=info.title;document.title=`${info.title} · RustPad`;applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else connect();}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
|
||||
document.querySelector("#identity-form").addEventListener("submit",e=>{e.preventDefault();nickname=document.querySelector("#nickname").value.trim();setNickname(nickname);identityDialog.close();document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else connect();});
|
||||
document.querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>{uiState={...uiState,view:b.dataset.view};applyUi({write:true});}));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();});
|
||||
window.addEventListener("popstate",()=>{uiState=readEditorState();applyUi();});window.addEventListener("rustpad:urlchange",updateAddressLabel);document.querySelector("#copy-link").addEventListener("click",async()=>{try{await copyText(currentShareUrl(uiState));toast("Link copied");}catch(e){toast(e.message);}});document.querySelectorAll("[data-format]").forEach(b=>b.addEventListener("click",()=>applyFormat(editor,b.dataset.format)));
|
||||
document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/pads/${encodeURIComponent(slug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null})});const url=new URL(result.url,location.origin).href;await copyText(url);toast("Page link copied");window.open(url,"_blank","noopener");}catch(error){toast(error.message);}});
|
||||
editor.addEventListener("scroll",()=>{gutter.scrollTop=editor.scrollTop;});editor.addEventListener("input",()=>{const newLines=editor.value.split("\n").length;const cursorLine=editor.value.slice(0,editor.selectionStart).split("\n").length-1;while(owners.length<newLines)owners.push(nickname);owners=owners.slice(0,newLines);owners[cursorLine]=nickname;render();if(applyingRemote)return;clearTimeout(saveTimer);document.querySelector("#save-state").textContent="Saving…";saveTimer=setTimeout(()=>socket?.update(editor.value,JSON.stringify(owners)),250);});
|
||||
document.querySelector("#password-form").addEventListener("submit",e=>{e.preventDefault();password=document.querySelector("#open-password").value;sessionStorage.setItem(`rustpad:pad:${slug}:password`,password);document.querySelector("#password-error").textContent="";connect();});
|
||||
const historyPanel=document.querySelector("#history-panel");document.querySelector("#history-button").addEventListener("click",async()=>{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 api(`/api/pads/${encodeURIComponent(slug)}/history`,{method:"POST",body:JSON.stringify({password:password||null})});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>${new Date(r.created_at.replace(" ","T")+"Z").toLocaleString("en-US")}</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 api(`/api/pads/${encodeURIComponent(slug)}/restore`,{method:"POST",body:JSON.stringify({password:password||null,revision_id:r.id})});toast("Version restored");});}}catch(e){list.innerHTML=`<p class="error">${escapeHtml(e.message)}</p>`;}});document.querySelector("#close-history").addEventListener("click",()=>{historyPanel.classList.remove("open");historyPanel.setAttribute("aria-hidden","true");document.body.classList.remove("history-open");});
|
||||
document.querySelector("#upload-button").addEventListener("click",()=>document.querySelector("#file-input").click());document.querySelector("#file-input").addEventListener("change",async e=>{const file=e.target.files[0];if(!file)return;const form=new FormData();form.append("password",password||"");form.append("file",file);try{const result=await api(`/api/pads/${encodeURIComponent(slug)}/files`,{method:"POST",body:form,headers:{}});const image=file.type.startsWith("image/");const text=image?``:`[${file.name}](${result.url})`;editor.setRangeText(text,editor.selectionStart,editor.selectionEnd,"end");editor.dispatchEvent(new Event("input"));toast("File uploaded");}catch(err){toast(err.message);}e.target.value="";});
|
||||
initialize();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
|
||||
const token = location.pathname.split("/").filter(Boolean)[1];
|
||||
const content = document.querySelector("#public-content");
|
||||
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
|
||||
async function renderMermaid(){const nodes=content.querySelectorAll(".mermaid");if(!nodes.length)return;try{const {default:mermaid}=await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs");mermaid.initialize({startOnLoad:false,theme:"dark",securityLevel:"strict"});await mermaid.run({nodes:[...nodes]});}catch{nodes.forEach(n=>n.insertAdjacentHTML("beforebegin",'<p class="error">Failed to load Mermaid.</p>'));}}
|
||||
async function initialize(){try{const page=await api(`/api/public/${encodeURIComponent(token)}`);document.querySelector("#public-title").textContent=page.title;document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")}`;document.title=`${page.title} · RustPad`;content.innerHTML=renderMarkdown(page.content);await renderMermaid();}catch(error){content.innerHTML=`<p class="error">${String(error.message)}</p>`;}}
|
||||
document.querySelector("#copy-public-link").addEventListener("click",async()=>{try{await copyText(location.href);toast("Link copied");}catch(error){toast(error.message);}});
|
||||
initialize();
|
||||
@@ -1,6 +1,5 @@
|
||||
export function passwordKey(workspaceSlug) { return `rustpad:workspace:${workspaceSlug}:password`; }
|
||||
export function getPassword(workspaceSlug) { return sessionStorage.getItem(passwordKey(workspaceSlug)) || ""; }
|
||||
export function setPassword(workspaceSlug, password) {
|
||||
if (password) sessionStorage.setItem(passwordKey(workspaceSlug), password);
|
||||
else sessionStorage.removeItem(passwordKey(workspaceSlug));
|
||||
}
|
||||
export function setPassword(workspaceSlug, password) { if (password) sessionStorage.setItem(passwordKey(workspaceSlug), password); else sessionStorage.removeItem(passwordKey(workspaceSlug)); }
|
||||
export function getNickname() { return localStorage.getItem("rustpad:nickname") || ""; }
|
||||
export function setNickname(value) { localStorage.setItem("rustpad:nickname", value.trim()); }
|
||||
|
||||
+7
-56
@@ -1,60 +1,11 @@
|
||||
export class NoteSocket {
|
||||
constructor({ workspaceSlug, noteSlug, password, onStatus, onAuthenticated, onDocument, onError }) {
|
||||
Object.assign(this, { workspaceSlug, noteSlug, password, onStatus, onAuthenticated, onDocument, onError });
|
||||
this.socket = null; this.timer = null; this.closed = false;
|
||||
}
|
||||
connect() {
|
||||
clearTimeout(this.timer); this.onStatus?.("connecting");
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
this.socket = new WebSocket(`${protocol}//${location.host}/ws/${encodeURIComponent(this.workspaceSlug)}/${encodeURIComponent(this.noteSlug)}`);
|
||||
this.socket.addEventListener("open", () => this.socket.send(JSON.stringify({ type: "authenticate", password: this.password || null })));
|
||||
this.socket.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === "error") { this.onError?.(message.message); this.closed = true; this.socket.close(); }
|
||||
if (message.type === "authenticated") { this.onStatus?.("online"); this.onAuthenticated?.(message); }
|
||||
if (message.type === "document") this.onDocument?.(message);
|
||||
});
|
||||
this.socket.addEventListener("close", () => { if (!this.closed) { this.onStatus?.("offline"); this.timer = setTimeout(() => this.connect(), 1500); } });
|
||||
this.socket.addEventListener("error", () => this.socket.close());
|
||||
}
|
||||
update(content) { if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify({ type: "update", content })); }
|
||||
stop() { this.closed = true; clearTimeout(this.timer); this.socket?.close(); }
|
||||
constructor({ workspaceSlug, noteSlug, password, nickname, onStatus, onAuthenticated, onDocument, onError }) { Object.assign(this, { workspaceSlug, noteSlug, password, nickname, onStatus, onAuthenticated, onDocument, onError }); this.socket=null; this.timer=null; this.closed=false; }
|
||||
connect() { clearTimeout(this.timer); this.closed=false; this.onStatus?.("connecting"); const protocol=location.protocol==="https:"?"wss:":"ws:"; this.socket=new WebSocket(`${protocol}//${location.host}/ws/${encodeURIComponent(this.workspaceSlug)}/${encodeURIComponent(this.noteSlug)}`); this.socket.addEventListener("open",()=>this.socket.send(JSON.stringify({type:"authenticate",password:this.password||null,nickname:this.nickname||null}))); this.socket.addEventListener("message",event=>{const m=JSON.parse(event.data); if(m.type==="error"){this.onError?.(m.message);this.closed=true;this.socket.close();} if(m.type==="authenticated"){this.onStatus?.("online");this.onAuthenticated?.(m);} if(m.type==="document")this.onDocument?.(m);}); this.socket.addEventListener("close",()=>{if(!this.closed){this.onStatus?.("offline");this.timer=setTimeout(()=>this.connect(),1500);}}); this.socket.addEventListener("error",()=>{this.onError?.("Failed to connect to the WebSocket server");this.socket.close();}); }
|
||||
update(content, ownerMap="[]") { if(this.socket?.readyState===WebSocket.OPEN)this.socket.send(JSON.stringify({type:"update",content,owner_map:ownerMap})); }
|
||||
stop(){this.closed=true;clearTimeout(this.timer);this.socket?.close();}
|
||||
}
|
||||
|
||||
export class PadSocket {
|
||||
constructor({ slug, password, onStatus, onAuthenticated, onDocument, onError }) {
|
||||
Object.assign(this, { slug, password, onStatus, onAuthenticated, onDocument, onError });
|
||||
this.socket = null;
|
||||
this.timer = null;
|
||||
this.closed = false;
|
||||
}
|
||||
connect() {
|
||||
clearTimeout(this.timer);
|
||||
this.closed = false;
|
||||
this.onStatus?.("connecting");
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
this.socket = new WebSocket(`${protocol}//${location.host}/ws/p/${encodeURIComponent(this.slug)}`);
|
||||
this.socket.addEventListener("open", () => this.socket.send(JSON.stringify({ type: "authenticate", password: this.password || null })));
|
||||
this.socket.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === "error") { this.onError?.(message.message); this.closed = true; this.socket.close(); }
|
||||
if (message.type === "authenticated") { this.onStatus?.("online"); this.onAuthenticated?.(message); }
|
||||
if (message.type === "document") this.onDocument?.(message);
|
||||
});
|
||||
this.socket.addEventListener("close", () => {
|
||||
if (!this.closed) {
|
||||
this.onStatus?.("offline");
|
||||
this.timer = setTimeout(() => this.connect(), 1500);
|
||||
}
|
||||
});
|
||||
this.socket.addEventListener("error", () => this.socket.close());
|
||||
}
|
||||
update(content) {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify({ type: "update", content }));
|
||||
}
|
||||
stop() {
|
||||
this.closed = true;
|
||||
clearTimeout(this.timer);
|
||||
this.socket?.close();
|
||||
}
|
||||
constructor({slug,password,nickname,onStatus,onAuthenticated,onDocument,onError}){Object.assign(this,{slug,password,nickname,onStatus,onAuthenticated,onDocument,onError});this.socket=null;this.timer=null;this.closed=false;}
|
||||
connect(){clearTimeout(this.timer);this.closed=false;this.onStatus?.("connecting");const protocol=location.protocol==="https:"?"wss:":"ws:";this.socket=new WebSocket(`${protocol}//${location.host}/ws/p/${encodeURIComponent(this.slug)}`);this.socket.addEventListener("open",()=>this.socket.send(JSON.stringify({type:"authenticate",password:this.password||null,nickname:this.nickname||null})));this.socket.addEventListener("message",e=>{const m=JSON.parse(e.data);if(m.type==="error"){this.onError?.(m.message);this.closed=true;this.socket.close();}if(m.type==="authenticated"){this.onStatus?.("online");this.onAuthenticated?.(m);}if(m.type==="document")this.onDocument?.(m);});this.socket.addEventListener("close",()=>{if(!this.closed){this.onStatus?.("offline");this.timer=setTimeout(()=>this.connect(),1500);}});this.socket.addEventListener("error",()=>{this.onError?.("Failed to connect to the WebSocket server");this.socket.close();});}
|
||||
update(content,ownerMap="[]"){if(this.socket?.readyState===WebSocket.OPEN)this.socket.send(JSON.stringify({type:"update",content,owner_map:ownerMap}));} stop(){this.closed=true;clearTimeout(this.timer);this.socket?.close();}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { api } from "./api.js?v=0.6.0"; import { copyText } from "./clipboard.js?v=0.6.0"; import { getPassword, setPassword } from "./session.js?v=0.6.0";
|
||||
import { api } from "@rustpad/api"; import { copyText } from "@rustpad/clipboard"; import { getPassword, setPassword } from "@rustpad/session";
|
||||
const parts = location.pathname.split("/").filter(Boolean), slug = parts[1]; let info, password = getPassword(slug); const dialog = document.querySelector("#password-dialog"), notesList = document.querySelector("#notes-list");
|
||||
function toast(text) { const el = document.querySelector("#toast"); el.textContent = text; el.classList.add("visible"); setTimeout(() => el.classList.remove("visible"), 1600); }
|
||||
function renderNotes(notes) { notesList.innerHTML = notes.length ? notes.map(note => `<a class="note-card" href="${note.url}?view=split&mode=markdown"><h3>${escapeHtml(note.title)}</h3><p>Aktualizacja: ${new Date(note.updated_at).toLocaleString("pl-PL")}</p></a>`).join("") : '<p class="empty">Brak notatek.</p>'; }
|
||||
function renderNotes(notes) { notesList.innerHTML = notes.length ? notes.map(note => `<a class="note-card" href="${note.url}?view=split&mode=markdown"><h3>${escapeHtml(note.title)}</h3><p>Updated: ${new Date(note.updated_at).toLocaleString("en-US")}</p></a>`).join("") : '<p class="empty">No notes yet.</p>'; }
|
||||
function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; }
|
||||
async function openWorkspace() { try { const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open`, { method: "POST", body: JSON.stringify({ password: password || null }) }); info = data.workspace; document.querySelector("#workspace-title").textContent = info.title; document.querySelector("#workspace-url").textContent = location.pathname; document.title = `${info.title} · RustPad`; renderNotes(data.notes); if (dialog.open) dialog.close(); } catch (e) { if (info?.protected || e.message.includes("hasło")) { document.querySelector("#password-error").textContent = e.message; if (!dialog.open) dialog.showModal(); } else document.querySelector("#workspace-error").textContent = e.message; } }
|
||||
async function openWorkspace() { try { const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open`, { method: "POST", body: JSON.stringify({ password: password || null }) }); info = data.workspace; document.querySelector("#workspace-title").textContent = info.title; document.querySelector("#workspace-url").textContent = location.pathname; document.title = `${info.title} · RustPad`; renderNotes(data.notes); if (dialog.open) dialog.close(); } catch (e) { if (info?.protected || e.message.toLowerCase().includes("password")) { document.querySelector("#password-error").textContent = e.message; if (!dialog.open) dialog.showModal(); } else document.querySelector("#workspace-error").textContent = e.message; } }
|
||||
async function init() { try { info = await api(`/api/workspaces/${encodeURIComponent(slug)}`); document.querySelector("#workspace-title").textContent = info.title; document.querySelector("#workspace-url").textContent = location.pathname; if (info.protected && !password) dialog.showModal(); else openWorkspace(); } catch (e) { document.querySelector("#workspace-error").textContent = e.message; } }
|
||||
document.querySelector("#password-form").addEventListener("submit", e => { e.preventDefault(); password = document.querySelector("#open-password").value; setPassword(slug, password); openWorkspace(); });
|
||||
document.querySelector("#new-note-button").addEventListener("click", () => document.querySelector("#note-dialog").showModal()); document.querySelector("#cancel-note").addEventListener("click", () => document.querySelector("#note-dialog").close());
|
||||
document.querySelector("#note-form").addEventListener("submit", async e => { e.preventDefault(); const error = document.querySelector("#note-error"); error.textContent = ""; try { const note = await api(`/api/workspaces/${encodeURIComponent(slug)}/notes`, { method: "POST", body: JSON.stringify({ name: document.querySelector("#note-name").value, password: password || null }) }); location.assign(`${note.url}?view=split&mode=markdown`); } catch (err) { error.textContent = err.message; } });
|
||||
document.querySelector("#copy-workspace-link").addEventListener("click", async () => { try { await copyText(new URL(location.pathname, location.origin).href); toast("Skopiowano link"); } catch (e) { toast(e.message); } }); init();
|
||||
document.querySelector("#copy-workspace-link").addEventListener("click", async () => { try { await copyText(new URL(location.pathname, location.origin).href); toast("Link copied"); } catch (e) { toast(e.message); } }); init();
|
||||
|
||||
Reference in New Issue
Block a user