page style

This commit is contained in:
Mateusz Gruszczyński
2026-07-31 11:22:00 +02:00
parent f7afee886b
commit 618e5ae0fb
5 changed files with 229 additions and 24 deletions
+134 -6
View File
@@ -32,8 +32,80 @@ 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 accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = 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 = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true, lastRevealedLineHash = "";
let editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false, connectionNoticeTimer = 0, connectionWasInterrupted = false;
const editHistory = {
entries: [],
index: -1,
lastKind: "",
lastRecordedAt: 0,
snapshot() {
return {
content: editor.value,
ownerMap: serializeAuthorship(authorship, editor.value.length),
selectionStart: editor.selectionStart,
selectionEnd: editor.selectionEnd,
selectionDirection: editor.selectionDirection,
scrollTop: editor.scrollTop,
scrollLeft: editor.scrollLeft,
};
},
reset() {
this.entries = [this.snapshot()];
this.index = 0;
this.lastKind = "";
this.lastRecordedAt = 0;
},
syncCurrent() {
if (this.index < 0) {
this.reset();
return;
}
this.entries[this.index] = this.snapshot();
},
record(inputType = "") {
const snapshot = this.snapshot();
if (this.index < 0) {
this.entries = [snapshot];
this.index = 0;
return;
}
if (this.entries[this.index]?.content === snapshot.content) {
this.entries[this.index] = snapshot;
return;
}
if (this.index < this.entries.length - 1) this.entries.splice(this.index + 1);
const kind = inputType === "insertText" || inputType === "insertCompositionText"
? "typing"
: inputType === "deleteContentBackward" || inputType === "deleteContentForward"
? "deleting"
: "action";
const now = Date.now();
const merge = kind !== "action" && kind === this.lastKind && now - this.lastRecordedAt < 900 && this.index > 0;
if (merge) this.entries[this.index] = snapshot;
else {
this.entries.push(snapshot);
this.index += 1;
if (this.entries.length > 100) {
this.entries.shift();
this.index -= 1;
}
}
this.lastKind = kind;
this.lastRecordedAt = now;
},
move(offset) {
const nextIndex = this.index + offset;
if (nextIndex < 0 || nextIndex >= this.entries.length) return false;
this.index = nextIndex;
this.lastKind = "";
this.lastRecordedAt = 0;
restoreHistorySnapshot(this.entries[this.index]);
return true;
},
undo() { return this.move(-1); },
redo() { return this.move(1); },
};
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
const singlePaneQuery = window.matchMedia("(max-width: 760px)");
let compactView = uiState.view === "preview" ? "preview" : "edit";
@@ -577,6 +649,29 @@ export function startNoteEditor(adapter) {
preview.scrollTop = ratio * previewRange;
}
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 · editable"; } 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…";
saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250);
}
function restoreHistorySnapshot(snapshot) {
if (!snapshot) return;
const maxOffset = snapshot.content.length;
const selectionStart = Math.min(snapshot.selectionStart ?? maxOffset, maxOffset);
const selectionEnd = Math.min(snapshot.selectionEnd ?? selectionStart, maxOffset);
applyingHistory = true;
editor.value = snapshot.content;
authorship = parseAuthorship(snapshot.content, snapshot.ownerMap);
previousContent = snapshot.content;
editor.setSelectionRange(selectionStart, selectionEnd, snapshot.selectionDirection || "none");
render();
editor.scrollTop = snapshot.scrollTop || 0;
editor.scrollLeft = snapshot.scrollLeft || 0;
syncEditorLayers();
applyingHistory = false;
editor.focus({ preventScroll: true });
scheduleDocumentSave();
}
function activeView() {
return singlePaneQuery.matches ? compactView : uiState.view;
}
@@ -621,6 +716,7 @@ export function startNoteEditor(adapter) {
if (content === editor.value) {
if (ownerMap != null) authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length);
previousContent = content;
editHistory.syncCurrent();
renderGutter();
requestAnimationFrame(revealLinkedLine);
return;
@@ -636,6 +732,7 @@ export function startNoteEditor(adapter) {
editor.scrollTop = scrollTop;
editor.scrollLeft = scrollLeft;
applyingRemote = false;
editHistory.reset();
render();
editor.scrollTop = scrollTop;
editor.scrollLeft = scrollLeft;
@@ -1062,6 +1159,29 @@ export function startNoteEditor(adapter) {
button.closest("details")?.removeAttribute("open");
});
});
function handleHistoryShortcut(event) {
const primary = event.ctrlKey || event.metaKey;
if (!primary || event.altKey) return;
const key = event.key.toLowerCase();
const undo = key === "z" && !event.shiftKey;
const redo = (key === "z" && event.shiftKey) || (key === "y" && !event.shiftKey);
if (!undo && !redo) return;
const target = event.target instanceof Element ? event.target : null;
if (target?.closest('.preview-editable[contenteditable="true"]')) return;
if (target && target !== editor && target.matches("input, textarea, select, [contenteditable='true']")) return;
if (document.querySelector("dialog[open]") && target !== editor) return;
event.preventDefault();
event.stopPropagation();
if (redo) editHistory.redo();
else editHistory.undo();
}
document.addEventListener("keydown", handleHistoryShortcut, true);
editor.addEventListener("beforeinput", event => {
if (event.inputType !== "historyUndo" && event.inputType !== "historyRedo") return;
event.preventDefault();
if (event.inputType === "historyRedo") editHistory.redo();
else editHistory.undo();
});
bindFormatShortcuts(editor);
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());
@@ -1151,12 +1271,21 @@ export function startNoteEditor(adapter) {
deletePreviewSelection();
});
const publishPageButton = document.querySelector("#publish-page");
const pageSettings = document.querySelector(".page-settings");
function updatePageControls() {
const enabled = publicPageEnabled.checked;
publishPageButton.disabled = !enabled;
publicTaskUpdates.disabled = !enabled;
unprotectPublicPage.disabled = !enabled;
pageSettings?.classList.toggle("is-enabled", enabled);
pageSettings?.querySelector("summary")?.setAttribute("title", enabled ? "Published page enabled" : "Published page disabled");
}
document.addEventListener("pointerdown", event => {
if (pageSettings?.open && !event.target.closest(".page-settings")) pageSettings.open = false;
}, { passive: true });
document.addEventListener("keydown", event => {
if (event.key === "Escape" && pageSettings?.open) pageSettings.open = false;
});
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked, publicPageEnabled.checked);
publicPageEnabled.addEventListener("change", async () => {
const previous = !publicPageEnabled.checked;
@@ -1211,17 +1340,16 @@ export function startNoteEditor(adapter) {
syncEditorLayers();
syncPreviewScroll();
});
editor.addEventListener("input", () => {
editor.addEventListener("input", event => {
const nextContent = editor.value;
authorship = adoptCurrentOwnerAliases(authorship, previousContent.length);
authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, currentOwner(), previousContent.length);
authorship = applyAuthorshipEdit(authorship, previousContent, nextContent, currentOwner());
previousContent = nextContent;
render();
if (applyingRemote) return;
clearTimeout(saveTimer);
document.querySelector("#save-state").textContent = "Saving…";
saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250);
if (applyingRemote || applyingHistory) return;
editHistory.record(event.inputType || "");
scheduleDocumentSave();
});
passwordDialog.addEventListener("cancel", event => {
if (info?.protected && !resourceUnlocked) {