v0.2.96
This commit is contained in:
1 parent
b15a6c54ce
commit
d36a4b2451
22 files changed
+926
-114
No files matched your search
+132
-6
@@ -37,7 +37,7 @@ import { syncSelectControl } from "@rustpad/select";
|
||||
import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-libs";
|
||||
|
||||
export function startNoteEditor(adapter) {
|
||||
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer");
|
||||
const 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"), documentTitle = document.querySelector("#document-title");
|
||||
const modeToggle = document.querySelector("#mode-toggle"), toolbarCollapseToggle = document.querySelector("#toolbar-collapse-toggle"), navbarCollapseToggle = document.querySelector("#navbar-collapse-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
||||
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), mobileConnectionDetails = document.querySelector("#mobile-connection-details"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread"), connectionNotice = document.querySelector("#connection-notice"), connectionNoticeTitle = document.querySelector("#connection-notice-title"), connectionNoticeMessage = document.querySelector("#connection-notice-message");
|
||||
const saveState = document.querySelector("#save-state"), favoriteToggle = document.querySelector("#favorite-toggle");
|
||||
@@ -309,13 +309,93 @@ export function startNoteEditor(adapter) {
|
||||
render();
|
||||
}
|
||||
|
||||
function syncInlineTitleRename() {
|
||||
if (!documentTitle) return;
|
||||
const enabled = Boolean(adapter.renameTitle && info?.can_rename);
|
||||
documentTitle.classList.toggle("document-title--renamable", enabled);
|
||||
documentTitle.closest("#document-link-copy")?.classList.toggle("document-heading--has-renamable-title", enabled);
|
||||
if (enabled) documentTitle.title = t("editor.renameHint", {}, "Double-click to rename");
|
||||
else documentTitle.removeAttribute("title");
|
||||
}
|
||||
|
||||
function beginInlineTitleRename() {
|
||||
if (!documentTitle || !adapter.renameTitle || !info?.can_rename || documentTitle.querySelector("input")) return;
|
||||
const previous = String(info.title || documentTitle.textContent || "").trim();
|
||||
const input = document.createElement("input");
|
||||
input.className = "document-title-inline-input";
|
||||
input.type = "text";
|
||||
input.maxLength = 80;
|
||||
input.value = previous;
|
||||
input.setAttribute("aria-label", t("editor.renameLabel", {}, "Note name"));
|
||||
documentTitle.replaceChildren(input);
|
||||
documentTitle.classList.add("document-title--editing");
|
||||
let finished = false;
|
||||
|
||||
const restore = title => {
|
||||
documentTitle.classList.remove("document-title--editing");
|
||||
documentTitle.textContent = title;
|
||||
syncInlineTitleRename();
|
||||
};
|
||||
const finish = async save => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
const next = input.value.trim();
|
||||
if (!save) { restore(previous); return; }
|
||||
if (!next) {
|
||||
restore(previous);
|
||||
return;
|
||||
}
|
||||
if (next === previous) { restore(previous); return; }
|
||||
input.disabled = true;
|
||||
try {
|
||||
const result = await adapter.renameTitle(sessionHeaders(), next);
|
||||
info = { ...info, title: result.title || next, updated_at: result.updated_at || info.updated_at };
|
||||
restore(info.title);
|
||||
document.title = adapter.title(info);
|
||||
toast.success(
|
||||
t("editor.renamed", { title: info.title }, `Renamed to “${info.title}”.`),
|
||||
{ title: t("editor.renamedTitle", {}, "Note renamed") },
|
||||
);
|
||||
} catch (error) {
|
||||
restore(previous);
|
||||
toast.danger(error.message, { title: t("editor.renameFailed", {}, "Could not rename note") });
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void finish(true);
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void finish(false);
|
||||
}
|
||||
});
|
||||
input.addEventListener("blur", () => { void finish(true); }, { once: true });
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
|
||||
documentTitle?.addEventListener("click", event => {
|
||||
if (!info?.can_rename) return;
|
||||
event.stopPropagation();
|
||||
});
|
||||
documentTitle?.addEventListener("dblclick", event => {
|
||||
if (!info?.can_rename) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
beginInlineTitleRename();
|
||||
});
|
||||
|
||||
async function loadNoteInfo() {
|
||||
info = await adapter.loadInfo(sessionHeaders());
|
||||
const documentTitle = document.querySelector("#document-title");
|
||||
if (documentTitle && info.title) {
|
||||
documentTitle.removeAttribute("data-i18n");
|
||||
documentTitle.textContent = info.title;
|
||||
}
|
||||
syncInlineTitleRename();
|
||||
globalColor = info.global_color || ""; noteColor = info.note_color || "";
|
||||
if (!getAuthToken()) noteColor = readGuestColor();
|
||||
updateMarkdownFiles(info.files || []);
|
||||
@@ -336,8 +416,7 @@ export function startNoteEditor(adapter) {
|
||||
syncMobileEditorControls();
|
||||
updateCurrentUser(); return info;
|
||||
}
|
||||
// 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 || {});
|
||||
@@ -506,6 +585,7 @@ export function startNoteEditor(adapter) {
|
||||
const renderVersion = ++mermaidRenderVersion;
|
||||
const nodes = [...preview.querySelectorAll(".mermaid")];
|
||||
if (!nodes.length) return;
|
||||
preview.querySelectorAll(".mermaid-error").forEach(error => error.remove());
|
||||
try {
|
||||
nodes.forEach(node => {
|
||||
if (!node.dataset.mermaidSource) node.dataset.mermaidSource = node.textContent || "";
|
||||
@@ -513,10 +593,12 @@ export function startNoteEditor(adapter) {
|
||||
const mermaid = await loadMermaid();
|
||||
mermaid.initialize({ startOnLoad: false, theme: getTheme() === "dark" ? "dark" : "default", securityLevel: "strict" });
|
||||
await mermaid.run({ nodes });
|
||||
if (renderVersion === mermaidRenderVersion) preview.querySelectorAll(".mermaid-error").forEach(error => error.remove());
|
||||
} catch {
|
||||
if (renderVersion !== mermaidRenderVersion) return;
|
||||
nodes.forEach(node => {
|
||||
if (!node.isConnected || !preview.contains(node) || !node.parentNode) return;
|
||||
if (node.previousElementSibling?.classList.contains("mermaid-error")) return;
|
||||
const message = document.createElement("p");
|
||||
message.className = "error mermaid-error";
|
||||
message.textContent = t("public.mermaidFailed", {}, "Failed to load Mermaid.");
|
||||
@@ -552,8 +634,6 @@ export function startNoteEditor(adapter) {
|
||||
reconcileChildren(participantBadges, nodes);
|
||||
}
|
||||
function syncOwnerLabels() {
|
||||
// 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)`;
|
||||
@@ -2268,6 +2348,7 @@ export function startNoteEditor(adapter) {
|
||||
const documentLinkCopy = document.querySelector("#document-link-copy");
|
||||
documentLinkCopy?.addEventListener("click", copyCurrentLink);
|
||||
documentLinkCopy?.addEventListener("keydown", event => {
|
||||
if (event.target instanceof Element && event.target.closest(".document-title--editing, input, textarea")) return;
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
copyCurrentLink();
|
||||
@@ -2306,6 +2387,51 @@ export function startNoteEditor(adapter) {
|
||||
renderFavoriteButton(false);
|
||||
});
|
||||
|
||||
function ensurePdfFormatButtons() {
|
||||
const menu = document.querySelector(".markdown-more-menu");
|
||||
if (!menu) return;
|
||||
const formats = [
|
||||
["page-break", "editor.pageBreak", "Page break (PDF)"],
|
||||
["pdf-header", "editor.pdfHeaderTag", "PDF header"],
|
||||
["pdf-footer", "editor.pdfFooterTag", "PDF footer"],
|
||||
["pdf-align-top", "editor.pdfAlignTopTag", "PDF align top"],
|
||||
["pdf-align-center", "editor.pdfAlignCenterTag", "PDF align center"],
|
||||
];
|
||||
const tocButton = menu.querySelector('[data-format="toc"]');
|
||||
let insertAfter = tocButton;
|
||||
for (const [format, key, fallback] of formats) {
|
||||
let button = menu.querySelector(`[data-format="${format}"]`);
|
||||
if (!button) {
|
||||
button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.format = format;
|
||||
button.dataset.i18n = key;
|
||||
button.textContent = t(key, {}, fallback);
|
||||
if (format === "page-break") {
|
||||
button.dataset.i18nTitle = "editor.pageBreakShortcut";
|
||||
button.title = t("editor.pageBreakShortcut", {}, "Page break (PDF) · Ctrl/Cmd+Enter");
|
||||
}
|
||||
if (insertAfter?.parentNode === menu) insertAfter.after(button);
|
||||
else menu.prepend(button);
|
||||
}
|
||||
insertAfter = button;
|
||||
}
|
||||
|
||||
let separator = menu.querySelector(".markdown-more-separator");
|
||||
if (!separator) {
|
||||
separator = document.createElement("div");
|
||||
separator.className = "markdown-more-separator";
|
||||
separator.setAttribute("role", "separator");
|
||||
separator.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
menu.append(separator);
|
||||
for (const [format] of formats) {
|
||||
const button = menu.querySelector(`[data-format="${format}"]`);
|
||||
if (button) menu.append(button);
|
||||
}
|
||||
}
|
||||
|
||||
ensurePdfFormatButtons();
|
||||
let pendingPreviewFormatRange = null;
|
||||
document.querySelectorAll("[data-format]").forEach(button => {
|
||||
button.addEventListener("pointerdown", event => {
|
||||
|
||||
Reference in new issue
Block a user