new functions and fixes
This commit is contained in:
+101
-10
@@ -28,10 +28,11 @@ import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url
|
||||
import { toast } from "@rustpad/toast";
|
||||
import { getTheme } from "@rustpad/theme";
|
||||
import { isResourceAccessError } from "@rustpad/security";
|
||||
import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-libs";
|
||||
|
||||
export function startNoteEditor(adapter) {
|
||||
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer");
|
||||
const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
||||
const modeToggle = document.querySelector("#mode-toggle"), toolbarCollapseToggle = document.querySelector("#toolbar-collapse-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
||||
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), mobileConnectionDetails = document.querySelector("#mobile-connection-details"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread"), connectionNotice = document.querySelector("#connection-notice"), connectionNoticeTitle = document.querySelector("#connection-notice-title"), connectionNoticeMessage = document.querySelector("#connection-notice-message");
|
||||
const saveState = document.querySelector("#save-state");
|
||||
editor.readOnly = true;
|
||||
@@ -40,12 +41,14 @@ export function startNoteEditor(adapter) {
|
||||
const mobileFontFamily = document.querySelector("#mobile-font-family"), mobileFontSize = document.querySelector("#mobile-font-size"), mobileLineToggle = document.querySelector("#mobile-line-numbers-toggle"), mobilePreviewLineToggle = document.querySelector("#mobile-preview-line-numbers-toggle"), mobileCompactToggle = document.querySelector("#mobile-compact-toggle"), mobileLineLinksToggle = document.querySelector("#mobile-line-links-toggle");
|
||||
const shareToken = new URLSearchParams(location.search).get("share");
|
||||
const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`;
|
||||
let toolbarCollapsed = localStorage.getItem(notePreferenceKey("toolbar-collapsed")) === "on";
|
||||
const collaborationClientId = typeof crypto.randomUUID === "function"
|
||||
? 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;
|
||||
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
|
||||
let pendingPreviewViewport = null;
|
||||
const editHistory = {
|
||||
entries: [],
|
||||
index: -1,
|
||||
@@ -230,6 +233,7 @@ export function startNoteEditor(adapter) {
|
||||
lineToggle.checked = info.editor_line_numbers !== false;
|
||||
previewLineToggle.checked = info.preview_line_numbers === true;
|
||||
lineLinksToggle.checked = info.line_links === true;
|
||||
toolbarCollapsed = info.toolbar_collapsed === true;
|
||||
if (["mono", "system", "serif", "arial", "georgia"].includes(info.font_family)) fontFamily.value = info.font_family;
|
||||
if (["14", "16", "18", "20", "22"].includes(String(info.font_size))) fontSize.value = String(info.font_size);
|
||||
}
|
||||
@@ -325,8 +329,9 @@ export function startNoteEditor(adapter) {
|
||||
setStatus(null, "Connecting…");
|
||||
}
|
||||
function updateAddressLabel() { document.querySelector(adapter.addressSelector).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: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
||||
async function renderCodeHighlight() { const nodes = preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)'); if (!nodes.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); nodes.forEach(node => { const lines = node.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(node); return; } const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); node.classList.add("hljs"); }); } catch { } }
|
||||
async function renderMermaid() { const nodes = preview.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const mermaid = await loadMermaid(); mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
||||
async function renderCodeHighlight() { const nodes = preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)'); if (!nodes.length) return; try { const hljs = await loadHighlight(); nodes.forEach(node => { const lines = node.querySelectorAll(".code-line"); if (!lines.length) { hljs.highlightElement(node); return; } const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.highlightAuto(line.textContent).value; } }); node.classList.add("hljs"); }); } catch { } }
|
||||
async function renderMediaPlayers() { const nodes = preview.querySelectorAll("[data-rustpad-player]"); if (!nodes.length) return; try { const { hydrateMediaPlayers } = await loadMediaPlayer(); hydrateMediaPlayers(preview); } catch { } }
|
||||
function renderParticipantBadges(owners) {
|
||||
if (!participantBadges) return;
|
||||
const people = new Map();
|
||||
@@ -870,15 +875,21 @@ export function startNoteEditor(adapter) {
|
||||
return Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 20;
|
||||
}
|
||||
|
||||
function scrollRatio(element) {
|
||||
function scrollState(element) {
|
||||
const range = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||
return range > 0 ? element.scrollTop / range : 0;
|
||||
const top = Math.max(0, Math.min(range, element.scrollTop));
|
||||
return {
|
||||
ratio: range > 0 ? top / range : 0,
|
||||
atStart: top <= 2,
|
||||
atEnd: range > 0 && range - top <= 2,
|
||||
};
|
||||
}
|
||||
|
||||
function editorScrollAnchor() {
|
||||
const state = scrollState(editor);
|
||||
return {
|
||||
sourceLine: 1 + Math.max(0, editor.scrollTop) / editorLineHeight(),
|
||||
ratio: scrollRatio(editor),
|
||||
...state,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -900,8 +911,9 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
|
||||
function previewScrollAnchor() {
|
||||
const state = scrollState(preview);
|
||||
const positions = previewLinePositions();
|
||||
if (!positions.length) return { sourceLine: null, ratio: scrollRatio(preview) };
|
||||
if (!positions.length) return { sourceLine: null, ...state };
|
||||
const paddingTop = parseFloat(getComputedStyle(preview).paddingTop) || 0;
|
||||
const viewportTop = preview.scrollTop + paddingTop;
|
||||
let currentIndex = positions.findIndex(position => position.bottom > viewportTop + 0.5);
|
||||
@@ -912,7 +924,7 @@ export function startNoteEditor(adapter) {
|
||||
const sourceLine = next
|
||||
? current.sourceLine + progress * (next.sourceLine - current.sourceLine)
|
||||
: current.sourceLine;
|
||||
return { sourceLine, ratio: scrollRatio(preview) };
|
||||
return { sourceLine, ...state };
|
||||
}
|
||||
|
||||
function activeScrollAnchor(view = renderedView) {
|
||||
@@ -925,7 +937,9 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
|
||||
function scrollEditorToAnchor(anchor) {
|
||||
if (Number.isFinite(anchor?.sourceLine)) {
|
||||
if (anchor?.atEnd) setScrollRatio(editor, 1);
|
||||
else if (anchor?.atStart) setScrollRatio(editor, 0);
|
||||
else if (Number.isFinite(anchor?.sourceLine)) {
|
||||
const range = Math.max(0, editor.scrollHeight - editor.clientHeight);
|
||||
editor.scrollTop = Math.max(0, Math.min(range, (anchor.sourceLine - 1) * editorLineHeight()));
|
||||
} else setScrollRatio(editor, anchor?.ratio || 0);
|
||||
@@ -933,6 +947,14 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
|
||||
function scrollPreviewToAnchor(anchor) {
|
||||
if (anchor?.atEnd) {
|
||||
setScrollRatio(preview, 1);
|
||||
return;
|
||||
}
|
||||
if (anchor?.atStart) {
|
||||
setScrollRatio(preview, 0);
|
||||
return;
|
||||
}
|
||||
const positions = previewLinePositions();
|
||||
if (!Number.isFinite(anchor?.sourceLine) || !positions.length) {
|
||||
setScrollRatio(preview, anchor?.ratio || 0);
|
||||
@@ -976,7 +998,62 @@ export function startNoteEditor(adapter) {
|
||||
if (activeView() !== "split") return;
|
||||
scrollPreviewToAnchor(editorScrollAnchor());
|
||||
}
|
||||
function renderNow() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); scheduleAliasFileRefresh(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}">${escapeHtml(line) || "<br>"}</div>`).join(""); document.querySelector("#preview-label").textContent = "Text preview"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); requestAnimationFrame(syncPreviewScroll); }
|
||||
|
||||
function capturePreviewViewport(target) {
|
||||
if (!target || !preview.contains(target)) return null;
|
||||
const sourceLine = Number(target.dataset.sourceLine);
|
||||
const previewRect = preview.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
return {
|
||||
sourceLine: Number.isFinite(sourceLine) ? sourceLine : null,
|
||||
viewportOffset: targetRect.top - previewRect.top,
|
||||
scrollTop: preview.scrollTop,
|
||||
focused: document.activeElement === target,
|
||||
};
|
||||
}
|
||||
|
||||
function restorePreviewViewport(snapshot) {
|
||||
if (!snapshot) return;
|
||||
const range = Math.max(0, preview.scrollHeight - preview.clientHeight);
|
||||
if (Number.isFinite(snapshot.sourceLine)) {
|
||||
const target = preview.querySelector(`.task-checkbox[data-source-line="${snapshot.sourceLine}"]`)
|
||||
|| preview.querySelector(`.preview-source-line[data-source-line="${snapshot.sourceLine}"]`);
|
||||
if (target) {
|
||||
const previewRect = preview.getBoundingClientRect();
|
||||
const currentOffset = target.getBoundingClientRect().top - previewRect.top;
|
||||
preview.scrollTop = Math.max(0, Math.min(range, preview.scrollTop + currentOffset - snapshot.viewportOffset));
|
||||
if (snapshot.focused) target.focus({ preventScroll: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
preview.scrollTop = Math.max(0, Math.min(range, snapshot.scrollTop || 0));
|
||||
}
|
||||
|
||||
function renderNow() {
|
||||
const previewViewport = pendingPreviewViewport;
|
||||
pendingPreviewViewport = null;
|
||||
if (uiState.mode === "markdown") {
|
||||
preview.classList.remove("preview--raw");
|
||||
preview.innerHTML = renderMarkdown(editor.value);
|
||||
scheduleAliasFileRefresh(editor.value);
|
||||
document.querySelector("#preview-label").textContent = "Preview (media / mermaid / markdown)";
|
||||
renderMermaid();
|
||||
renderCodeHighlight();
|
||||
renderMediaPlayers();
|
||||
} else {
|
||||
preview.classList.add("preview--raw");
|
||||
preview.innerHTML = editor.value.split("\n").map((line, index) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}">${escapeHtml(line) || "<br>"}</div>`).join("");
|
||||
document.querySelector("#preview-label").textContent = "Text preview";
|
||||
}
|
||||
alignPreviewLineNumbers(preview);
|
||||
document.querySelector("#characters").textContent = `${editor.value.length} characters`;
|
||||
document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`;
|
||||
renderGutter();
|
||||
requestAnimationFrame(() => {
|
||||
if (previewViewport) restorePreviewViewport(previewViewport);
|
||||
else syncPreviewScroll();
|
||||
});
|
||||
}
|
||||
const render = createRenderQueue(renderNow);
|
||||
function queueCollaborativeOperation(operation, ownerReplacements = []) {
|
||||
if (!collaboration.ready || !canEditDocument()) return false;
|
||||
@@ -1066,6 +1143,16 @@ export function startNoteEditor(adapter) {
|
||||
return singlePaneQuery.matches ? compactView : uiState.view;
|
||||
}
|
||||
|
||||
function applyToolbarCollapsed() {
|
||||
document.body.classList.toggle("toolbar-collapsed", toolbarCollapsed);
|
||||
if (!toolbarCollapseToggle) return;
|
||||
const label = toolbarCollapsed ? "Show editor toolbar" : "Hide editor toolbar";
|
||||
toolbarCollapseToggle.setAttribute("aria-pressed", String(toolbarCollapsed));
|
||||
toolbarCollapseToggle.setAttribute("aria-label", label);
|
||||
toolbarCollapseToggle.title = label;
|
||||
toolbarCollapseToggle.querySelector(".toolbar-collapse-toggle__icon").textContent = toolbarCollapsed ? "⌄" : "⌃";
|
||||
}
|
||||
|
||||
function applyUi({ write = false, replace = false } = {}) {
|
||||
const view = activeView();
|
||||
renderedView = view;
|
||||
@@ -1073,6 +1160,7 @@ export function startNoteEditor(adapter) {
|
||||
editorWorkspace.style.setProperty("--editor-font-size", `${fontSize.value}px`);
|
||||
document.body.classList.toggle("compact-editor", compactToggle.checked);
|
||||
document.body.classList.toggle("compact-note-layout", compactLayoutQuery.matches);
|
||||
applyToolbarCollapsed();
|
||||
document.querySelectorAll("[data-view]").forEach(button => {
|
||||
const active = button.dataset.view === view;
|
||||
button.classList.toggle("active", active);
|
||||
@@ -1483,6 +1571,7 @@ export function startNoteEditor(adapter) {
|
||||
if (event.key === "Escape" && mobileEditorOptions?.open) mobileEditorOptions.open = false;
|
||||
});
|
||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUiPreservingScroll({ write: true }); });
|
||||
toolbarCollapseToggle?.addEventListener("click", () => { toolbarCollapsed = !toolbarCollapsed; localStorage.setItem(notePreferenceKey("toolbar-collapsed"), toolbarCollapsed ? "on" : "off"); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
lineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("line-numbers"), lineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
previewLineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("preview-line-numbers"), previewLineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); alignPreviewLineNumbers(preview); scheduleEditorSettingsSave({ personal: true }); });
|
||||
compactToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("compact"), compactToggle.checked ? "on" : "off"); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
@@ -1680,6 +1769,7 @@ export function startNoteEditor(adapter) {
|
||||
editor_line_numbers: lineToggle.checked,
|
||||
preview_line_numbers: previewLineToggle.checked,
|
||||
line_links: lineLinksToggle.checked,
|
||||
toolbar_collapsed: toolbarCollapsed,
|
||||
font_family: fontFamily.value,
|
||||
font_size: Number(fontSize.value),
|
||||
};
|
||||
@@ -1811,6 +1901,7 @@ export function startNoteEditor(adapter) {
|
||||
const lineIndex = Number(checkbox.dataset.sourceLine) - 1;
|
||||
const lines = editor.value.split("\n");
|
||||
if (lineIndex < 0 || lineIndex >= lines.length) return;
|
||||
pendingPreviewViewport = capturePreviewViewport(checkbox);
|
||||
lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`);
|
||||
editor.value = lines.join("\n");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
Reference in New Issue
Block a user