This commit is contained in:
Mateusz Gruszczyński committed 2026-10-12 12:19:33 +02:00
1 parent acfe926066
commit df403c0aa7
26 files changed
+1502 -144

No files matched your search

+86 -9
View File
@@ -17,10 +17,12 @@ import { CollaborationRevisionGapError, CollaborationSession } from "@rustpad/co
import { copyText } from "@rustpad/clipboard";
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
import { applyFormat, bindFormatShortcuts, bindIndentationShortcuts } from "@rustpad/editor-format";
import { createMentionAutocomplete } from "@rustpad/mention-autocomplete";
import { createMentionNavigator } from "@rustpad/mention-navigation";
import { updateImageAliasInLineBySource } from "@rustpad/image-alias";
import { previewEditingHost } from "@rustpad/preview-edit";
import { createRenderQueue } from "@rustpad/render-queue";
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, setMarkdownMentions, 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";
@@ -49,7 +51,7 @@ export function startNoteEditor(adapter) {
? crypto.randomUUID().replaceAll("-", "")
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
const collaboration = new CollaborationSession(collaborationClientId);
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "", flushRequested = false, accountSession = null;
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, applyingHistory = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "", lastRevealedHeadingHash = "", flushRequested = false, accountSession = null;
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
let pendingPreviewViewport = null;
const editHistory = {
@@ -192,6 +194,7 @@ export function startNoteEditor(adapter) {
editor.readOnly = readOnly;
document.body.classList.toggle("document-read-only", readOnly);
if (!readOnly) return;
mentionAutocomplete.close();
clearTimeout(saveTimer);
saveState.textContent = label;
const activePreviewEdit = preview.querySelector('.preview-editable[contenteditable="true"]');
@@ -260,6 +263,37 @@ export function startNoteEditor(adapter) {
renderFavoriteButton(false);
}
}
const mentionAutocomplete = createMentionAutocomplete(editor);
const mentionNavigator = createMentionNavigator({
container: document.querySelector("#mentioned-users"),
list: document.querySelector("#mentioned-users-list"),
previewContainer: document.querySelector("#mentioned-users-preview"),
previewList: document.querySelector("#mentioned-users-preview-list"),
editor, preview, getView: () => activeView(), onEditorScroll: () => syncEditorLayers(),
});
let mentionLoadId = 0;
async function refreshMentionUsers() {
const requestId = ++mentionLoadId;
setMarkdownMentions([]);
mentionAutocomplete.setUsers([]);
mentionNavigator.setUsers([]);
if (!accountSession) { render(); return; }
try {
const params = new URLSearchParams({ kind: adapter.access.kind, slug: adapter.access.key });
const response = await api(`/api/auth/resources/mentions?${params}`);
if (requestId !== mentionLoadId) return;
setMarkdownMentions(response.users || []);
mentionAutocomplete.setUsers(response.users || []);
mentionNavigator.setUsers(response.users || []);
} catch {
if (requestId !== mentionLoadId) return;
setMarkdownMentions([]);
mentionAutocomplete.setUsers([]);
mentionNavigator.setUsers([]);
}
render();
}
async function loadNoteInfo() {
info = await adapter.loadInfo(sessionHeaders());
const documentTitle = document.querySelector("#document-title");
@@ -270,6 +304,7 @@ export function startNoteEditor(adapter) {
globalColor = info.global_color || ""; noteColor = info.note_color || "";
if (!getAuthToken()) noteColor = readGuestColor();
updateMarkdownFiles(info.files || []);
void refreshMentionUsers();
if (info.personal_editor_settings) {
compactToggle.checked = info.compact_view !== false;
lineToggle.checked = info.editor_line_numbers !== false;
@@ -315,11 +350,11 @@ export function startNoteEditor(adapter) {
setDiagnosticField("quality", qualityLabel);
setDiagnosticField("latency", Number.isFinite(latency.current)
? t("diagnostics.latency", {
current: formatNumber(latency.current),
average: formatNumber(latency.average),
minimum: formatNumber(latency.minimum),
maximum: formatNumber(latency.maximum),
}, `${latency.current} ms · avg ${latency.average} ms · ${latency.minimum}–${latency.maximum} ms`)
current: formatNumber(latency.current),
average: formatNumber(latency.average),
minimum: formatNumber(latency.minimum),
maximum: formatNumber(latency.maximum),
}, `${latency.current} ms · avg ${latency.average} ms · ${latency.minimum}–${latency.maximum} ms`)
: t("diagnostics.waitHeartbeat", {}, "Waiting for heartbeat"));
setDiagnosticField("jitter", Number.isFinite(latency.jitter) ? `${formatNumber(latency.jitter)} ms` : "—");
setDiagnosticField("uptime", runtime.authenticated_at
@@ -513,6 +548,29 @@ export function startNoteEditor(adapter) {
syncEditorLayers();
}
function revealHeadingAnchor() {
if (!location.hash || location.hash === lastRevealedHeadingHash || uiState.mode !== "markdown") return;
let id;
try { id = decodeURIComponent(location.hash.slice(1)); } catch { return; }
if (!id || /^L[1-9]\d*$/i.test(id)) return;
const heading = [...preview.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")]
.find(element => element.id === id);
if (!heading) return;
for (let parent = heading.parentElement; parent && parent !== preview; parent = parent.parentElement) {
if (parent.matches("details.markdown-details")) parent.open = true;
}
if (renderedView !== "edit") {
const headingBounds = heading.getBoundingClientRect();
const previewBounds = preview.getBoundingClientRect();
preview.scrollTop += headingBounds.top - previewBounds.top - 16;
}
if (renderedView !== "preview") {
const sourceLine = Number(heading.dataset.sourceLine);
if (Number.isFinite(sourceLine)) scrollEditorToAnchor({ sourceLine });
}
lastRevealedHeadingHash = location.hash;
}
function revealLinkedLine() {
if (!location.hash || location.hash === lastRevealedLineHash) return;
const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1);
@@ -540,6 +598,7 @@ export function startNoteEditor(adapter) {
function previewNodeMarkdownParts(current) {
if (current.nodeType !== Node.ELEMENT_NODE) return { open: "", close: "", atomic: null };
const tag = current.tagName.toLowerCase();
if (current.classList.contains("heading-permalink")) return { open: "", close: "", atomic: "" };
if (tag === "strong" || tag === "b") return { open: "**", close: "**", atomic: null };
if (tag === "em" || tag === "i") return { open: "*", close: "*", atomic: null };
if (tag === "s" || tag === "del") return { open: "~~", close: "~~", atomic: null };
@@ -1176,6 +1235,7 @@ export function startNoteEditor(adapter) {
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";
}
mentionNavigator.render({ markdown: uiState.mode === "markdown" });
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`;
@@ -1183,6 +1243,7 @@ export function startNoteEditor(adapter) {
requestAnimationFrame(() => {
if (previewViewport) restorePreviewViewport(previewViewport);
else syncPreviewScroll();
revealHeadingAnchor();
});
}
const render = createRenderQueue(renderNow);
@@ -1252,6 +1313,7 @@ export function startNoteEditor(adapter) {
function restoreHistorySnapshot(snapshot) {
if (!snapshot || !canEditDocument()) return;
mentionAutocomplete.close();
const previous = editor.value;
const viewportScrollTop = editor.scrollTop;
const viewportScrollLeft = editor.scrollLeft;
@@ -1340,6 +1402,7 @@ export function startNoteEditor(adapter) {
}
function applyEditorDocument(content, ownerMap, { resetHistory = true } = {}) {
if (content !== editor.value) mentionAutocomplete.close();
if (content === editor.value) {
if (ownerMap != null) authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length);
previousContent = content;
@@ -1966,8 +2029,20 @@ export function startNoteEditor(adapter) {
}
}
}
window.addEventListener("popstate", () => { const anchor = activeScrollAnchor(); lastRevealedLineHash = ""; uiState = readEditorState(); applyUi(); restoreScrollAnchor(anchor); requestAnimationFrame(revealLinkedLine); });
window.addEventListener("hashchange", () => { lastRevealedLineHash = ""; renderGutter(); requestAnimationFrame(revealLinkedLine); });
window.addEventListener("popstate", () => { const anchor = activeScrollAnchor(); lastRevealedLineHash = ""; lastRevealedHeadingHash = ""; uiState = readEditorState(); applyUi(); restoreScrollAnchor(anchor); requestAnimationFrame(() => { revealLinkedLine(); revealHeadingAnchor(); }); });
window.addEventListener("hashchange", () => { lastRevealedLineHash = ""; lastRevealedHeadingHash = ""; renderGutter(); requestAnimationFrame(() => { revealLinkedLine(); revealHeadingAnchor(); }); });
preview.addEventListener("click", event => {
const link = event.target instanceof Element ? event.target.closest('a[href^="#"]') : null;
if (!link) return;
const targetHash = link.getAttribute("href");
let headingId;
try { headingId = decodeURIComponent(targetHash.slice(1)); } catch { return; }
if (![...preview.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")].some(node => node.id === headingId)) return;
event.preventDefault();
if (location.hash !== targetHash) history.pushState(null, "", targetHash);
lastRevealedHeadingHash = "";
revealHeadingAnchor();
});
window.addEventListener("rustpad:urlchange", updateAddressLabel);
gutter.addEventListener("click", async event => {
const button = event.target.closest(".line-number-button[data-line]");
@@ -2020,10 +2095,12 @@ export function startNoteEditor(adapter) {
});
window.addEventListener("rustpad:session-change", event => {
accountSession = event.detail?.session || null;
void refreshMentionUsers();
syncFavoriteButton();
});
window.addEventListener("rustpad:session-expired", () => {
accountSession = null;
void refreshMentionUsers();
renderFavoriteButton(false);
});