new functions
This commit is contained in:
@@ -4698,3 +4698,102 @@ dialog::backdrop {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Compact Page settings dropdown. */
|
||||
.page-settings { position: relative; }
|
||||
.page-settings > summary { list-style: none; cursor: pointer; }
|
||||
.page-settings > summary::-webkit-details-marker { display: none; }
|
||||
.page-settings-menu { position: absolute; z-index: 30; top: calc(100% + 6px); right: 0; display: grid; gap: 4px; min-width: 220px; padding: 8px; border: 1px solid var(--border); border-radius: 9px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.28); }
|
||||
.page-settings-menu .public-task-toggle { min-height: 30px; padding: 5px 7px; border-radius: 6px; }
|
||||
.page-settings-menu .public-task-toggle:hover { background: color-mix(in srgb, var(--surface-strong, #262b35) 72%, transparent); }
|
||||
#publish-page:disabled { opacity: .45; cursor: not-allowed; }
|
||||
|
||||
/* Keep the Simple/Full switch inside the editor label frame. */
|
||||
.authorship-mode-control { padding: 1px; border-radius: 7px; }
|
||||
.authorship-mode-control button { min-height: 20px; height: 20px; padding: 0 7px; border-radius: 5px; line-height: 20px; }
|
||||
|
||||
/* Make secondary editor actions read clearly as buttons. */
|
||||
.markdown-more > summary,
|
||||
.pad-page .toolbar-action { display: inline-flex; align-items: center; justify-content: center; min-height: 30px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-strong, #262b35); color: var(--text); font-weight: 600; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,.04); }
|
||||
.markdown-more > summary:hover,
|
||||
.pad-page .toolbar-action:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); filter: brightness(1.08); }
|
||||
.markdown-more > summary { list-style: none; }
|
||||
.markdown-more > summary::-webkit-details-marker { display: none; }
|
||||
|
||||
|
||||
/* Final UI fixes: opaque Page settings and per-note authorship controls. */
|
||||
.page-settings-menu {
|
||||
background: #171c24;
|
||||
opacity: 1;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.authorship-controls { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.authorship-color-toggle { display: inline-flex; align-items: center; gap: 5px; min-height: 22px; color: var(--muted); font-size: 11px; cursor: pointer; }
|
||||
.authorship-color-toggle input { width: 28px; height: 16px; margin: 0; accent-color: var(--accent); }
|
||||
.authorship-layer { overflow: hidden; padding: 0; }
|
||||
.authorship-canvas { position: absolute; top: 0; left: 0; box-sizing: border-box; color: transparent; white-space: pre; tab-size: 4; will-change: transform; }
|
||||
.share-link-row .share-link-info { justify-self: end; width: min(100%, 520px); text-align: right; }
|
||||
.share-link-row .share-link-inline { width: 100%; text-align: left; }
|
||||
@media (max-width: 800px) {
|
||||
.share-link-row .share-link-info { justify-self: stretch; width: 100%; text-align: left; }
|
||||
.authorship-controls { max-width: 100%; gap: 5px; }
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle { background: #171c24; }
|
||||
.page-settings-menu .public-task-toggle:hover { background: #242b36; }
|
||||
|
||||
/* Shared editor display settings */
|
||||
.authorship-controls { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
||||
.switch-control { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 11px; color: var(--muted); user-select: none; }
|
||||
.switch-control input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.switch-control__track { position: relative; width: 30px; height: 16px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-strong, #262b35); transition: .15s ease; }
|
||||
.switch-control__track::after { content: ""; position: absolute; width: 10px; height: 10px; left: 2px; top: 2px; border-radius: 50%; background: var(--muted); transition: .15s ease; }
|
||||
.switch-control input:checked + .switch-control__track { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 28%, var(--surface-strong, #262b35)); }
|
||||
.switch-control input:checked + .switch-control__track::after { transform: translateX(14px); background: var(--accent); }
|
||||
.editor-settings-save { min-height: 22px; height: 22px; padding: 0 8px; font-size: 11px; }
|
||||
.editor-settings-save:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.share-link-row .share-link-info { margin-top: 8px; justify-self: start; text-align: left; }
|
||||
|
||||
.preview-editable--source {
|
||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
||||
/* Keep generated individual links below the new-link form. */
|
||||
.share-link-list-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.share-link-list-wrap h5 {
|
||||
margin: 0;
|
||||
font-size: .86rem;
|
||||
}
|
||||
.share-link-list {
|
||||
width: 100%;
|
||||
}
|
||||
.share-link-row {
|
||||
grid-template-columns: minmax(130px, auto) 100px auto auto;
|
||||
align-items: end;
|
||||
}
|
||||
.share-link-row .share-link-info {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
justify-self: stretch;
|
||||
text-align: left;
|
||||
margin-top: 0;
|
||||
}
|
||||
.share-link-row .share-link-inline {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.share-link-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.share-link-row .share-link-info {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-17
@@ -175,31 +175,33 @@ export function renderAuthorshipLayer(layer, editor, model, colorFor) {
|
||||
if (!layer) return;
|
||||
const style = getComputedStyle(editor);
|
||||
layer.style.left = `${editor.offsetLeft}px`;
|
||||
layer.style.paddingTop = style.paddingTop;
|
||||
layer.style.paddingRight = style.paddingRight;
|
||||
layer.style.paddingBottom = style.paddingBottom;
|
||||
layer.style.paddingLeft = style.paddingLeft;
|
||||
layer.style.fontFamily = style.fontFamily;
|
||||
layer.style.fontSize = style.fontSize;
|
||||
layer.style.fontWeight = style.fontWeight;
|
||||
layer.style.lineHeight = style.lineHeight;
|
||||
layer.style.letterSpacing = style.letterSpacing;
|
||||
const canvas = document.createElement("div");
|
||||
canvas.className = "authorship-canvas";
|
||||
canvas.style.paddingTop = style.paddingTop;
|
||||
canvas.style.paddingRight = style.paddingRight;
|
||||
canvas.style.paddingBottom = style.paddingBottom;
|
||||
canvas.style.paddingLeft = style.paddingLeft;
|
||||
canvas.style.fontFamily = style.fontFamily;
|
||||
canvas.style.fontSize = style.fontSize;
|
||||
canvas.style.fontWeight = style.fontWeight;
|
||||
canvas.style.lineHeight = style.lineHeight;
|
||||
canvas.style.letterSpacing = style.letterSpacing;
|
||||
canvas.style.minWidth = `${editor.scrollWidth}px`;
|
||||
canvas.style.minHeight = `${editor.scrollHeight}px`;
|
||||
const text = editor.value;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let cursor = 0;
|
||||
for (const span of normalize(model?.spans, text.length)) {
|
||||
if (span.start > cursor) fragment.append(document.createTextNode(text.slice(cursor, span.start)));
|
||||
if (span.start > cursor) canvas.append(document.createTextNode(text.slice(cursor, span.start)));
|
||||
const mark = document.createElement("span");
|
||||
mark.className = "authorship-fragment";
|
||||
mark.style.setProperty("--owner", colorFor(span.owner));
|
||||
mark.textContent = text.slice(span.start, span.end);
|
||||
mark.title = span.owner.split("\u001f", 1)[0];
|
||||
fragment.append(mark);
|
||||
canvas.append(mark);
|
||||
cursor = span.end;
|
||||
}
|
||||
if (cursor < text.length) fragment.append(document.createTextNode(text.slice(cursor)));
|
||||
if (!text.endsWith("\n")) fragment.append(document.createTextNode("\n"));
|
||||
layer.replaceChildren(fragment);
|
||||
layer.scrollTop = editor.scrollTop;
|
||||
layer.scrollLeft = editor.scrollLeft;
|
||||
if (cursor < text.length) canvas.append(document.createTextNode(text.slice(cursor)));
|
||||
if (!text.endsWith("\n")) canvas.append(document.createTextNode("\n"));
|
||||
canvas.style.transform = `translate3d(${-editor.scrollLeft}px, ${-editor.scrollTop}px, 0)`;
|
||||
layer.replaceChildren(canvas);
|
||||
}
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ async function loadResources() {
|
||||
<label><span>Valid for</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" inputmode="numeric"><span>hours</span></div></label>
|
||||
<label class="share-forever"><input name="forever" type="checkbox"><span>Never expires</span></label>
|
||||
<button class="primary-button" type="submit">Create link</button>
|
||||
</form><div data-link-list class="share-list"></div></section>
|
||||
</form><div class="share-link-list-wrap"><h5>Individual links</h5><div data-link-list class="share-list share-link-list"></div></div></section>
|
||||
</div>
|
||||
<div class="share-dialog-footer"><p class="form-message resource-inline-message" data-inline-message role="status"></p><button class="secondary-button" type="button" data-done>Done</button></div>
|
||||
</div>`;
|
||||
|
||||
@@ -17,6 +17,7 @@ export function createPadAdapter() {
|
||||
loadInfo: headers => api(base, { headers }),
|
||||
loadColor: headers => api(`${base}/editor-color`, { headers }),
|
||||
saveColor: (headers, color) => api(`${base}/editor-color`, { method: "POST", headers, body: JSON.stringify({ color }) }),
|
||||
saveEditorSettings: (headers, settings) => api(`${base}/editor-settings`, { method: "POST", headers, body: JSON.stringify(settings) }),
|
||||
fileEndpoints: {
|
||||
list: `${base}/files`,
|
||||
upload: `${base}/files`,
|
||||
@@ -27,9 +28,9 @@ export function createPadAdapter() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ kind: "pad", slug, password }),
|
||||
}),
|
||||
publish: (accessToken, allowTaskUpdates, unprotectPage) => api(`${base}/publish`, {
|
||||
publish: (accessToken, allowTaskUpdates, unprotectPage, enabled = true) => api(`${base}/publish`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage }),
|
||||
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage, enabled }),
|
||||
}),
|
||||
loadHistory: accessToken => api(`${base}/history`, {
|
||||
method: "POST",
|
||||
@@ -57,6 +58,7 @@ export function createWorkspaceNoteAdapter() {
|
||||
loadInfo: headers => api(base, { headers }),
|
||||
loadColor: headers => api(`${base}/editor-color`, { headers }),
|
||||
saveColor: (headers, color) => api(`${base}/editor-color`, { method: "POST", headers, body: JSON.stringify({ color }) }),
|
||||
saveEditorSettings: (headers, settings) => api(`${base}/editor-settings`, { method: "POST", headers, body: JSON.stringify(settings) }),
|
||||
fileEndpoints: {
|
||||
list: `${base}/files`,
|
||||
upload: `${base}/files`,
|
||||
@@ -67,9 +69,9 @@ export function createWorkspaceNoteAdapter() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ kind: "workspace", slug: workspaceSlug, password }),
|
||||
}),
|
||||
publish: (accessToken, allowTaskUpdates, unprotectPage) => api(`${base}/publish`, {
|
||||
publish: (accessToken, allowTaskUpdates, unprotectPage, enabled = true) => api(`${base}/publish`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage }),
|
||||
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage, enabled }),
|
||||
}),
|
||||
loadHistory: accessToken => api(`${base}/history`, {
|
||||
method: "POST",
|
||||
|
||||
+139
-15
@@ -17,9 +17,10 @@ export function startNoteEditor(adapter) {
|
||||
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"), 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");
|
||||
let unreadChat = 0;
|
||||
const compactToggle = document.querySelector("#compact-toggle"), 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"), useGlobalColorButton = document.querySelector("#use-global-color");
|
||||
const compactToggle = document.querySelector("#compact-toggle"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), saveEditorSettingsButton = document.querySelector("#save-editor-settings"), 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"), useGlobalColorButton = document.querySelector("#use-global-color");
|
||||
const shareToken = new URLSearchParams(location.search).get("share"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
|
||||
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 = ["full", "advanced"].includes(localStorage.getItem("rustpad:authorship-mode")) ? "full" : "simple";
|
||||
const notePreferenceKey = name => `rustpad:${name}:${adapter.access.kind}:${adapter.access.key}`;
|
||||
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;
|
||||
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
|
||||
let compactView = uiState.view === "preview" ? "preview" : "edit";
|
||||
const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem("rustpad:line-numbers") !== "off";
|
||||
@@ -27,6 +28,12 @@ export function startNoteEditor(adapter) {
|
||||
compactToggle.checked = localStorage.getItem("rustpad:compact") !== "off";
|
||||
fontFamily.value = localStorage.getItem("rustpad:font-family") || "mono";
|
||||
fontSize.value = localStorage.getItem("rustpad:font-size") || "14";
|
||||
authorshipColorsToggle.checked = authorshipColorsEnabled;
|
||||
function updateAuthorshipControls() {
|
||||
authorshipColorsToggle.checked = authorshipColorsEnabled;
|
||||
authorshipColorsLabel.textContent = authorshipColorsEnabled ? "Colors on" : "Colors off";
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode));
|
||||
}
|
||||
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; }
|
||||
@@ -59,6 +66,10 @@ export function startNoteEditor(adapter) {
|
||||
} else {
|
||||
noteColor = readGuestColor();
|
||||
}
|
||||
authorshipMode = info.authorship_mode === "full" ? "full" : "simple";
|
||||
authorshipColorsEnabled = info.colors_enabled !== false;
|
||||
updateAuthorshipControls();
|
||||
if (saveEditorSettingsButton) saveEditorSettingsButton.disabled = !info.can_save_editor_settings;
|
||||
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 = `${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 || "Guest"; li.title = label.textContent; li.append(dot, label); return li; })); if (!entries.length) { const li = document.createElement("li"); li.textContent = "No active users"; roomUsers.append(li); } renderGutter(); }
|
||||
@@ -95,12 +106,12 @@ 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);
|
||||
const showAuthorship = owners.length > 0;
|
||||
const showAuthorship = authorshipColorsEnabled && owners.length > 0;
|
||||
const authorsByLine = showAuthorship ? lineAuthors(editor.value, authorship) : [];
|
||||
const full = authorshipMode === "full";
|
||||
authorshipLayer.hidden = !showAuthorship;
|
||||
ownerLabels.hidden = !full || !showAuthorship;
|
||||
renderParticipantBadges(owners);
|
||||
renderParticipantBadges(authorshipColorsEnabled ? owners : []);
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode));
|
||||
editorWorkspace.dataset.authorshipMode = authorshipMode;
|
||||
const style = getComputedStyle(editor), lineHeight = parseFloat(style.lineHeight) || 29, paddingTop = parseFloat(style.paddingTop) || 24, paddingBottom = parseFloat(style.paddingBottom) || 24;
|
||||
@@ -146,10 +157,10 @@ export function startNoteEditor(adapter) {
|
||||
const title = current.getAttribute("title");
|
||||
return `}"` : ""})`;
|
||||
}
|
||||
if (tag === "br") return " ";
|
||||
if (tag === "br") return "\n";
|
||||
return body;
|
||||
};
|
||||
return [...node.childNodes].map(walk).join("").replace(/\n/g, " ").trim();
|
||||
return [...node.childNodes].map(walk).join("").replace(/\u00a0/g, " ");
|
||||
}
|
||||
|
||||
function previewCaretOffset(target) {
|
||||
@@ -219,11 +230,6 @@ export function startNoteEditor(adapter) {
|
||||
editorWorkspace.style.setProperty("--editor-font-size", `${fontSize.value}px`);
|
||||
document.body.classList.toggle("compact-editor", compactToggle.checked);
|
||||
document.body.classList.toggle("compact-note-layout", compactLayoutQuery.matches);
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.addEventListener("click", () => {
|
||||
authorshipMode = button.dataset.authorshipMode === "full" ? "full" : "simple";
|
||||
localStorage.setItem("rustpad:authorship-mode", authorshipMode);
|
||||
renderGutter();
|
||||
}));
|
||||
document.querySelectorAll("[data-view]").forEach(button => {
|
||||
const active = button.dataset.view === view;
|
||||
button.classList.toggle("active", active);
|
||||
@@ -274,7 +280,7 @@ export function startNoteEditor(adapter) {
|
||||
accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key);
|
||||
await loadNoteInfo();
|
||||
document.title = adapter.title(info);
|
||||
publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected);
|
||||
publicPageEnabled.checked = Boolean(info.public_page_enabled); publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected); updatePageControls();
|
||||
adapter.configureView?.(info);
|
||||
applyUi({ write: true, replace: true });
|
||||
updateCurrentUser();
|
||||
@@ -394,9 +400,127 @@ export function startNoteEditor(adapter) {
|
||||
mobileBubbleDrag.addEventListener("pointercancel", end);
|
||||
});
|
||||
window.addEventListener("resize", () => { if (mobileBubble?.style.left) placeMobileBubble(mobileBubble.getBoundingClientRect()); });
|
||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); }); window.addEventListener("rustpad:urlchange", updateAddressLabel); document.querySelector("#copy-link").addEventListener("click", async () => { try { await copyText(currentShareUrl(uiState)); toast("Link copied"); } catch (e) { toast(e.message); } }); document.querySelectorAll("[data-format]").forEach(b => b.addEventListener("click", () => { applyFormat(editor, b.dataset.format); b.closest("details")?.removeAttribute("open"); })); 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()); document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close()); preview.addEventListener("change", event => { const checkbox = event.target.closest(".task-checkbox"); if (!checkbox) return; const lineIndex = Number(checkbox.dataset.sourceLine) - 1; const lines = editor.value.split("\n"); if (lineIndex < 0 || lineIndex >= lines.length) return; lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`); editor.value = lines.join("\n"); editor.dispatchEvent(new Event("input", { bubbles: true })); }); preview.addEventListener("keydown", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (event.key === "Enter") { event.preventDefault(); target.blur(); return; } if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (movePreviewCaret(target, event.key === "ArrowUp" ? -1 : 1)) event.preventDefault(); } }); preview.addEventListener("blur", event => { const target = event.target.closest(".preview-editable"); if (!target) return; const lineIndex = Number(target.dataset.sourceLine) - 1; if (lineIndex < 0) return; const lines = editor.value.split("\n"); const value = markdownFromPreview(target); let next; if (target.dataset.tableCell !== undefined) next = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value); else { const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || ""; next = prefix + value + suffix; } if (lines[lineIndex] === next) return; lines[lineIndex] = next; editor.value = lines.join("\n"); editor.setSelectionRange(editor.value.length, editor.value.length); editor.dispatchEvent(new Event("input", { bubbles: true })); }, { capture: true });
|
||||
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked);
|
||||
publicTaskUpdates.addEventListener("change", async () => { publicTaskUpdates.disabled = true; try { await savePublicOptions(); toast(publicTaskUpdates.checked ? "Public task updates enabled" : "Public task updates disabled"); } catch (error) { publicTaskUpdates.checked = !publicTaskUpdates.checked; toast(error.message); } finally { publicTaskUpdates.disabled = false; } }); unprotectPublicPage.addEventListener("change", async () => { unprotectPublicPage.disabled = true; try { await savePublicOptions(); toast(unprotectPublicPage.checked ? "Published page is now unprotected" : "Published page protection enabled"); } catch (error) { unprotectPublicPage.checked = !unprotectPublicPage.checked; toast(error.message); } finally { unprotectPublicPage.disabled = false; } }); document.querySelector("#publish-page").addEventListener("click", async () => { try { const result = await savePublicOptions(); const url = new URL(result.url, location.origin).href; await copyText(url); toast("Page link copied"); window.open(url, "_blank", "noopener"); } catch (error) { toast(error.message); } });
|
||||
const cancelledPreviewEdits = new WeakSet();
|
||||
function commitPreviewEdit(target, { focusNextLine = false } = {}) {
|
||||
const lineIndex = Number(target.dataset.sourceLine) - 1;
|
||||
if (lineIndex < 0) return;
|
||||
const value = markdownFromPreview(target);
|
||||
const lines = editor.value.split("\n");
|
||||
if (target.dataset.rawSourceEdit === "true") {
|
||||
if (value === lines[lineIndex]) return;
|
||||
lines[lineIndex] = value.replace(/\n/g, "");
|
||||
editor.value = lines.join("\n");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
if (!focusNextLine && value === target.dataset.originalValue) return;
|
||||
if (focusNextLine) cancelledPreviewEdits.add(target);
|
||||
if (target.dataset.tableCell !== undefined) {
|
||||
lines[lineIndex] = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value.replace(/\n/g, " "));
|
||||
if (focusNextLine) lines.splice(lineIndex + 1, 0, "");
|
||||
} else {
|
||||
const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || "";
|
||||
const editedLines = value.split("\n");
|
||||
const replacements = editedLines.map((part, index) => `${index === 0 ? prefix : ""}${part}${index === editedLines.length - 1 ? suffix : ""}`);
|
||||
lines.splice(lineIndex, 1, ...replacements);
|
||||
}
|
||||
editor.value = lines.join("\n");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
if (focusNextLine) {
|
||||
const nextLine = lineIndex + Math.max(2, value.split("\n").length);
|
||||
const next = preview.querySelector(`[data-source-line="${nextLine}"].preview-editable`);
|
||||
next?.focus();
|
||||
if (next) placePreviewCaret(next, 0);
|
||||
}
|
||||
}
|
||||
function editRawPreviewLine(target) {
|
||||
const lineIndex = Number(target.dataset.sourceLine) - 1;
|
||||
const lines = editor.value.split("\n");
|
||||
if (lineIndex < 0 || lineIndex >= lines.length) return;
|
||||
const caretOffset = Math.min(previewCaretOffset(target), lines[lineIndex].length);
|
||||
target.dataset.rawSourceEdit = "true";
|
||||
target.dataset.originalValue = lines[lineIndex];
|
||||
target.textContent = lines[lineIndex];
|
||||
target.classList.add("preview-editable--source");
|
||||
target.focus({ preventScroll: true });
|
||||
placePreviewCaret(target, caretOffset);
|
||||
}
|
||||
|
||||
function insertPreviewLineBreak(target) {
|
||||
const lineIndex = Number(target.dataset.sourceLine) - 1;
|
||||
if (lineIndex < 0) return;
|
||||
const lines = editor.value.split("\n");
|
||||
const value = markdownFromPreview(target);
|
||||
let insertedLineIndex;
|
||||
if (target.dataset.tableCell !== undefined) {
|
||||
lines[lineIndex] = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value.replace(/\n/g, " "));
|
||||
insertedLineIndex = lineIndex + 1;
|
||||
lines.splice(insertedLineIndex, 0, "");
|
||||
} else {
|
||||
const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || "";
|
||||
const editedLines = value.split("\n");
|
||||
const replacements = editedLines.map((part, index) => `${index === 0 ? prefix : ""}${part}${index === editedLines.length - 1 ? suffix : ""}`);
|
||||
insertedLineIndex = lineIndex + replacements.length;
|
||||
lines.splice(lineIndex, 1, ...replacements, "");
|
||||
}
|
||||
cancelledPreviewEdits.add(target);
|
||||
editor.value = lines.join("\n");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
const sourceLine = insertedLineIndex + 1;
|
||||
const restoreFocus = () => {
|
||||
const next = preview.querySelector(`[data-source-line="${sourceLine}"].preview-editable`);
|
||||
if (!next) return;
|
||||
next.focus({ preventScroll: true });
|
||||
placePreviewCaret(next, 0);
|
||||
};
|
||||
restoreFocus();
|
||||
queueMicrotask(() => {
|
||||
const active = document.activeElement;
|
||||
if (!active || active === document.body || !preview.contains(active)) restoreFocus();
|
||||
});
|
||||
}
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.addEventListener("click", () => {
|
||||
authorshipMode = button.dataset.authorshipMode === "full" ? "full" : "simple";
|
||||
updateAuthorshipControls();
|
||||
renderGutter();
|
||||
}));
|
||||
authorshipColorsToggle?.addEventListener("change", () => {
|
||||
authorshipColorsEnabled = authorshipColorsToggle.checked;
|
||||
updateAuthorshipControls();
|
||||
renderGutter();
|
||||
});
|
||||
saveEditorSettingsButton?.addEventListener("click", async () => {
|
||||
if (!info?.can_save_editor_settings) return;
|
||||
saveEditorSettingsButton.disabled = true;
|
||||
try {
|
||||
await adapter.saveEditorSettings(sessionHeaders(), { authorship_mode: authorshipMode, colors_enabled: authorshipColorsEnabled });
|
||||
toast("Editor settings saved for everyone");
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
} finally {
|
||||
saveEditorSettingsButton.disabled = !info?.can_save_editor_settings;
|
||||
}
|
||||
});
|
||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); }); window.addEventListener("rustpad:urlchange", updateAddressLabel); document.querySelector("#copy-link").addEventListener("click", async () => { try { await copyText(currentShareUrl(uiState)); toast("Link copied"); } catch (e) { toast(e.message); } }); document.querySelectorAll("[data-format]").forEach(b => b.addEventListener("click", () => { applyFormat(editor, b.dataset.format); b.closest("details")?.removeAttribute("open"); })); 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()); document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close()); preview.addEventListener("change", event => { const checkbox = event.target.closest(".task-checkbox"); if (!checkbox) return; const lineIndex = Number(checkbox.dataset.sourceLine) - 1; const lines = editor.value.split("\n"); if (lineIndex < 0 || lineIndex >= lines.length) return; lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`); editor.value = lines.join("\n"); editor.dispatchEvent(new Event("input", { bubbles: true })); }); preview.addEventListener("focusin", event => { const target = event.target.closest(".preview-editable"); if (!target) return; target.dataset.originalHtml = target.innerHTML; target.dataset.originalValue = markdownFromPreview(target); }); preview.addEventListener("beforeinput", event => { if (!event.target.closest(".preview-editable")) return; if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") event.preventDefault(); }); preview.addEventListener("keydown", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); if (target.dataset.rawSourceEdit === "true") { cancelledPreviewEdits.add(target); render(); } else editRawPreviewLine(target); return; } if (event.key === "Enter") { event.preventDefault(); event.stopPropagation(); if (event.altKey) insertPreviewLineBreak(target); else target.blur(); return; } if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (movePreviewCaret(target, event.key === "ArrowUp" ? -1 : 1)) event.preventDefault(); } }); preview.addEventListener("blur", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (cancelledPreviewEdits.has(target)) { cancelledPreviewEdits.delete(target); return; } commitPreviewEdit(target); }, { capture: true });
|
||||
const publishPageButton = document.querySelector("#publish-page");
|
||||
function updatePageControls() {
|
||||
const enabled = publicPageEnabled.checked;
|
||||
publishPageButton.disabled = !enabled;
|
||||
publicTaskUpdates.disabled = !enabled;
|
||||
unprotectPublicPage.disabled = !enabled;
|
||||
}
|
||||
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked, publicPageEnabled.checked);
|
||||
publicPageEnabled.addEventListener("change", async () => {
|
||||
const previous = !publicPageEnabled.checked;
|
||||
updatePageControls();
|
||||
publicPageEnabled.disabled = true;
|
||||
try { await savePublicOptions(); toast(publicPageEnabled.checked ? "Page enabled" : "Page disabled"); }
|
||||
catch (error) { publicPageEnabled.checked = previous; updatePageControls(); toast(error.message); }
|
||||
finally { publicPageEnabled.disabled = false; }
|
||||
});
|
||||
publicTaskUpdates.addEventListener("change", async () => { publicTaskUpdates.disabled = true; try { await savePublicOptions(); toast(publicTaskUpdates.checked ? "Public task updates enabled" : "Public task updates disabled"); } catch (error) { publicTaskUpdates.checked = !publicTaskUpdates.checked; toast(error.message); } finally { updatePageControls(); } });
|
||||
unprotectPublicPage.addEventListener("change", async () => { unprotectPublicPage.disabled = true; try { await savePublicOptions(); toast(unprotectPublicPage.checked ? "Published page is now unprotected" : "Published page protection enabled"); } catch (error) { unprotectPublicPage.checked = !unprotectPublicPage.checked; toast(error.message); } finally { updatePageControls(); } });
|
||||
publishPageButton.addEventListener("click", async () => { if (!publicPageEnabled.checked) return; try { const result = await savePublicOptions(); if (!result.url) throw new Error("Page is disabled"); const url = new URL(result.url, location.origin).href; await copyText(url); toast("Page link copied"); window.open(url, "_blank", "noopener"); } catch (error) { toast(error.message); } });
|
||||
roomDetails.addEventListener("toggle", () => { if (roomDetails.open) { clearUnread(); chatInput.focus(); if ("Notification" in window && Notification.permission === "default") Notification.requestPermission().catch(() => { }); } else { roomDetails.classList.remove("is-mobile-open"); } });
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden && roomDetails.open) clearUnread(); });
|
||||
chatForm.addEventListener("submit", event => { event.preventDefault(); const text = chatInput.value.trim(); if (!text || !socket) return; socket.chat(text); chatInput.value = ""; chatInput.focus(); });
|
||||
|
||||
+2
-5
@@ -30,10 +30,7 @@
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="header-actions" class="header-actions"><button id="copy-link"
|
||||
class="secondary-button">Copy link</button><button id="publish-page"
|
||||
class="secondary-button">Page</button><div class="public-page-options"><label class="public-task-toggle"
|
||||
title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates"
|
||||
type="checkbox"> Editable tasks on Page</label><label class="public-task-toggle" title="Allow the published page to open without the note password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div><button id="files-button"
|
||||
class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><details class="page-settings"><summary class="secondary-button">Page settings</summary><div class="page-settings-menu"><label class="public-task-toggle" title="Enable or disable the published page"><input id="public-page-enabled" type="checkbox"> Enable Page</label><label class="public-task-toggle" title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates" type="checkbox"> Editable tasks</label><label class="public-task-toggle" title="Allow the published page to open without the resource password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div></details><button id="files-button"
|
||||
class="secondary-button">Files</button><button id="delete-note" class="secondary-button danger-button"
|
||||
hidden>Delete</button><button id="history-button" class="secondary-button">History</button></div>
|
||||
</div>
|
||||
@@ -104,7 +101,7 @@
|
||||
</div>
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-controls"><label class="switch-control authorship-colors-switch" title="Show or hide author coloring"><input id="authorship-colors-toggle" type="checkbox" checked><span class="switch-control__track" aria-hidden="true"></span><span id="authorship-colors-label">Colors on</span></label><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div><button id="save-editor-settings" class="secondary-button compact-button editor-settings-save" type="button">Save</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="editor-shell">
|
||||
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
|
||||
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div><div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor"
|
||||
|
||||
+3
-6
@@ -32,10 +32,7 @@
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="header-actions" class="header-actions"><button id="copy-link" class="secondary-button">Copy
|
||||
link</button><button id="publish-page" class="secondary-button">Page</button><div class="public-page-options"><label
|
||||
class="public-task-toggle"
|
||||
title="Allow visitors to update task checkboxes on the published page"><input
|
||||
id="public-task-updates" type="checkbox"> Editable tasks on Page</label><label class="public-task-toggle" title="Allow the published page to open without the note password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div><button
|
||||
link</button><button id="publish-page" class="secondary-button">Page</button><details class="page-settings"><summary class="secondary-button">Page settings</summary><div class="page-settings-menu"><label class="public-task-toggle" title="Enable or disable the published page"><input id="public-page-enabled" type="checkbox"> Enable Page</label><label class="public-task-toggle" title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates" type="checkbox"> Editable tasks</label><label class="public-task-toggle" title="Allow the published page to open without the resource password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div></details><button
|
||||
id="files-button" class="secondary-button">Files</button><button id="history-button"
|
||||
class="secondary-button">History</button></div>
|
||||
</div>
|
||||
@@ -107,7 +104,7 @@
|
||||
</div>
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-controls"><label class="switch-control authorship-colors-switch" title="Show or hide author coloring"><input id="authorship-colors-toggle" type="checkbox" checked><span class="switch-control__track" aria-hidden="true"></span><span id="authorship-colors-label">Colors on</span></label><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div><button id="save-editor-settings" class="secondary-button compact-button editor-settings-save" type="button">Save</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="editor-shell">
|
||||
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
|
||||
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div>
|
||||
@@ -171,7 +168,7 @@
|
||||
<div class="shortcut-grid">
|
||||
<kbd>Ctrl/Cmd+Z</kbd><span>Undo</span><kbd>Ctrl/Cmd+B</kbd><span>Bold</span><kbd>Ctrl/Cmd+I</kbd><span>Italic</span><kbd>Ctrl/Cmd+Shift+X</kbd><span>Strikethrough</span><kbd>Ctrl/Cmd+K</kbd><span>Link</span><kbd>Ctrl/Cmd+Shift+7</kbd><span>Numbered
|
||||
list</span><kbd>Ctrl/Cmd+Shift+8</kbd><span>Bullet list</span><kbd>Ctrl/Cmd+Shift+9</kbd><span>Task
|
||||
list</span><kbd>Alt+1…4</kbd><span>Headings H1–H4</span>
|
||||
list</span><kbd>Alt+1…4</kbd><span>Headings H1–H4</span><kbd>Alt+Enter</kbd><span>New line while editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
Reference in New Issue
Block a user