fixes in styles

This commit is contained in:
Mateusz Gruszczyński
2026-08-03 12:32:55 +02:00
parent 55e58c4bff
commit 8b8b3b82b8
9 changed files with 536 additions and 89 deletions
+170 -21
View File
@@ -13,7 +13,7 @@ installGlobalDiagnostics();
import { applyAuthorshipEdit, authorshipOwners, lineAuthors, mapSelectionThroughEdit, parseAuthorship, renderAuthorshipLayer, replaceAuthorshipOwner, serializeAuthorship, syncAuthorshipLayer } from "@rustpad/authorship";
import { copyText } from "@rustpad/clipboard";
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
import { applyFormat, bindFormatShortcuts, bindIndentationShortcuts } from "@rustpad/editor-format";
import { bindEmojiPicker } from "@rustpad/emoji-picker";
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
@@ -27,12 +27,14 @@ 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 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;
let unreadChat = 0;
const compactToggle = document.querySelector("#compact-toggle"), lineLinksToggle = document.querySelector("#line-links-toggle"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), publicPageEnabled = document.querySelector("#public-page-enabled"), publicTaskUpdates = document.querySelector("#public-task-updates"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), mobileColorPicker = document.querySelector("#mobile-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
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 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 = "";
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 = "", lastServerContent = "", lastServerOwnerMap = "[]", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
const editHistory = {
entries: [],
@@ -95,6 +97,7 @@ export function startNoteEditor(adapter) {
this.lastRecordedAt = now;
},
move(offset) {
if (!canEditDocument()) return false;
const nextIndex = this.index + offset;
if (nextIndex < 0 || nextIndex >= this.entries.length) return false;
this.index = nextIndex;
@@ -109,6 +112,7 @@ export function startNoteEditor(adapter) {
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
const singlePaneQuery = window.matchMedia("(max-width: 760px)");
let compactView = uiState.view === "preview" ? "preview" : "edit";
let renderedView = singlePaneQuery.matches ? compactView : uiState.view;
let refreshFilesForAliases = () => { };
let aliasRefreshTimer = 0;
let lastUnresolvedAliasKey = "";
@@ -164,6 +168,16 @@ export function startNoteEditor(adapter) {
const controls = document.querySelector(".authorship-controls");
if (controls) controls.title = canManage ? "Global authorship settings" : "Only the owner can change authorship settings";
}
function setDocumentReadOnly(readOnly, label = "Read only") {
editor.readOnly = readOnly;
document.body.classList.toggle("document-read-only", readOnly);
if (!readOnly) return;
clearTimeout(saveTimer);
saveState.textContent = label;
const activePreviewEdit = preview.querySelector('.preview-editable[contenteditable="true"]');
if (activePreviewEdit) deactivatePreviewEdit(activePreviewEdit);
}
function canEditDocument() { return !editor.readOnly; }
function defaultColorFor(name) { let h = 0; for (const c of name || "?") h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 70% 62%)`; }
function ownerParts(owner) { const raw = String(owner || ""); const split = raw.lastIndexOf("\u001f"); return split < 0 ? { name: raw, color: "" } : { name: raw.slice(0, split), color: raw.slice(split + 1) }; }
function ownerName(owner) { return ownerParts(owner).name; }
@@ -517,7 +531,7 @@ export function startNoteEditor(adapter) {
}
function activatePreviewEdit(target, offset = null) {
if (!target) return;
if (!target || !canEditDocument()) return;
const active = preview.querySelector('.preview-editable[contenteditable="true"]');
if (active && active !== target) {
const targetIndex = [...preview.querySelectorAll(".preview-editable")].indexOf(target);
@@ -667,6 +681,7 @@ export function startNoteEditor(adapter) {
}
function deletePreviewSelection() {
if (!canEditDocument()) return false;
const range = previewSelectionSourceRange({ expandWholeLines: true });
if (!range || range.end <= range.start) return false;
editor.setRangeText("", range.start, range.end, "end");
@@ -687,21 +702,129 @@ export function startNoteEditor(adapter) {
return null;
}
function editorLineHeight() {
const lineHeight = parseFloat(getComputedStyle(editor).lineHeight);
return Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 20;
}
function scrollRatio(element) {
const range = Math.max(0, element.scrollHeight - element.clientHeight);
return range > 0 ? element.scrollTop / range : 0;
}
function editorScrollAnchor() {
return {
sourceLine: 1 + Math.max(0, editor.scrollTop) / editorLineHeight(),
ratio: scrollRatio(editor),
};
}
function previewLinePositions() {
const previewRect = preview.getBoundingClientRect();
const positions = [];
preview.querySelectorAll(".preview-source-line[data-source-line]").forEach(node => {
const sourceLine = Number(node.dataset.sourceLine);
if (!Number.isFinite(sourceLine) || sourceLine < 1 || !node.getClientRects().length) return;
const rect = node.getBoundingClientRect();
positions.push({
node,
sourceLine,
top: rect.top - previewRect.top + preview.scrollTop,
bottom: rect.bottom - previewRect.top + preview.scrollTop,
});
});
return positions;
}
function previewScrollAnchor() {
const positions = previewLinePositions();
if (!positions.length) return { sourceLine: null, ratio: scrollRatio(preview) };
const paddingTop = parseFloat(getComputedStyle(preview).paddingTop) || 0;
const viewportTop = preview.scrollTop + paddingTop;
let currentIndex = positions.findIndex(position => position.bottom > viewportTop + 0.5);
if (currentIndex < 0) currentIndex = positions.length - 1;
const current = positions[currentIndex];
const next = positions.slice(currentIndex + 1).find(position => position.sourceLine > current.sourceLine);
const progress = Math.max(0, Math.min(1, (viewportTop - current.top) / Math.max(1, current.bottom - current.top)));
const sourceLine = next
? current.sourceLine + progress * (next.sourceLine - current.sourceLine)
: current.sourceLine;
return { sourceLine, ratio: scrollRatio(preview) };
}
function activeScrollAnchor(view = renderedView) {
return view === "preview" ? previewScrollAnchor() : editorScrollAnchor();
}
function setScrollRatio(element, ratio) {
const range = Math.max(0, element.scrollHeight - element.clientHeight);
element.scrollTop = Math.max(0, Math.min(range, ratio * range));
}
function scrollEditorToAnchor(anchor) {
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);
syncEditorLayers();
}
function scrollPreviewToAnchor(anchor) {
const positions = previewLinePositions();
if (!Number.isFinite(anchor?.sourceLine) || !positions.length) {
setScrollRatio(preview, anchor?.ratio || 0);
return;
}
const sourceLine = Math.max(1, anchor.sourceLine);
const exact = positions.find(position => position.sourceLine === sourceLine);
let targetTop = exact?.top;
if (!Number.isFinite(targetTop)) {
const before = [...positions].reverse().find(position => position.sourceLine <= sourceLine);
const after = positions.find(position => position.sourceLine >= sourceLine);
if (before && after && after.sourceLine > before.sourceLine) {
const progress = (sourceLine - before.sourceLine) / (after.sourceLine - before.sourceLine);
targetTop = before.top + progress * (after.top - before.top);
} else targetTop = (before || after)?.top;
}
if (!Number.isFinite(targetTop)) {
setScrollRatio(preview, anchor.ratio || 0);
return;
}
const paddingTop = parseFloat(getComputedStyle(preview).paddingTop) || 0;
const range = Math.max(0, preview.scrollHeight - preview.clientHeight);
preview.scrollTop = Math.max(0, Math.min(range, targetTop - paddingTop));
}
function restoreScrollAnchor(anchor) {
requestAnimationFrame(() => {
const view = activeView();
if (view === "edit" || view === "split") scrollEditorToAnchor(anchor);
if (view === "preview" || view === "split") scrollPreviewToAnchor(anchor);
});
}
function applyUiPreservingScroll(options) {
const anchor = activeScrollAnchor();
applyUi(options);
restoreScrollAnchor(anchor);
}
function syncPreviewScroll() {
if (activeView() !== "split") return;
const editorRange = Math.max(0, editor.scrollHeight - editor.clientHeight);
const previewRange = Math.max(0, preview.scrollHeight - preview.clientHeight);
const ratio = editorRange > 0 ? editor.scrollTop / editorRange : 0;
preview.scrollTop = ratio * previewRange;
scrollPreviewToAnchor(editorScrollAnchor());
}
function render() { 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 scheduleDocumentSave() {
clearTimeout(saveTimer);
document.querySelector("#save-state").textContent = "Saving…";
if (!canEditDocument()) {
saveState.textContent = "Read only";
return;
}
saveState.textContent = "Saving…";
saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250);
}
function restoreHistorySnapshot(snapshot) {
if (!snapshot) return;
if (!snapshot || !canEditDocument()) return;
const maxOffset = snapshot.content.length;
const selectionStart = Math.min(snapshot.selectionStart ?? maxOffset, maxOffset);
const selectionEnd = Math.min(snapshot.selectionEnd ?? selectionStart, maxOffset);
@@ -724,6 +847,7 @@ export function startNoteEditor(adapter) {
function applyUi({ write = false, replace = false } = {}) {
const view = activeView();
renderedView = view;
editorWorkspace.className = `workspace view-${view} editor-workspace-font-${fontFamily.value}`;
editorWorkspace.style.setProperty("--editor-font-size", `${fontSize.value}px`);
document.body.classList.toggle("compact-editor", compactToggle.checked);
@@ -735,6 +859,7 @@ export function startNoteEditor(adapter) {
});
const markdown = uiState.mode === "markdown";
modeToggle.classList.toggle("active", markdown);
modeToggle.setAttribute("aria-pressed", String(markdown));
modeToggle.textContent = markdown ? "Markdown" : "Text";
render();
if (write) writeEditorState(uiState, { replace });
@@ -759,6 +884,8 @@ export function startNoteEditor(adapter) {
}
function applyRemote(content, ownerMap) {
lastServerContent = content;
if (ownerMap != null) lastServerOwnerMap = ownerMap;
if (content === editor.value) {
if (ownerMap != null) authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length);
previousContent = content;
@@ -809,14 +936,15 @@ export function startNoteEditor(adapter) {
resourceUnlocked = true;
if (passwordDialog.open) passwordDialog.close();
const readOnly = message.access_level === "read_only";
editor.readOnly = readOnly;
setDocumentReadOnly(readOnly);
accessLevel.textContent = readOnly ? "Access: read only" : "Access: full";
applyRemote(message.content, message.owner_map);
saveState.textContent = readOnly ? "Read only" : "Changes are saved automatically";
if (!readOnly) editor.focus();
},
onDocument: message => {
applyRemote(message.content, message.owner_map);
document.querySelector("#save-state").textContent = `${message.author ? `${message.author} · ` : ""}${new Date(message.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`;
saveState.textContent = editor.readOnly ? "Read only" : `${message.author ? `${message.author} · ` : ""}${new Date(message.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`;
},
onPresence: updatePresence,
onLatency: updateLatency,
@@ -829,9 +957,13 @@ export function startNoteEditor(adapter) {
if (/read-only access/i.test(message)) {
toast(friendly);
accessLevel.textContent = "Access: read only";
editor.readOnly = true;
setDocumentReadOnly(true, "Read only — changes not saved");
applyRemote(lastServerContent, lastServerOwnerMap);
queueMicrotask(connect);
return;
}
clearTimeout(saveTimer);
if (saveState.textContent === "Saving…") saveState.textContent = "Save failed";
if (/nickname|session|account/i.test(message)) {
if (!identityDialog.open) identityDialog.showModal();
} else if (info?.protected && !passwordDialog.open) {
@@ -886,16 +1018,19 @@ export function startNoteEditor(adapter) {
}
document.querySelectorAll("[data-view]").forEach(button => button.addEventListener("click", () => {
const anchor = activeScrollAnchor();
if (singlePaneQuery.matches) {
compactView = button.dataset.view === "preview" ? "preview" : "edit";
applyUi();
restoreScrollAnchor(anchor);
return;
}
uiState = { ...uiState, view: button.dataset.view };
applyUi({ write: true });
restoreScrollAnchor(anchor);
}));
singlePaneQuery.addEventListener("change", () => applyUi());
compactLayoutQuery.addEventListener("change", () => applyUi());
singlePaneQuery.addEventListener("change", () => applyUiPreservingScroll());
compactLayoutQuery.addEventListener("change", () => applyUiPreservingScroll());
const headerMenuToggle = document.querySelector("#header-menu-toggle");
const headerActions = document.querySelector("#header-actions");
const setHeaderMenuOpen = open => {
@@ -928,13 +1063,13 @@ export function startNoteEditor(adapter) {
document.addEventListener("keydown", event => {
if (event.key === "Escape" && mobileEditorOptions?.open) mobileEditorOptions.open = false;
});
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); });
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUiPreservingScroll({ write: 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(); applyUi(); 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(); applyUi(); scheduleEditorSettingsSave({ personal: true }); });
fontSize.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("font-size"), fontSize.value); syncMobileEditorControls(); applyUi(); scheduleEditorSettingsSave({ personal: true }); });
fontFamily.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("font-family"), fontFamily.value); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
fontSize.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("font-size"), fontSize.value); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); });
const mirrorMobileControl = (mobileControl, desktopControl) => mobileControl?.addEventListener("change", () => {
if (desktopControl instanceof HTMLInputElement && desktopControl.type === "checkbox") desktopControl.checked = mobileControl.checked;
else desktopControl.value = mobileControl.value;
@@ -1017,6 +1152,7 @@ export function startNoteEditor(adapter) {
window.addEventListener("resize", () => { if (mobileBubble?.style.left) placeMobileBubble(mobileBubble.getBoundingClientRect()); });
const cancelledPreviewEdits = new WeakSet();
function commitPreviewEdit(target, { focusNextLine = false } = {}) {
if (!canEditDocument()) { render(); return; }
const lineIndex = Number(target.dataset.sourceLine) - 1;
if (lineIndex < 0) return;
const value = markdownFromPreview(target);
@@ -1054,6 +1190,7 @@ export function startNoteEditor(adapter) {
}
}
function editRawPreviewLine(target) {
if (!canEditDocument()) return;
const lineIndex = Number(target.dataset.sourceLine) - 1;
const lines = editor.value.split("\n");
if (lineIndex < 0 || lineIndex >= lines.length) return;
@@ -1070,6 +1207,7 @@ export function startNoteEditor(adapter) {
}
function insertPreviewLineBreak(target) {
if (!canEditDocument()) return;
const lineIndex = Number(target.dataset.sourceLine) - 1;
if (lineIndex < 0) return;
const lines = editor.value.split("\n");
@@ -1162,7 +1300,7 @@ export function startNoteEditor(adapter) {
}
}
}
window.addEventListener("popstate", () => { lastRevealedLineHash = ""; uiState = readEditorState(); applyUi(); requestAnimationFrame(revealLinkedLine); });
window.addEventListener("popstate", () => { const anchor = activeScrollAnchor(); lastRevealedLineHash = ""; uiState = readEditorState(); applyUi(); restoreScrollAnchor(anchor); requestAnimationFrame(revealLinkedLine); });
window.addEventListener("hashchange", () => { lastRevealedLineHash = ""; renderGutter(); requestAnimationFrame(revealLinkedLine); });
window.addEventListener("rustpad:urlchange", updateAddressLabel);
gutter.addEventListener("click", async event => {
@@ -1230,6 +1368,7 @@ export function startNoteEditor(adapter) {
else editHistory.undo();
});
bindFormatShortcuts(editor);
bindIndentationShortcuts(editor, { size: 2 });
bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") });
document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal());
document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close());
@@ -1245,6 +1384,11 @@ export function startNoteEditor(adapter) {
preview.addEventListener("change", event => {
const checkbox = event.target.closest(".task-checkbox");
if (!checkbox) return;
if (!canEditDocument()) {
checkbox.checked = !checkbox.checked;
saveState.textContent = "Read only";
return;
}
const lineIndex = Number(checkbox.dataset.sourceLine) - 1;
const lines = editor.value.split("\n");
if (lineIndex < 0 || lineIndex >= lines.length) return;
@@ -1368,7 +1512,7 @@ export function startNoteEditor(adapter) {
authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, replacement, editor.value.length);
updateCurrentUser(); render();
socket?.setColor(noteColor);
if (socket) socket.update(editor.value, serializeAuthorship(authorship, editor.value.length));
if (socket && canEditDocument()) socket.update(editor.value, serializeAuthorship(authorship, editor.value.length));
}
userColorPicker.addEventListener("change", () => saveUserColor(userColorPicker.value));
mobileColorPicker?.addEventListener("change", () => saveUserColor(mobileColorPicker.value));
@@ -1382,7 +1526,7 @@ export function startNoteEditor(adapter) {
authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, replacement, editor.value.length);
updateCurrentUser(); render();
socket?.setColor(currentUserColor() || null);
if (socket) socket.update(editor.value, serializeAuthorship(authorship, editor.value.length));
if (socket && canEditDocument()) socket.update(editor.value, serializeAuthorship(authorship, editor.value.length));
toast("Global profile color restored");
});
editor.addEventListener("keydown", continueIndentation);
@@ -1391,6 +1535,11 @@ export function startNoteEditor(adapter) {
syncPreviewScroll();
});
editor.addEventListener("input", event => {
if (!canEditDocument() && !applyingRemote) {
applyRemote(lastServerContent, lastServerOwnerMap);
saveState.textContent = "Read only";
return;
}
const nextContent = editor.value;
authorship = adoptCurrentOwnerAliases(authorship, previousContent.length);
authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, currentOwner(), previousContent.length);