first commit
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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");
|
||||
});
|
||||
|
||||
initialize();
|
||||
Reference in New Issue
Block a user