fixes in styles
This commit is contained in:
@@ -109,3 +109,76 @@ export function bindFormatShortcuts(editor) {
|
||||
applyFormat(editor, format);
|
||||
});
|
||||
}
|
||||
|
||||
function selectedLineRange(value, start, end) {
|
||||
const blockStart = value.lastIndexOf("\n", Math.max(0, start - 1)) + 1;
|
||||
let blockEnd = value.indexOf("\n", end);
|
||||
if (blockEnd < 0) blockEnd = value.length;
|
||||
if (end > start && value[end - 1] === "\n") blockEnd = end - 1;
|
||||
return { blockStart, blockEnd };
|
||||
}
|
||||
|
||||
function mapOffsetThroughEdits(offset, edits) {
|
||||
let delta = 0;
|
||||
for (const edit of edits) {
|
||||
if (offset < edit.start) break;
|
||||
const editEnd = edit.start + edit.remove;
|
||||
if (offset <= editEnd) return edit.start + delta + edit.insert.length;
|
||||
delta += edit.insert.length - edit.remove;
|
||||
}
|
||||
return offset + delta;
|
||||
}
|
||||
|
||||
export function applyIndentation(editor, { outdent = false, size = 2 } = {}) {
|
||||
if (!editor || editor.readOnly) return false;
|
||||
const indent = " ".repeat(Math.max(1, Number(size) || 2));
|
||||
const start = editor.selectionStart;
|
||||
const end = editor.selectionEnd;
|
||||
const direction = editor.selectionDirection || "none";
|
||||
|
||||
if (!outdent && start === end) {
|
||||
editor.setRangeText(indent, start, end, "end");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const value = editor.value;
|
||||
const { blockStart, blockEnd } = selectedLineRange(value, start, end);
|
||||
const block = value.slice(blockStart, blockEnd);
|
||||
const lineStarts = [0];
|
||||
for (let index = 0; index < block.length; index++) {
|
||||
if (block[index] === "\n") lineStarts.push(index + 1);
|
||||
}
|
||||
|
||||
const edits = lineStarts.map(relativeStart => {
|
||||
const absoluteStart = blockStart + relativeStart;
|
||||
if (!outdent) return { start: absoluteStart, remove: 0, insert: indent };
|
||||
const line = value.slice(absoluteStart, value.indexOf("\n", absoluteStart) < 0 ? value.length : value.indexOf("\n", absoluteStart));
|
||||
const removable = line.startsWith("\t") ? 1 : Math.min(indent.length, (line.match(/^ +/) || [""])[0].length);
|
||||
return { start: absoluteStart, remove: removable, insert: "" };
|
||||
}).filter(edit => edit.remove || edit.insert);
|
||||
|
||||
if (!edits.length) return false;
|
||||
let replacement = block;
|
||||
for (let index = edits.length - 1; index >= 0; index--) {
|
||||
const edit = edits[index];
|
||||
const relativeStart = edit.start - blockStart;
|
||||
replacement = `${replacement.slice(0, relativeStart)}${edit.insert}${replacement.slice(relativeStart + edit.remove)}`;
|
||||
}
|
||||
|
||||
const nextStart = mapOffsetThroughEdits(start, edits);
|
||||
const nextEnd = mapOffsetThroughEdits(end, edits);
|
||||
editor.setRangeText(replacement, blockStart, blockEnd, "start");
|
||||
editor.setSelectionRange(nextStart, nextEnd, direction);
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
export function bindIndentationShortcuts(editor, { size = 2 } = {}) {
|
||||
editor.addEventListener("keydown", event => {
|
||||
if (event.key !== "Tab" || event.ctrlKey || event.metaKey || event.altKey) return;
|
||||
if (editor.readOnly) return;
|
||||
event.preventDefault();
|
||||
applyIndentation(editor, { outdent: event.shiftKey, size });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ export function alignPreviewLineNumbers(root) {
|
||||
const targetLeft = parseFloat(styles.paddingLeft || "0") - 50;
|
||||
const rootLeft = root.getBoundingClientRect().left;
|
||||
root.querySelectorAll(".preview-source-line").forEach(line => {
|
||||
const lineLeft = line.getBoundingClientRect().left - rootLeft;
|
||||
const lineLeft = line.getBoundingClientRect().left - rootLeft + root.scrollLeft;
|
||||
line.style.setProperty("--preview-line-left", `${targetLeft - lineLeft}px`);
|
||||
});
|
||||
}
|
||||
|
||||
+170
-21
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user