v0.2.90
This commit is contained in:
1 parent
0abc8e871d
commit
4da41a1404
10 files changed
+458
-49
No files matched your search
@@ -6318,6 +6318,13 @@ dialog::backdrop {
|
||||
}
|
||||
}
|
||||
|
||||
/* The author labels scroll as one layer; clipping still belongs to .owner-labels. */
|
||||
.owner-labels-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.owner-label-group {
|
||||
position: absolute;
|
||||
right: calc(var(--editor-rendered-font-size, 14px) * .57);
|
||||
|
||||
+22
-1
@@ -116,13 +116,34 @@ export function authorshipOwners(model) {
|
||||
}
|
||||
|
||||
export function lineAuthors(content, model) {
|
||||
const spans = model?.spans || [];
|
||||
const starts = [0];
|
||||
for (let i = 0; i < content.length; i++) if (content.charCodeAt(i) === 10) starts.push(i + 1);
|
||||
|
||||
// Parsed authorship spans are sorted and non-overlapping. Retain the old
|
||||
// result for non-canonical callers instead of silently changing semantics.
|
||||
let canonical = true;
|
||||
for (let i = 0, previousEnd = 0; i < spans.length; i++) {
|
||||
const span = spans[i];
|
||||
if (!Number.isFinite(span?.start) || !Number.isFinite(span?.end)
|
||||
|| span.start < previousEnd || span.end < span.start) {
|
||||
canonical = false;
|
||||
break;
|
||||
}
|
||||
previousEnd = span.end;
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
return starts.map((start, index) => {
|
||||
const end = index + 1 < starts.length ? starts[index + 1] : content.length;
|
||||
const totals = new Map();
|
||||
const representatives = new Map();
|
||||
for (const span of model?.spans || []) {
|
||||
if (canonical) {
|
||||
while (cursor < spans.length && spans[cursor].end <= start) cursor++;
|
||||
}
|
||||
for (let i = canonical ? cursor : 0; i < spans.length; i++) {
|
||||
const span = spans[i];
|
||||
if (canonical && span.start >= end) break;
|
||||
const overlap = Math.max(0, Math.min(end, span.end) - Math.max(start, span.start));
|
||||
if (!overlap) continue;
|
||||
const identity = ownerIdentity(span.owner);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
||||
* Source-Available Code / Dual-Licensed.
|
||||
* See LICENSE.md for terms.
|
||||
*/
|
||||
|
||||
// Preserve existing nodes. Remove stale entries first: otherwise deleting a
|
||||
// user near the start of a 1000-person list would move every following node.
|
||||
// `desired` must contain each node at most once.
|
||||
export function reconcileChildren(container, desired) {
|
||||
const retained = new Set(desired);
|
||||
for (const node of [...container.childNodes]) {
|
||||
if (!retained.has(node)) node.remove();
|
||||
}
|
||||
let cursor = container.firstChild;
|
||||
for (const node of desired) {
|
||||
if (node === cursor) cursor = cursor.nextSibling;
|
||||
else container.insertBefore(node, cursor);
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -420,11 +420,12 @@ export function alignPreviewLineNumbers(root) {
|
||||
const styles = getComputedStyle(root);
|
||||
const targetLeft = parseFloat(styles.paddingLeft || "0") - 50;
|
||||
const rootLeft = root.getBoundingClientRect().left;
|
||||
const positions = [];
|
||||
// Finish all geometry reads before writing CSS properties. Interleaved
|
||||
// reads and writes force a full layout for each source line in Chromium.
|
||||
root.querySelectorAll(".preview-source-line").forEach(line => {
|
||||
const lineRect = line.getBoundingClientRect();
|
||||
const lineLeft = lineRect.left - rootLeft + root.scrollLeft;
|
||||
line.style.setProperty("--preview-line-left", `${targetLeft - lineLeft}px`);
|
||||
|
||||
const textRect = firstRenderedTextRect(line);
|
||||
const lineStyles = getComputedStyle(line);
|
||||
const gutterStyles = getComputedStyle(line, "::before");
|
||||
@@ -432,9 +433,16 @@ export function alignPreviewLineNumbers(root) {
|
||||
const textLineHeight = textRect?.height || 0;
|
||||
const borderTop = parseFloat(lineStyles.borderTopWidth) || 0;
|
||||
const textTop = textRect ? textRect.top - lineRect.top - borderTop : 0;
|
||||
const gutterTop = Math.max(0, textTop + (textLineHeight - gutterLineHeight) / 2);
|
||||
line.style.setProperty("--preview-line-top", `${gutterTop}px`);
|
||||
positions.push({
|
||||
line,
|
||||
left: `${targetLeft - lineLeft}px`,
|
||||
top: `${Math.max(0, textTop + (textLineHeight - gutterLineHeight) / 2)}px`,
|
||||
});
|
||||
});
|
||||
for (const position of positions) {
|
||||
position.line.style.setProperty("--preview-line-left", position.left);
|
||||
position.line.style.setProperty("--preview-line-top", position.top);
|
||||
}
|
||||
}
|
||||
|
||||
function stripFenceIndent(line, columns) {
|
||||
|
||||
+163
-42
@@ -22,6 +22,7 @@ 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 { reconcileChildren } from "@rustpad/dom-reconcile";
|
||||
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";
|
||||
@@ -323,7 +324,44 @@ export function startNoteEditor(adapter) {
|
||||
syncMobileEditorControls();
|
||||
updateCurrentUser(); return info;
|
||||
}
|
||||
function updatePresence(users) { const entries = Array.isArray(users) ? users : []; presenceUsers = entries.map(entry => typeof entry === "string" ? { name: entry, color: "" } : entry || {}); roomCount.textContent = t(entries.length === 1 ? "editor.user" : "editor.users", { count: entries.length }, `${entries.length} ${entries.length === 1 ? "user" : "users"}`); roomUsers.replaceChildren(...presenceUsers.map(user => { const li = document.createElement("li"), dot = document.createElement("span"), label = document.createElement("span"); li.className = "room-user"; dot.className = "room-user__dot"; dot.style.setProperty("--owner", /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(user.name)); label.textContent = user.name || t("editor.guest", {}, "Guest"); li.title = label.textContent; li.append(dot, label); return li; })); if (!entries.length) { const li = document.createElement("li"); li.textContent = t("editor.noActiveUsers", {}, "No active users"); roomUsers.append(li); } renderGutter(); }
|
||||
// Presence is independent of document text, authorship spans and the gutter.
|
||||
// Match by name, preserving duplicates and their DOM nodes between snapshots.
|
||||
function updatePresence(users) {
|
||||
const entries = Array.isArray(users) ? users : [];
|
||||
presenceUsers = entries.map(entry => typeof entry === "string" ? { name: entry, color: "" } : entry || {});
|
||||
roomCount.textContent = t(entries.length === 1 ? "editor.user" : "editor.users", { count: entries.length }, `${entries.length} ${entries.length === 1 ? "user" : "users"}`);
|
||||
const existing = new Map();
|
||||
for (const li of roomUsers.children) {
|
||||
if (li.dataset.presenceName === undefined) continue;
|
||||
const bucket = existing.get(li.dataset.presenceName) || [];
|
||||
bucket.push(li);
|
||||
existing.set(li.dataset.presenceName, bucket);
|
||||
}
|
||||
const nodes = presenceUsers.map(user => {
|
||||
const name = String(user.name || "");
|
||||
const bucket = existing.get(name);
|
||||
const li = bucket?.shift() || document.createElement("li");
|
||||
const color = /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(name);
|
||||
const displayName = name || t("editor.guest", {}, "Guest");
|
||||
if (!li.classList.contains("room-user")) {
|
||||
li.className = "room-user";
|
||||
li.append(document.createElement("span"), document.createElement("span"));
|
||||
li.firstElementChild.className = "room-user__dot";
|
||||
}
|
||||
li.dataset.presenceName = name;
|
||||
if (li.firstElementChild.style.getPropertyValue("--owner") !== color) li.firstElementChild.style.setProperty("--owner", color);
|
||||
if (li.lastElementChild.textContent !== displayName) li.lastElementChild.textContent = displayName;
|
||||
if (li.title !== displayName) li.title = displayName;
|
||||
return li;
|
||||
});
|
||||
if (!nodes.length) {
|
||||
const empty = document.createElement("li");
|
||||
empty.textContent = t("editor.noActiveUsers", {}, "No active users");
|
||||
nodes.push(empty);
|
||||
}
|
||||
reconcileChildren(roomUsers, nodes);
|
||||
renderParticipantBadges(authorshipColorsEnabled ? renderedAuthorshipOwners : []);
|
||||
}
|
||||
function updateLatency(ms) {
|
||||
const text = Number.isFinite(ms) ? `${ms} ms` : "— ms";
|
||||
socketLatency.textContent = text;
|
||||
@@ -476,6 +514,7 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
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 { } }
|
||||
let renderedAuthorshipOwners = [];
|
||||
function renderParticipantBadges(owners) {
|
||||
if (!participantBadges) return;
|
||||
const people = new Map();
|
||||
@@ -487,19 +526,26 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
participantBadges.hidden = authorshipMode !== "simple" || people.size < 2;
|
||||
const compact = people.size > 4;
|
||||
participantBadges.replaceChildren(...[...people.values()].map(person => {
|
||||
const badge = document.createElement("span");
|
||||
const previous = new Map([...participantBadges.children].map(badge => [badge.dataset.personName, badge]));
|
||||
const nodes = [...people.values()].map(person => {
|
||||
const badge = previous.get(person.name) || document.createElement("span");
|
||||
const label = compact && person.compactName ? person.compactName : person.name;
|
||||
badge.className = "participant-badge";
|
||||
badge.style.setProperty("--owner", person.color);
|
||||
badge.textContent = compact && person.compactName ? person.compactName : person.name;
|
||||
badge.title = person.name;
|
||||
badge.dataset.personName = person.name;
|
||||
if (badge.style.getPropertyValue("--owner") !== person.color) badge.style.setProperty("--owner", person.color);
|
||||
if (badge.textContent !== label) badge.textContent = label;
|
||||
if (badge.title !== person.name) badge.title = person.name;
|
||||
return badge;
|
||||
}));
|
||||
});
|
||||
reconcileChildren(participantBadges, nodes);
|
||||
}
|
||||
function syncOwnerLabels() {
|
||||
ownerLabels.querySelectorAll(".owner-label-group[data-content-top]").forEach(group => {
|
||||
group.style.top = `${Number(group.dataset.contentTop) - editor.scrollTop}px`;
|
||||
});
|
||||
// One composited transform instead of rewriting every author's position
|
||||
// on every scroll event (important for thousands of distinct authors).
|
||||
const canvas = ownerLabels.firstElementChild;
|
||||
if (canvas?.classList.contains("owner-labels-canvas")) {
|
||||
canvas.style.transform = `translate3d(0, ${-editor.scrollTop}px, 0)`;
|
||||
}
|
||||
}
|
||||
|
||||
function syncEditorLayers() {
|
||||
@@ -508,6 +554,9 @@ export function startNoteEditor(adapter) {
|
||||
syncOwnerLabels();
|
||||
}
|
||||
|
||||
let gutterLineCount = 0;
|
||||
let gutterLineHeight = 0;
|
||||
let highlightedGutterLine = null;
|
||||
function renderGutter() {
|
||||
document.body.classList.toggle("hide-editor-line-numbers", !lineToggle.checked);
|
||||
document.body.classList.toggle("hide-preview-line-numbers", !previewLineToggle.checked);
|
||||
@@ -517,6 +566,7 @@ export function startNoteEditor(adapter) {
|
||||
const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1);
|
||||
const lines = Array.from({ length: lineCount });
|
||||
const owners = authorshipOwners(authorship);
|
||||
renderedAuthorshipOwners = owners;
|
||||
const showAuthorship = authorshipColorsEnabled && owners.length > 0;
|
||||
const authorsByLine = showAuthorship ? lineAuthors(editor.value, authorship) : [];
|
||||
const full = authorshipMode === "full";
|
||||
@@ -527,12 +577,27 @@ export function startNoteEditor(adapter) {
|
||||
editorWorkspace.dataset.authorshipMode = authorshipMode;
|
||||
const style = getComputedStyle(editor), lineHeight = parseFloat(style.lineHeight) || 29, fontSize = parseFloat(style.fontSize) || 14, paddingTop = parseFloat(style.paddingTop) || 24, paddingBottom = parseFloat(style.paddingBottom) || 24;
|
||||
gutter.style.paddingTop = `${paddingTop}px`; gutter.style.paddingBottom = `${paddingBottom}px`; gutter.style.lineHeight = `${lineHeight}px`;
|
||||
gutter.innerHTML = lines.map((_, i) => `<div class="line-number-row" style="height:${lineHeight}px"><button class="line-number-button" type="button" tabindex="-1" data-line="${i + 1}" aria-label="Copy link to line ${i + 1}">${i + 1}</button></div>`).join("");
|
||||
// Physical rows represent sequential line numbers, not specific text lines.
|
||||
// Reuse them across character edits and add/remove only the changed tail.
|
||||
if (gutterLineHeight !== lineHeight) {
|
||||
for (const row of gutter.children) row.style.height = `${lineHeight}px`;
|
||||
gutterLineHeight = lineHeight;
|
||||
}
|
||||
if (lineCount > gutterLineCount) {
|
||||
let html = "";
|
||||
for (let i = gutterLineCount; i < lineCount; i++) {
|
||||
html += `<div class="line-number-row" style="height:${lineHeight}px"><button class="line-number-button" type="button" tabindex="-1" data-line="${i + 1}" aria-label="Copy link to line ${i + 1}">${i + 1}</button></div>`;
|
||||
}
|
||||
gutter.insertAdjacentHTML("beforeend", html);
|
||||
} else if (lineCount < gutterLineCount) {
|
||||
for (let i = gutterLineCount; i > lineCount; i--) gutter.lastElementChild?.remove();
|
||||
}
|
||||
gutterLineCount = lineCount;
|
||||
ownerLabels.style.setProperty("--editor-line-height", `${lineHeight}px`);
|
||||
ownerLabels.style.setProperty("--editor-rendered-font-size", `${fontSize}px`);
|
||||
if (full) {
|
||||
let previousAuthorSignature = null;
|
||||
ownerLabels.innerHTML = lines.map((_, i) => {
|
||||
ownerLabels.innerHTML = `<div class="owner-labels-canvas">${lines.map((_, i) => {
|
||||
const authors = authorsByLine[i] || [];
|
||||
if (!authors.length) return "";
|
||||
const top = paddingTop + i * lineHeight + lineHeight / 2;
|
||||
@@ -540,13 +605,17 @@ export function startNoteEditor(adapter) {
|
||||
if (signature === previousAuthorSignature) return "";
|
||||
previousAuthorSignature = signature;
|
||||
const badges = authors.map(owner => `<span class="owner-label" style="--owner:${colorFor(owner)}">${escapeHtml(ownerName(owner))}</span>`).join("");
|
||||
return `<span class="owner-label-group" data-content-top="${top}">${badges}</span>`;
|
||||
}).join("");
|
||||
return `<span class="owner-label-group" style="top:${top}px">${badges}</span>`;
|
||||
}).join("")}</div>`;
|
||||
} else ownerLabels.replaceChildren();
|
||||
if (showAuthorship) renderAuthorshipLayer(authorshipLayer, editor, authorship, colorFor);
|
||||
else authorshipLayer.replaceChildren();
|
||||
const linkedLine = lineFromHash(location.hash, lineCount);
|
||||
gutter.querySelector(`[data-line="${linkedLine}"]`)?.classList.add("is-linked");
|
||||
if (highlightedGutterLine !== linkedLine) {
|
||||
gutter.querySelector(`[data-line="${highlightedGutterLine}"]`)?.classList.remove("is-linked");
|
||||
gutter.querySelector(`[data-line="${linkedLine}"]`)?.classList.add("is-linked");
|
||||
highlightedGutterLine = linkedLine;
|
||||
}
|
||||
syncEditorLayers();
|
||||
}
|
||||
|
||||
@@ -1062,7 +1131,30 @@ export function startNoteEditor(adapter) {
|
||||
};
|
||||
}
|
||||
|
||||
let previewPositionCache = null;
|
||||
let cachedPreviewHeight = -1;
|
||||
let cachedPreviewWidth = -1;
|
||||
function invalidatePreviewPositions() { previewPositionCache = null; }
|
||||
// The (rect.top - container.top + scrollTop) coordinates are invariant to
|
||||
// scrolling; only rebuild them when layout/content actually changes.
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
new ResizeObserver(invalidatePreviewPositions).observe(preview);
|
||||
}
|
||||
if (typeof MutationObserver !== "undefined") {
|
||||
new MutationObserver(invalidatePreviewPositions).observe(preview, {
|
||||
subtree: true, childList: true, characterData: true,
|
||||
attributes: true, attributeFilter: ["open", "src", "class"],
|
||||
});
|
||||
}
|
||||
preview.addEventListener("toggle", invalidatePreviewPositions, true);
|
||||
preview.addEventListener("load", invalidatePreviewPositions, true);
|
||||
window.addEventListener("resize", invalidatePreviewPositions);
|
||||
document.fonts?.addEventListener?.("loadingdone", invalidatePreviewPositions);
|
||||
function previewLinePositions() {
|
||||
// Height/width checks catch dynamic embeds that change geometry without
|
||||
// a DOM mutation (e.g. a media player expanding after initialization).
|
||||
const height = preview.scrollHeight, width = preview.clientWidth;
|
||||
if (previewPositionCache && cachedPreviewHeight === height && cachedPreviewWidth === width) return previewPositionCache;
|
||||
const previewRect = preview.getBoundingClientRect();
|
||||
const positions = [];
|
||||
preview.querySelectorAll(".preview-source-line[data-source-line]").forEach(node => {
|
||||
@@ -1084,7 +1176,10 @@ export function startNoteEditor(adapter) {
|
||||
bottom: rect.bottom - previewRect.top + preview.scrollTop,
|
||||
});
|
||||
});
|
||||
return positions.sort((left, right) => left.top - right.top || left.sourceLine - right.sourceLine);
|
||||
previewPositionCache = positions.sort((left, right) => left.top - right.top || left.sourceLine - right.sourceLine);
|
||||
cachedPreviewHeight = height;
|
||||
cachedPreviewWidth = width;
|
||||
return previewPositionCache;
|
||||
}
|
||||
|
||||
function previewScrollAnchor() {
|
||||
@@ -1215,34 +1310,51 @@ export function startNoteEditor(adapter) {
|
||||
preview.scrollTop = Math.max(0, Math.min(range, snapshot.scrollTop || 0));
|
||||
}
|
||||
|
||||
let previewStale = true;
|
||||
let forcePreviewRender = false;
|
||||
function renderNow() {
|
||||
const previewViewport = pendingPreviewViewport;
|
||||
pendingPreviewViewport = null;
|
||||
const openDetailsLines = uiState.mode === "markdown"
|
||||
? new Set([...preview.querySelectorAll("details.markdown-details[open][data-source-line]")].map(node => node.dataset.sourceLine))
|
||||
: new Set();
|
||||
if (uiState.mode === "markdown") {
|
||||
preview.classList.remove("preview--raw");
|
||||
preview.innerHTML = renderMarkdown(editor.value);
|
||||
preview.querySelectorAll("details.markdown-details[data-source-line]").forEach(node => {
|
||||
node.open = openDetailsLines.has(node.dataset.sourceLine);
|
||||
});
|
||||
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";
|
||||
}
|
||||
// In editor-only view this DOM is display:none. Building a full Markdown
|
||||
// tree (and highlighting / Mermaid) on each remote edit is wasted work.
|
||||
// Keep the exceptional consumers working: heading deep-links and PDF export.
|
||||
const headingRevealPending = uiState.mode === "markdown" && location.hash
|
||||
&& !/^#L[1-9]\d*$/i.test(location.hash)
|
||||
&& lastRevealedHeadingHash !== location.hash;
|
||||
const previewNeededForPdf = forcePreviewRender || document.querySelector("#pdf-export-dialog")?.open;
|
||||
const buildPreview = activeView() !== "edit" || previewNeededForPdf || headingRevealPending;
|
||||
forcePreviewRender = false;
|
||||
if (buildPreview) {
|
||||
pendingPreviewViewport = null;
|
||||
const openDetailsLines = uiState.mode === "markdown"
|
||||
? new Set([...preview.querySelectorAll("details.markdown-details[open][data-source-line]")].map(node => node.dataset.sourceLine))
|
||||
: new Set();
|
||||
if (uiState.mode === "markdown") {
|
||||
preview.classList.remove("preview--raw");
|
||||
preview.innerHTML = renderMarkdown(editor.value);
|
||||
preview.querySelectorAll("details.markdown-details[data-source-line]").forEach(node => {
|
||||
node.open = openDetailsLines.has(node.dataset.sourceLine);
|
||||
});
|
||||
scheduleAliasFileRefresh(editor.value);
|
||||
document.querySelector("#preview-label").textContent = "Preview (media / mermaid / markdown)";
|
||||
if (activeView() !== "edit" || previewNeededForPdf) {
|
||||
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";
|
||||
}
|
||||
previewStale = false;
|
||||
invalidatePreviewPositions();
|
||||
} else previewStale = true;
|
||||
mentionNavigator.render({ markdown: uiState.mode === "markdown" });
|
||||
alignPreviewLineNumbers(preview);
|
||||
if (buildPreview && previewLineToggle.checked && activeView() !== "edit") 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 (buildPreview) requestAnimationFrame(() => {
|
||||
if (previewViewport) restorePreviewViewport(previewViewport);
|
||||
else syncPreviewScroll();
|
||||
revealHeadingAnchor();
|
||||
@@ -1809,7 +1921,7 @@ export function startNoteEditor(adapter) {
|
||||
toolbarCollapseToggle?.addEventListener("click", () => { toolbarCollapsed = !toolbarCollapsed; localStorage.setItem(notePreferenceKey("toolbar-collapsed"), toolbarCollapsed ? "on" : "off"); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
navbarCollapseToggle?.addEventListener("click", () => { navbarCollapsed = !navbarCollapsed; localStorage.setItem(notePreferenceKey("navbar-collapsed"), navbarCollapsed ? "on" : "off"); if (navbarCollapsed) setHeaderMenuOpen(false); 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 }); });
|
||||
previewLineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("preview-line-numbers"), previewLineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); if (previewLineToggle.checked && activeView() !== "edit") alignPreviewLineNumbers(preview); scheduleEditorSettingsSave({ personal: true }); });
|
||||
compactToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("compact"), compactToggle.checked ? "on" : "off"); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
lineLinksToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("line-links"), lineLinksToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
fontFamily.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("font-family"), fontFamily.value); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
|
||||
@@ -2047,7 +2159,7 @@ export function startNoteEditor(adapter) {
|
||||
}
|
||||
}
|
||||
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(); }); });
|
||||
window.addEventListener("hashchange", () => { lastRevealedLineHash = ""; lastRevealedHeadingHash = ""; renderGutter(); if (activeView() === "edit" && uiState.mode === "markdown" && location.hash && !/^#L[1-9]\d*$/i.test(location.hash)) render(); requestAnimationFrame(() => { revealLinkedLine(); revealHeadingAnchor(); }); });
|
||||
preview.addEventListener("click", event => {
|
||||
const link = event.target instanceof Element ? event.target.closest('a[href^="#"]') : null;
|
||||
if (!link) return;
|
||||
@@ -2170,6 +2282,10 @@ export function startNoteEditor(adapter) {
|
||||
let pdfExportBound = false;
|
||||
let pdfExportLoad = null;
|
||||
pdfExportArgs.button?.addEventListener("click", async event => {
|
||||
if (previewStale) {
|
||||
forcePreviewRender = true;
|
||||
render();
|
||||
}
|
||||
if (pdfExportBound) return;
|
||||
event.stopImmediatePropagation();
|
||||
pdfExportLoad ||= import("@rustpad/pdf-export").then(({ bindPdfExport }) => {
|
||||
@@ -2489,9 +2605,14 @@ export function startNoteEditor(adapter) {
|
||||
toast.info("The global profile color is active again.", { title: "Profile color restored" });
|
||||
});
|
||||
editor.addEventListener("keydown", continueIndentation);
|
||||
let editorScrollFrame = 0;
|
||||
editor.addEventListener("scroll", () => {
|
||||
syncEditorLayers();
|
||||
syncPreviewScroll();
|
||||
if (editorScrollFrame) return;
|
||||
editorScrollFrame = requestAnimationFrame(() => {
|
||||
editorScrollFrame = 0;
|
||||
syncEditorLayers();
|
||||
syncPreviewScroll();
|
||||
});
|
||||
});
|
||||
editor.addEventListener("input", event => {
|
||||
if (!canEditDocument() && !applyingRemote) {
|
||||
|
||||
Reference in new issue
Block a user