new finctions and fixes
This commit is contained in:
+109
-12
@@ -300,6 +300,18 @@ textarea:focus {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.document-heading--copy {
|
||||
border-radius: 6px;
|
||||
cursor: copy;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.document-heading--copy:hover,
|
||||
.document-heading--copy:focus-visible {
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
|
||||
.document-heading h1 {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
@@ -4532,11 +4544,22 @@ dialog::backdrop {
|
||||
gap: 5px;
|
||||
max-width: calc(100vw - 20px);
|
||||
padding: 5px;
|
||||
isolation: isolate;
|
||||
border: 1px solid rgba(255, 255, 255, .12);
|
||||
border-radius: 999px;
|
||||
background: rgba(17, 21, 28, .88);
|
||||
background: transparent;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .28);
|
||||
}
|
||||
|
||||
.pad-page .mobile-editor-bubble::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: rgba(17, 21, 28, .88);
|
||||
backdrop-filter: blur(10px);
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mobile-editor-bubble>button,
|
||||
@@ -5059,6 +5082,12 @@ dialog::backdrop {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.authorship-controls button:disabled,
|
||||
.authorship-controls input:disabled+.switch-control__track {
|
||||
cursor: not-allowed;
|
||||
opacity: .55;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -5107,17 +5136,6 @@ dialog::backdrop {
|
||||
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;
|
||||
@@ -5320,6 +5338,7 @@ dialog::backdrop {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional links to exact editor lines. */
|
||||
.line-number-button {
|
||||
display: block;
|
||||
@@ -5357,3 +5376,81 @@ dialog::backdrop {
|
||||
background: color-mix(in srgb, var(--success) 18%, transparent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Personal editor preferences in the compact mobile action bar. */
|
||||
@media (max-width: 1499px) {
|
||||
.mobile-editor-options {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mobile-editor-options>summary {
|
||||
display: inline-grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .07);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mobile-editor-options>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-editor-options[open]>summary {
|
||||
background: color-mix(in srgb, var(--accent) 24%, rgba(255, 255, 255, .07));
|
||||
}
|
||||
|
||||
.mobile-editor-options__panel {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: calc(64px + env(safe-area-inset-bottom, 0px));
|
||||
left: auto;
|
||||
display: grid;
|
||||
width: min(360px, calc(100vw - 24px));
|
||||
max-height: calc(100dvh - 88px);
|
||||
gap: 5px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 10px;
|
||||
background: var(--surface-strong, #11161e);
|
||||
box-shadow: 0 16px 40px rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.mobile-editor-options__panel>label:not(.mobile-option-check) {
|
||||
display: grid;
|
||||
grid-template-columns: 70px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
font-size: .78rem;
|
||||
}
|
||||
|
||||
.mobile-editor-options__panel select {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.mobile-option-check {
|
||||
display: flex;
|
||||
min-height: 24px;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--text);
|
||||
font-size: .8rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.mobile-option-check input {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,19 +5,20 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>__PAD_TITLE__ · RustPad</title>
|
||||
<title>__DOCUMENT_TITLE__ · __PARENT_TITLE__</title>
|
||||
__APP_STYLESHEET__
|
||||
__APP_IMPORT_MAP__
|
||||
__APP_ENTRYPOINT__
|
||||
</head>
|
||||
|
||||
<body class="pad-page" data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<body class="pad-page" data-resource-kind="__RESOURCE_KIND__" data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<header class="app-header">
|
||||
<div class="app-header__main"><a class="brand home-brand" href="/">RustPad</a><span
|
||||
class="header-divider"></span>
|
||||
<div class="document-heading">
|
||||
<h1 id="pad-title">__PAD_TITLE__</h1>
|
||||
<p id="pad-url" class="document-url"></p>
|
||||
<div class="app-header__main"><a id="resource-parent-link" class="brand __PARENT_CLASS__"
|
||||
href="__PARENT_URL__">__PARENT_TITLE__</a><span class="header-divider"></span>
|
||||
<div id="document-link-copy" class="document-heading document-heading--copy" role="button" tabindex="0"
|
||||
title="Copy this link" aria-label="Copy this link">
|
||||
<h1 id="document-title">__DOCUMENT_TITLE__</h1>
|
||||
<p id="document-url" class="document-url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="header-user-control user-color-control"><button id="current-user" class="user-chip" type="button"
|
||||
@@ -43,7 +44,8 @@
|
||||
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"
|
||||
</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>
|
||||
@@ -119,14 +121,12 @@
|
||||
<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"
|
||||
type="checkbox" checked disabled><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>
|
||||
type="button" data-authorship-mode="simple" class="active" disabled>Simple</button><button
|
||||
type="button" data-authorship-mode="full" disabled>Full</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
@@ -193,8 +193,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><kbd>Alt+Enter</kbd><span>New line while
|
||||
editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>
|
||||
list</span><kbd>Alt+1…4</kbd><span>Headings H1–H4</span>__EXTRA_SHORTCUTS__
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -219,9 +218,9 @@
|
||||
guest</button><button id="show-register" class="text-button" type="button">Register</button><button
|
||||
id="show-login" class="text-button" type="button">Log in</button></div>
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail / LDAP Username<input
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail / organization login<input
|
||||
id="auth-email" name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com or name.second-name"></label><label>Password<input
|
||||
placeholder="you@example.com"></label><label>Password<input
|
||||
id="auth-password" name="password" type="password" minlength="8" maxlength="128"
|
||||
autocomplete="current-password"></label><button id="auth-submit" class="primary-button"
|
||||
type="submit">Log in and continue</button>
|
||||
@@ -235,7 +234,7 @@
|
||||
</dialog>
|
||||
<dialog id="password-dialog">
|
||||
<form id="password-form" class="dialog-panel">
|
||||
<h2>Protected note</h2><input id="open-password" type="password" autocomplete="current-password" required
|
||||
<h2>Protected __PROTECTED_RESOURCE_LABEL__</h2><input id="open-password" type="password" autocomplete="current-password" required
|
||||
placeholder="Password">
|
||||
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
|
||||
class="dialog-link" href="/">Cancel</a>
|
||||
@@ -245,6 +244,29 @@
|
||||
<button id="mobile-bubble-drag" class="mobile-bubble-drag" type="button" title="Move quick actions"
|
||||
aria-label="Move quick actions">⋮⋮</button>
|
||||
<button id="mobile-files-button" type="button" title="Files" aria-label="Open files">📎</button>
|
||||
<details id="mobile-editor-options" class="mobile-editor-options">
|
||||
<summary title="Editor options" aria-label="Open editor options">⚙</summary>
|
||||
<div class="mobile-editor-options__panel">
|
||||
<label>Font<select id="mobile-font-family">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="system">System</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="arial">Arial</option>
|
||||
<option value="georgia">Georgia</option>
|
||||
</select></label>
|
||||
<label>Size<select id="mobile-font-size">
|
||||
<option value="14">14</option>
|
||||
<option value="16">16</option>
|
||||
<option value="18">18</option>
|
||||
<option value="20">20</option>
|
||||
<option value="22">22</option>
|
||||
</select></label>
|
||||
<label class="mobile-option-check"><input id="mobile-line-numbers-toggle" type="checkbox"> Editor lines</label>
|
||||
<label class="mobile-option-check"><input id="mobile-preview-line-numbers-toggle" type="checkbox"> Preview lines</label>
|
||||
<label class="mobile-option-check"><input id="mobile-compact-toggle" type="checkbox"> Compact view</label>
|
||||
<label class="mobile-option-check"><input id="mobile-line-links-toggle" type="checkbox"> Line links</label>
|
||||
</div>
|
||||
</details>
|
||||
<label id="mobile-color-button" class="mobile-color-button" title="Editor color"
|
||||
aria-label="Change editor color"><span class="mobile-color-dot" aria-hidden="true"></span><input
|
||||
id="mobile-color-picker" type="color" aria-label="Change editor color"></label>
|
||||
@@ -28,6 +28,25 @@ function safeUrl(value) {
|
||||
}
|
||||
|
||||
const emoji = EMOJI_SHORTCODES;
|
||||
let markdownFiles = new Map();
|
||||
|
||||
export function setMarkdownFiles(files) {
|
||||
markdownFiles = new Map((Array.isArray(files) ? files : [])
|
||||
.filter(file => file && file.filename && file.url)
|
||||
.map(file => [String(file.filename), {
|
||||
url: String(file.url),
|
||||
mimeType: String(file.mime_type || ""),
|
||||
}]));
|
||||
}
|
||||
|
||||
export function unresolvedMarkdownFileAliases(value) {
|
||||
const missing = new Set();
|
||||
const source = String(value || "").replace(/`[^`]*`/g, "");
|
||||
for (const match of source.matchAll(/\[(?:file|image|img)=([^,\]\s]+)(?:,[^\]]*)?\]/gi)) {
|
||||
if (!markdownFiles.has(match[1])) missing.add(match[1]);
|
||||
}
|
||||
return [...missing];
|
||||
}
|
||||
|
||||
function inline(value) {
|
||||
const tokens = [];
|
||||
@@ -39,6 +58,16 @@ function inline(value) {
|
||||
let html = escapeHtml(value);
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, (_, code) => stash(`<code>${code}</code>`));
|
||||
html = html.replace(/\[(file|image|img)=([^,\]\s]+)(?:,([^\]]*))?\]/gi, (match, kind, filename, label) => {
|
||||
const file = markdownFiles.get(filename);
|
||||
if (!file) return match;
|
||||
const text = String(label || filename).trim() || filename;
|
||||
if (kind.toLowerCase() === "file") {
|
||||
return stash(`<a href="${safeUrl(file.url)}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-file-alias="file" data-file-name="${filename}">${text}</a>`);
|
||||
}
|
||||
if (!file.mimeType.startsWith("image/")) return match;
|
||||
return stash(`<img src="${safeUrl(file.url)}" alt="${text}" loading="lazy" decoding="async" referrerpolicy="no-referrer" draggable="false" contenteditable="false" data-file-alias="image" data-file-name="${filename}">`);
|
||||
});
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => {
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
return stash(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async" referrerpolicy="no-referrer" draggable="false" contenteditable="false"${titleAttr}>`);
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createPadAdapter() {
|
||||
|
||||
return {
|
||||
access: { kind: "pad", key: slug },
|
||||
addressSelector: "#pad-url",
|
||||
addressSelector: "#document-url",
|
||||
title: info => `${info.title} · RustPad`,
|
||||
loadInfo: headers => api(base, { headers }),
|
||||
loadColor: headers => api(`${base}/editor-color`, { headers }),
|
||||
@@ -62,7 +62,7 @@ export function createWorkspaceNoteAdapter() {
|
||||
|
||||
return {
|
||||
access: { kind: "workspace", key: workspaceSlug },
|
||||
addressSelector: "#note-url",
|
||||
addressSelector: "#document-url",
|
||||
title: info => `${info.title} · ${info.workspace_title}`,
|
||||
loadInfo: headers => api(base, { headers }),
|
||||
loadColor: headers => api(`${base}/editor-color`, { headers }),
|
||||
|
||||
+158
-25
@@ -15,7 +15,7 @@ import { copyText } from "@rustpad/clipboard";
|
||||
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
|
||||
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
|
||||
import { bindEmojiPicker } from "@rustpad/emoji-picker";
|
||||
import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
|
||||
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
|
||||
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
|
||||
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
||||
import { bindNoteFiles } from "@rustpad/note-files";
|
||||
@@ -27,24 +27,69 @@ 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"), lineLinksToggle = document.querySelector("#line-links-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"), mobileColorPicker = document.querySelector("#mobile-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
|
||||
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"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
|
||||
const notePreferenceKey = name => `rustpad:${name}:${adapter.access.kind}:${adapter.access.key}`;
|
||||
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 editorSettingsSaveTimer, editorSettingsSaveInFlight = false, pendingPersonalSettingsSave = false, pendingAuthorshipSettingsSave = false;
|
||||
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
|
||||
const singlePaneQuery = window.matchMedia("(max-width: 760px)");
|
||||
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";
|
||||
previewLineToggle.checked = localStorage.getItem("rustpad:preview-line-numbers") === "on";
|
||||
compactToggle.checked = localStorage.getItem("rustpad:compact") !== "off";
|
||||
lineLinksToggle.checked = localStorage.getItem("rustpad:line-links") === "on";
|
||||
fontFamily.value = localStorage.getItem("rustpad:font-family") || "mono";
|
||||
fontSize.value = localStorage.getItem("rustpad:font-size") || "14";
|
||||
let refreshFilesForAliases = () => { };
|
||||
let aliasRefreshTimer = 0;
|
||||
let lastUnresolvedAliasKey = "";
|
||||
let markdownFileSignature = "";
|
||||
function updateMarkdownFiles(files, { rerender = false } = {}) {
|
||||
const normalized = (Array.isArray(files) ? files : []).map(file => ({
|
||||
filename: String(file?.filename || ""),
|
||||
url: String(file?.url || ""),
|
||||
mime_type: String(file?.mime_type || ""),
|
||||
})).sort((left, right) => left.filename.localeCompare(right.filename));
|
||||
const nextSignature = JSON.stringify(normalized);
|
||||
const changed = nextSignature !== markdownFileSignature;
|
||||
markdownFileSignature = nextSignature;
|
||||
setMarkdownFiles(normalized);
|
||||
if (rerender && changed) render();
|
||||
}
|
||||
function scheduleAliasFileRefresh(content) {
|
||||
const key = unresolvedMarkdownFileAliases(content).sort().join("\u0000");
|
||||
if (!key) {
|
||||
lastUnresolvedAliasKey = "";
|
||||
return;
|
||||
}
|
||||
if (key === lastUnresolvedAliasKey) return;
|
||||
lastUnresolvedAliasKey = key;
|
||||
clearTimeout(aliasRefreshTimer);
|
||||
aliasRefreshTimer = window.setTimeout(() => refreshFilesForAliases(), 200);
|
||||
}
|
||||
const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem(notePreferenceKey("line-numbers")) !== "off";
|
||||
previewLineToggle.checked = localStorage.getItem(notePreferenceKey("preview-line-numbers")) === "on";
|
||||
compactToggle.checked = localStorage.getItem(notePreferenceKey("compact")) !== "off";
|
||||
lineLinksToggle.checked = localStorage.getItem(notePreferenceKey("line-links")) === "on";
|
||||
fontFamily.value = localStorage.getItem(notePreferenceKey("font-family")) || "mono";
|
||||
fontSize.value = localStorage.getItem(notePreferenceKey("font-size")) || "14";
|
||||
authorshipColorsToggle.checked = authorshipColorsEnabled;
|
||||
function syncMobileEditorControls() {
|
||||
if (mobileFontFamily) mobileFontFamily.value = fontFamily.value;
|
||||
if (mobileFontSize) mobileFontSize.value = fontSize.value;
|
||||
if (mobileLineToggle) mobileLineToggle.checked = lineToggle.checked;
|
||||
if (mobilePreviewLineToggle) mobilePreviewLineToggle.checked = previewLineToggle.checked;
|
||||
if (mobileCompactToggle) mobileCompactToggle.checked = compactToggle.checked;
|
||||
if (mobileLineLinksToggle) mobileLineLinksToggle.checked = lineLinksToggle.checked;
|
||||
}
|
||||
syncMobileEditorControls();
|
||||
function updateAuthorshipControls() {
|
||||
const canManage = info?.can_manage_authorship === true;
|
||||
authorshipColorsToggle.checked = authorshipColorsEnabled;
|
||||
authorshipColorsToggle.disabled = !canManage;
|
||||
authorshipColorsLabel.textContent = authorshipColorsEnabled ? "Colors on" : "Colors off";
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode));
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => {
|
||||
button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode);
|
||||
button.disabled = !canManage;
|
||||
});
|
||||
const controls = document.querySelector(".authorship-controls");
|
||||
if (controls) controls.title = canManage ? "Global authorship settings" : "Only the owner can change authorship settings";
|
||||
}
|
||||
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) }; }
|
||||
@@ -67,7 +112,13 @@ export function startNoteEditor(adapter) {
|
||||
}, replacement, contentLength);
|
||||
}
|
||||
function updateCurrentUser() { const color = currentUserColor() || defaultColorFor(nickname); const pickerColor = /^#[0-9a-f]{6}$/i.test(color) ? color : "#7c6cff"; const overridden = Boolean(noteUserColor()); currentUser.querySelector(".user-chip__name").textContent = nickname; currentUser.style.setProperty("--owner", color); currentUser.title = overridden ? "Note color override" : "Global profile color"; userColorPicker.value = pickerColor; if (mobileColorPicker) mobileColorPicker.value = pickerColor; useGlobalColorButton.hidden = !overridden; document.querySelector(".mobile-editor-bubble")?.style.setProperty("--owner", color); }
|
||||
function sessionHeaders() { const token = accessToken || getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||
function sessionHeaders() {
|
||||
const userToken = getAuthToken();
|
||||
const resourceToken = accessToken || userToken;
|
||||
const headers = resourceToken ? { Authorization: `Bearer ${resourceToken}` } : {};
|
||||
if (userToken && userToken !== resourceToken) headers["X-RustPad-User-Token"] = userToken;
|
||||
return headers;
|
||||
}
|
||||
function accountHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||
async function loadNoteInfo() {
|
||||
info = await adapter.loadInfo(sessionHeaders());
|
||||
@@ -78,10 +129,19 @@ export function startNoteEditor(adapter) {
|
||||
} else {
|
||||
noteColor = readGuestColor();
|
||||
}
|
||||
updateMarkdownFiles(info.files || []);
|
||||
if (info.personal_editor_settings) {
|
||||
compactToggle.checked = info.compact_view !== false;
|
||||
lineToggle.checked = info.editor_line_numbers !== false;
|
||||
previewLineToggle.checked = info.preview_line_numbers === true;
|
||||
lineLinksToggle.checked = info.line_links === true;
|
||||
if (["mono", "system", "serif", "arial", "georgia"].includes(info.font_family)) fontFamily.value = info.font_family;
|
||||
if (["14", "16", "18", "20", "22"].includes(String(info.font_size))) fontSize.value = String(info.font_size);
|
||||
}
|
||||
authorshipMode = info.authorship_mode === "full" ? "full" : "simple";
|
||||
authorshipColorsEnabled = info.colors_enabled !== false;
|
||||
updateAuthorshipControls();
|
||||
if (saveEditorSettingsButton) saveEditorSettingsButton.disabled = !info.can_save_editor_settings;
|
||||
syncMobileEditorControls();
|
||||
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(); }
|
||||
@@ -202,10 +262,17 @@ export function startNoteEditor(adapter) {
|
||||
if (tag === "code") return { open: "`", close: "`", atomic: null };
|
||||
if (tag === "sub") return { open: "~", close: "~", atomic: null };
|
||||
if (tag === "sup" && !current.classList.contains("footnote-ref")) return { open: "^", close: "^", atomic: null };
|
||||
const fileAlias = current.getAttribute("data-file-alias");
|
||||
const fileName = current.getAttribute("data-file-name");
|
||||
if (tag === "a" && fileAlias === "file" && fileName) return { open: `[file=${fileName},`, close: "]", atomic: null };
|
||||
if (tag === "a") return { open: "[", close: `](${current.getAttribute("href") || "#"})`, atomic: null };
|
||||
if (tag === "img") {
|
||||
const src = current.getAttribute("src") || "";
|
||||
const alt = current.getAttribute("alt") || "";
|
||||
if (fileAlias === "image" && fileName) {
|
||||
const safeAlt = alt.replace(/\]/g, ")").replace(/[\r\n]+/g, " ");
|
||||
return { open: "", close: "", atomic: `[image=${fileName},${safeAlt}]` };
|
||||
}
|
||||
const src = current.getAttribute("src") || "";
|
||||
const title = current.getAttribute("title");
|
||||
return { open: "", close: "", atomic: `}"` : ""})` };
|
||||
}
|
||||
@@ -478,7 +545,7 @@ export function startNoteEditor(adapter) {
|
||||
const ratio = editorRange > 0 ? editor.scrollTop / editorRange : 0;
|
||||
preview.scrollTop = ratio * previewRange;
|
||||
}
|
||||
function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(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 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 activeView() {
|
||||
return singlePaneQuery.matches ? compactView : uiState.view;
|
||||
}
|
||||
@@ -549,9 +616,11 @@ export function startNoteEditor(adapter) {
|
||||
const { loadFiles } = bindNoteFiles({
|
||||
editor, toast, getAccessToken: () => accessToken, canDelete: () => Boolean(info?.can_delete_files),
|
||||
endpoints: adapter.fileEndpoints,
|
||||
onFilesChanged: files => updateMarkdownFiles(files, { rerender: true }),
|
||||
});
|
||||
refreshFilesForAliases = () => loadFiles();
|
||||
function connect() { socket?.stop(); socket = adapter.createSocket({ password, accessToken, nickname, color: currentUserColor() || null, sessionToken: getAuthToken(), guestId: getGuestId(), onStatus: s => setStatus(s === "online" ? "online" : s === "offline" ? "offline" : null, s === "online" ? "Connected" : s === "offline" ? "Reconnecting…" : "Connecting…"), onAuthenticated: m => { resourceUnlocked = true; if (passwordDialog.open) passwordDialog.close(); const readOnly = m.access_level === "read_only"; editor.readOnly = readOnly; accessLevel.textContent = readOnly ? "Access: read only" : "Access: full"; applyRemote(m.content, m.owner_map); if (!readOnly) editor.focus(); }, onDocument: m => { applyRemote(m.content, m.owner_map); document.querySelector("#save-state").textContent = `${m.author ? `${m.author} · ` : ""}${new Date(m.updated_at).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}`; }, onPresence: updatePresence, onLatency: updateLatency, onChat: appendChatMessage, onError: m => { const friendly = /read-only access/i.test(m) ? "This note is read only. Enter the password or ask the owner to grant write access." : m; document.querySelector("#password-error").textContent = friendly; if (/read-only access/i.test(m)) { toast(friendly); accessLevel.textContent = "Access: read only"; editor.readOnly = true; return; } if (/nickname|session|account/i.test(m)) { if (!identityDialog.open) identityDialog.showModal(); } else if (info?.protected && !passwordDialog.open) passwordDialog.showModal(); } }); socket.connect(); }
|
||||
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||
bindIdentityDialog({ dialog: identityDialog, onIdentity: async value => { nickname = value; accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key) || getAuthToken(); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && !accessToken) passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
|
||||
async function showSystemNotFound() {
|
||||
try {
|
||||
@@ -579,7 +648,7 @@ export function startNoteEditor(adapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key);
|
||||
accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key) || getAuthToken();
|
||||
await loadNoteInfo();
|
||||
document.title = adapter.title(info);
|
||||
publicPageEnabled.checked = Boolean(info.public_page_enabled); publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected); updatePageControls();
|
||||
@@ -631,7 +700,24 @@ export function startNoteEditor(adapter) {
|
||||
compactLayoutQuery.addEventListener("change", event => {
|
||||
if (!event.matches) setHeaderMenuOpen(false);
|
||||
});
|
||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); }); lineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:line-numbers", lineToggle.checked ? "on" : "off"); renderGutter(); }); previewLineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:preview-line-numbers", previewLineToggle.checked ? "on" : "off"); renderGutter(); alignPreviewLineNumbers(preview); }); compactToggle.addEventListener("change", () => { localStorage.setItem("rustpad:compact", compactToggle.checked ? "on" : "off"); applyUi(); }); lineLinksToggle.addEventListener("change", () => { localStorage.setItem("rustpad:line-links", lineLinksToggle.checked ? "on" : "off"); renderGutter(); }); fontFamily.addEventListener("change", () => { localStorage.setItem("rustpad:font-family", fontFamily.value); applyUi(); }); fontSize.addEventListener("change", () => { localStorage.setItem("rustpad:font-size", fontSize.value); applyUi(); });
|
||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ 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 }); });
|
||||
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 }); });
|
||||
const mirrorMobileControl = (mobileControl, desktopControl) => mobileControl?.addEventListener("change", () => {
|
||||
if (desktopControl instanceof HTMLInputElement && desktopControl.type === "checkbox") desktopControl.checked = mobileControl.checked;
|
||||
else desktopControl.value = mobileControl.value;
|
||||
desktopControl.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
mirrorMobileControl(mobileFontFamily, fontFamily);
|
||||
mirrorMobileControl(mobileFontSize, fontSize);
|
||||
mirrorMobileControl(mobileLineToggle, lineToggle);
|
||||
mirrorMobileControl(mobilePreviewLineToggle, previewLineToggle);
|
||||
mirrorMobileControl(mobileCompactToggle, compactToggle);
|
||||
mirrorMobileControl(mobileLineLinksToggle, lineLinksToggle);
|
||||
document.querySelector("#mobile-files-button")?.addEventListener("click", () => document.querySelector("#files-button")?.click());
|
||||
const compactBubbleQuery = matchMedia("(max-width: 1499px)");
|
||||
const roomPopover = roomDetails.querySelector(".room-popover");
|
||||
@@ -788,27 +874,66 @@ export function startNoteEditor(adapter) {
|
||||
});
|
||||
}
|
||||
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.addEventListener("click", () => {
|
||||
if (!info?.can_manage_authorship) return;
|
||||
authorshipMode = button.dataset.authorshipMode === "full" ? "full" : "simple";
|
||||
updateAuthorshipControls();
|
||||
syncMobileEditorControls();
|
||||
renderGutter();
|
||||
scheduleEditorSettingsSave({ authorship: true });
|
||||
}));
|
||||
authorshipColorsToggle?.addEventListener("change", () => {
|
||||
if (!info?.can_manage_authorship) return;
|
||||
authorshipColorsEnabled = authorshipColorsToggle.checked;
|
||||
updateAuthorshipControls();
|
||||
syncMobileEditorControls();
|
||||
renderGutter();
|
||||
scheduleEditorSettingsSave({ authorship: true });
|
||||
});
|
||||
saveEditorSettingsButton?.addEventListener("click", async () => {
|
||||
if (!info?.can_save_editor_settings) return;
|
||||
saveEditorSettingsButton.disabled = true;
|
||||
function personalEditorSettingsPayload() {
|
||||
return {
|
||||
compact_view: compactToggle.checked,
|
||||
editor_line_numbers: lineToggle.checked,
|
||||
preview_line_numbers: previewLineToggle.checked,
|
||||
line_links: lineLinksToggle.checked,
|
||||
font_family: fontFamily.value,
|
||||
font_size: Number(fontSize.value),
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleEditorSettingsSave({ personal = false, authorship = false } = {}) {
|
||||
if (personal) pendingPersonalSettingsSave = true;
|
||||
if (authorship && info?.can_manage_authorship) pendingAuthorshipSettingsSave = true;
|
||||
if (!info?.can_save_editor_settings || (!pendingPersonalSettingsSave && !pendingAuthorshipSettingsSave)) return;
|
||||
clearTimeout(editorSettingsSaveTimer);
|
||||
editorSettingsSaveTimer = window.setTimeout(flushEditorSettingsSave, 250);
|
||||
}
|
||||
|
||||
async function flushEditorSettingsSave() {
|
||||
if (editorSettingsSaveInFlight || !info?.can_save_editor_settings) return;
|
||||
const savePersonal = pendingPersonalSettingsSave;
|
||||
const saveAuthorship = pendingAuthorshipSettingsSave && info.can_manage_authorship;
|
||||
if (!savePersonal && !saveAuthorship) return;
|
||||
pendingPersonalSettingsSave = false;
|
||||
pendingAuthorshipSettingsSave = false;
|
||||
editorSettingsSaveInFlight = true;
|
||||
const settings = savePersonal ? personalEditorSettingsPayload() : {};
|
||||
if (saveAuthorship) {
|
||||
settings.authorship_mode = authorshipMode;
|
||||
settings.colors_enabled = authorshipColorsEnabled;
|
||||
}
|
||||
try {
|
||||
await adapter.saveEditorSettings(sessionHeaders(), { authorship_mode: authorshipMode, colors_enabled: authorshipColorsEnabled });
|
||||
toast("Editor settings saved for everyone");
|
||||
await adapter.saveEditorSettings(sessionHeaders(), settings);
|
||||
if (savePersonal) info.personal_editor_settings = true;
|
||||
} catch (error) {
|
||||
toast(error.message);
|
||||
} finally {
|
||||
saveEditorSettingsButton.disabled = !info?.can_save_editor_settings;
|
||||
editorSettingsSaveInFlight = false;
|
||||
if (pendingPersonalSettingsSave || pendingAuthorshipSettingsSave) {
|
||||
clearTimeout(editorSettingsSaveTimer);
|
||||
editorSettingsSaveTimer = window.setTimeout(flushEditorSettingsSave, 250);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
window.addEventListener("popstate", () => { lastRevealedLineHash = ""; uiState = readEditorState(); applyUi(); requestAnimationFrame(revealLinkedLine); });
|
||||
window.addEventListener("hashchange", () => { lastRevealedLineHash = ""; renderGutter(); requestAnimationFrame(revealLinkedLine); });
|
||||
window.addEventListener("rustpad:urlchange", updateAddressLabel);
|
||||
@@ -826,9 +951,17 @@ export function startNoteEditor(adapter) {
|
||||
toast(error.message);
|
||||
}
|
||||
});
|
||||
document.querySelector("#copy-link").addEventListener("click", async () => {
|
||||
async function copyCurrentLink() {
|
||||
try { await copyText(currentShareUrl(uiState)); toast("Link copied"); }
|
||||
catch (error) { toast(error.message); }
|
||||
}
|
||||
document.querySelector("#copy-link").addEventListener("click", copyCurrentLink);
|
||||
const documentLinkCopy = document.querySelector("#document-link-copy");
|
||||
documentLinkCopy?.addEventListener("click", copyCurrentLink);
|
||||
documentLinkCopy?.addEventListener("keydown", event => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
copyCurrentLink();
|
||||
});
|
||||
|
||||
let pendingPreviewFormatRange = null;
|
||||
|
||||
+19
-7
@@ -31,7 +31,17 @@ function formatDate(value) {
|
||||
return Number.isNaN(date.getTime()) ? "" : date.toLocaleString("pl-PL");
|
||||
}
|
||||
|
||||
export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, toast }) {
|
||||
function aliasCode(filename, label, mimeType) {
|
||||
const kind = String(mimeType || "").startsWith("image/") ? "image" : "file";
|
||||
const safeLabel = String(label || filename).replace(/\]/g, ")").replace(/[\r\n]+/g, " ").trim() || filename;
|
||||
return `[${kind}=${filename},${safeLabel}]`;
|
||||
}
|
||||
|
||||
function markdownCode(url, label, mimeType) {
|
||||
return String(mimeType || "").startsWith("image/") ? `` : `[${label}](${url})`;
|
||||
}
|
||||
|
||||
export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, toast, onFilesChanged = () => {} }) {
|
||||
const dialog = document.querySelector("#files-dialog");
|
||||
const list = document.querySelector("#files-list");
|
||||
const input = document.querySelector("#file-input");
|
||||
@@ -50,12 +60,14 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="action-button action-button--secondary compact-button" data-show-file-code="link" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Link</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="alias" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Alias</button>
|
||||
<button class="action-button action-button--primary compact-button" data-show-file-code="markdown" data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Markdown</button>
|
||||
<button class="action-button action-button--primary compact-button" data-add-file-to-note data-url="${escapeHtml(file.url)}" data-name="${escapeHtml(file.filename)}" data-mime="${escapeHtml(file.mime_type)}">Add to note</button>
|
||||
${canDelete() ? `<button class="action-button action-button--danger compact-button" data-delete-file="${file.id}" data-file-name="${escapeHtml(file.filename)}">Delete</button>` : ""}
|
||||
</div>
|
||||
<div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button class="action-button action-button--primary compact-button" data-copy-generated>Copy</button></div>
|
||||
</div>`).join("") : '<p class="dialog-copy">No files uploaded.</p>';
|
||||
onFilesChanged(files);
|
||||
if (open && !dialog.open) dialog.showModal();
|
||||
} catch (error) {
|
||||
if (open) toast(error.message);
|
||||
@@ -95,8 +107,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
});
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
const fileUrl = safeAppUrl(result.url);
|
||||
const text = file.type.startsWith("image/") ? `` : `[${file.name}](${fileUrl})`;
|
||||
const text = aliasCode(result.name, file.name, result.mime_type || file.type);
|
||||
editor.setRangeText(text, editor.selectionStart, editor.selectionEnd, "end");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
uploadToast.success();
|
||||
@@ -117,8 +128,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
list.addEventListener("click", async event => {
|
||||
const addButton = event.target.closest("[data-add-file-to-note]");
|
||||
if (addButton) {
|
||||
const relative = safeAppUrl(addButton.dataset.url);
|
||||
const text = addButton.dataset.mime?.startsWith("image/") ? `` : `[${addButton.dataset.name}](${relative})`;
|
||||
const text = aliasCode(addButton.dataset.name, addButton.dataset.name, addButton.dataset.mime);
|
||||
editor.setRangeText(text, editor.selectionStart, editor.selectionEnd, "end");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
toast("Added to note");
|
||||
@@ -130,9 +140,11 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, canDelete, to
|
||||
const output = panel.querySelector("textarea");
|
||||
const absolute = new URL(safeAppUrl(showButton.dataset.url), location.origin).href;
|
||||
let text = absolute;
|
||||
if (showButton.dataset.showFileCode === "markdown") {
|
||||
if (showButton.dataset.showFileCode === "alias") {
|
||||
text = aliasCode(showButton.dataset.name, showButton.dataset.name, showButton.dataset.mime);
|
||||
} else if (showButton.dataset.showFileCode === "markdown") {
|
||||
const relative = safeAppUrl(showButton.dataset.url);
|
||||
text = showButton.dataset.mime?.startsWith("image/") ? `` : `[${showButton.dataset.name}](${relative})`;
|
||||
text = markdownCode(relative, showButton.dataset.name, showButton.dataset.mime);
|
||||
}
|
||||
output.value = text; panel.hidden = false; output.focus(); output.select(); return;
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ installGlobalDiagnostics();
|
||||
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
|
||||
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown";
|
||||
import { toast } from "@rustpad/toast";
|
||||
|
||||
const token = location.pathname.split("/").filter(Boolean)[1];
|
||||
@@ -39,7 +39,7 @@ function scrollToPublicAnchor(hash, behavior = "auto") {
|
||||
target.scrollIntoView({ behavior, block: "start" });
|
||||
return true;
|
||||
}
|
||||
async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; content.innerHTML = renderMarkdown(page.content); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } }
|
||||
async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } }
|
||||
content.addEventListener("click", event => {
|
||||
const link = event.target.closest('.markdown-toc a[href^="#"]');
|
||||
if (!link) return;
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>__NOTE_TITLE__ · RustPad</title>
|
||||
__APP_STYLESHEET__
|
||||
__APP_IMPORT_MAP__
|
||||
__APP_ENTRYPOINT__
|
||||
</head>
|
||||
|
||||
<body class="pad-page" data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<header class="app-header">
|
||||
<div class="app-header__main"><a id="workspace-link" class="brand"
|
||||
href="/w/__WORKSPACE_SLUG__">__WORKSPACE_TITLE__</a><span class="header-divider"></span>
|
||||
<div class="document-heading">
|
||||
<h1 id="note-title">__NOTE_TITLE__</h1>
|
||||
<p id="note-url" class="document-url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="header-user-control user-color-control"><button id="current-user" class="user-chip" type="button"
|
||||
title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span
|
||||
class="user-chip__name"></span></button><input id="user-color-picker" class="user-color-picker"
|
||||
type="color" aria-label="Override color for this note"><button id="use-global-color"
|
||||
class="use-global-color" type="button" title="Use global profile color"
|
||||
aria-label="Use global profile color" hidden>↺</button></span>
|
||||
<div class="header-navigation">
|
||||
<button id="header-menu-toggle" class="header-menu-toggle" type="button" aria-expanded="false"
|
||||
aria-controls="header-actions" aria-label="Open navigation menu">
|
||||
<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>
|
||||
<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>
|
||||
</header>
|
||||
<main class="editor-layout">
|
||||
<section class="editor-panel">
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-group"><button data-format="bold" title="Bold"><strong>B</strong></button><button
|
||||
data-format="italic" title="Italic"><em>I</em></button><button data-format="strike"
|
||||
title="Strikethrough"><s>S</s></button><button data-format="heading1">H1</button><button
|
||||
data-format="heading2">H2</button><button data-format="heading3">H3</button><button
|
||||
data-format="heading4">H4</button><button data-format="bullet">• List</button><button
|
||||
data-format="number" title="Numbered list · Ctrl/Cmd+Shift+7">1. List</button><button
|
||||
data-format="task" title="Task list · Ctrl/Cmd+Shift+9">☑ Task</button><button
|
||||
data-format="quote">Quote</button><button data-format="link">Link</button>
|
||||
<details id="emoji-picker" class="emoji-picker">
|
||||
<summary title="Insert emoji" aria-label="Insert emoji">😀 Emoji</summary>
|
||||
<div class="emoji-picker-panel">
|
||||
<input id="emoji-search" class="emoji-search" type="search" placeholder="Search emoji…"
|
||||
autocomplete="off" aria-label="Search emoji">
|
||||
<div id="emoji-categories" class="emoji-categories" aria-label="Emoji categories"></div>
|
||||
<div id="emoji-grid" class="emoji-grid" role="listbox" aria-label="Emoji"></div>
|
||||
<p id="emoji-empty" class="emoji-empty" hidden>No emoji found.</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="markdown-more">
|
||||
<summary title="Extended Markdown">More</summary>
|
||||
<div class="markdown-more-menu"><button type="button" data-format="details">Collapsible
|
||||
section</button><button type="button" data-format="toc">Table of
|
||||
contents</button><button type="button" data-format="alert-success">Success
|
||||
alert</button><button type="button" data-format="alert-info">Info
|
||||
alert</button><button type="button" data-format="alert-warning">Warning
|
||||
alert</button><button type="button" data-format="alert-danger">Danger
|
||||
alert</button><button type="button" data-format="inline-code">Inline
|
||||
code</button><button type="button" data-format="codeblock">Code block</button><button
|
||||
type="button" data-format="codeblock-lines">Code block with line
|
||||
numbers</button><button type="button" data-format="mermaid">Mermaid
|
||||
diagram</button><button type="button" data-format="table">Table</button><button
|
||||
type="button" data-format="footnote">Footnote</button><button type="button"
|
||||
data-format="definition">Definition</button><button type="button"
|
||||
data-format="highlight">Highlight</button><button type="button"
|
||||
data-format="subscript">Subscript</button><button type="button"
|
||||
data-format="superscript">Superscript</button><button type="button"
|
||||
data-format="horizontal-rule">Horizontal rule</button></div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="editor-controls"><label>Font<select id="font-family">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="system">System</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="arial">Arial</option>
|
||||
<option value="georgia">Georgia</option>
|
||||
</select></label><label>Size<select id="font-size">
|
||||
<option value="14" selected>14</option>
|
||||
<option value="16">16</option>
|
||||
<option value="18">18</option>
|
||||
<option value="20">20</option>
|
||||
<option value="22">22</option>
|
||||
</select></label></div><button id="upload-button"
|
||||
class="toolbar-action">Image/file</button><input id="file-input" type="file" hidden><label
|
||||
class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Editor
|
||||
lines</label><label class="line-toggle"><input id="preview-line-numbers-toggle" type="checkbox">
|
||||
Preview lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox" checked>
|
||||
Compact</label><label class="line-toggle"><input id="line-links-toggle" type="checkbox">
|
||||
Line links</label>
|
||||
<div class="toolbar-fill"></div><button id="mode-toggle" class="markdown-toggle active"
|
||||
aria-pressed="true">Markdown</button>
|
||||
<div class="view-switch"><button data-view="edit">Edit</button><button data-view="split"
|
||||
class="active">Split</button><button data-view="preview">Preview</button></div>
|
||||
</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-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"
|
||||
wrap="off" placeholder="Start writing…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-column">
|
||||
<div id="preview-label" class="column-label">Markdown preview</div>
|
||||
<article id="preview" class="preview markdown-body"></article>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="editor-footer">
|
||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0
|
||||
words</span><span class="footer-connection-block"> · <span class="footer-status status"><span
|
||||
id="status-dot" class="status__dot"></span><span
|
||||
id="status-text">Connecting…</span></span> · <span id="socket-latency"
|
||||
class="footer-socket-latency" title="WebSocket round-trip time">— ms</span></span> ·
|
||||
<details id="room-details" class="room-details">
|
||||
<summary><span id="room-count">0 users</span><span id="chat-unread" class="chat-unread"
|
||||
hidden></span></summary>
|
||||
<div class="room-popover">
|
||||
<section class="room-users"><strong>In this room</strong>
|
||||
<ul id="room-users"></ul>
|
||||
</section>
|
||||
<section class="room-chat">
|
||||
<div class="room-chat__head"><strong>Room chat</strong><span>Messages disappear after
|
||||
disconnect</span></div>
|
||||
<div id="chat-messages" class="chat-messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="chat-form"><input id="chat-input" maxlength="1000"
|
||||
autocomplete="off" placeholder="Write a message…"
|
||||
aria-label="Chat message"><button type="submit">Send</button></form>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <span id="access-level"
|
||||
class="footer-access">Access: checking…</span> · <button id="footer-files" class="footer-link"
|
||||
type="button">0 files</button> · <span id="save-state">Changes are saved
|
||||
automatically</span></span>
|
||||
</footer>
|
||||
</section>
|
||||
<aside id="history-panel" class="history-panel" aria-hidden="true">
|
||||
<div class="history-header">
|
||||
<div>
|
||||
<h2>Change history</h2>
|
||||
<p>Author, time, and version preview</p>
|
||||
</div><button id="close-history" class="icon-button">×</button>
|
||||
</div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</aside>
|
||||
</main>
|
||||
<dialog id="shortcuts-dialog">
|
||||
<div class="dialog-panel shortcuts-panel">
|
||||
<div class="files-head">
|
||||
<div>
|
||||
<h2>Keyboard shortcuts</h2>
|
||||
<p>Use Ctrl on Windows/Linux or Cmd on macOS.</p>
|
||||
</div><button id="close-shortcuts" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="files-dialog" class="image-editor-dialog files-dialog">
|
||||
<div class="image-editor-panel files-panel">
|
||||
<div class="files-head">
|
||||
<div>
|
||||
<h2>Note files</h2>
|
||||
<p>Copy a direct link or ready Markdown/HTML code.</p>
|
||||
</div><button id="close-files" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<div id="files-list" class="files-list"></div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="identity-dialog">
|
||||
<form id="identity-form" autocomplete="on" class="dialog-panel identity-panel">
|
||||
<h2>What should we call you?</h2>
|
||||
<p class="dialog-copy">Use a free nickname without an account, or register it to reserve it.</p><input
|
||||
id="nickname" maxlength="40" autocomplete="off" data-bwignore="true" required
|
||||
placeholder="Name or nickname">
|
||||
<div class="identity-actions"><button id="guest-continue" class="primary-button" type="submit">Continue as
|
||||
guest</button><button id="show-register" class="text-button" type="button">Register</button><button
|
||||
id="show-login" class="text-button" type="button">Log in</button></div>
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail / organization login<input
|
||||
id="auth-email" name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password"
|
||||
type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button
|
||||
id="auth-submit" class="primary-button" type="submit">Log in and continue</button>
|
||||
<div class="identity-links"><button id="show-reset" class="text-button" type="button">Forgot
|
||||
password?</button><button id="auth-back" class="text-button" type="button">Back to
|
||||
nickname</button><button id="logout-account" class="text-button" type="button">Log out saved
|
||||
account</button></div>
|
||||
</section>
|
||||
<p id="identity-error" class="form-message error" role="alert"></p>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="password-dialog">
|
||||
<form id="password-form" class="dialog-panel">
|
||||
<h2>Protected workspace</h2><input id="open-password" type="password" autocomplete="current-password"
|
||||
required placeholder="Password">
|
||||
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
|
||||
class="dialog-link" href="/">Cancel</a>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="mobile-editor-bubble" class="mobile-editor-bubble" aria-label="Quick editor actions">
|
||||
<button id="mobile-bubble-drag" class="mobile-bubble-drag" type="button" title="Move quick actions"
|
||||
aria-label="Move quick actions">⋮⋮</button>
|
||||
<button id="mobile-files-button" type="button" title="Files" aria-label="Open files">📎</button>
|
||||
<label id="mobile-color-button" class="mobile-color-button" title="Editor color"
|
||||
aria-label="Change editor color"><span class="mobile-color-dot" aria-hidden="true"></span><input
|
||||
id="mobile-color-picker" type="color" aria-label="Change editor color"></label>
|
||||
<button id="mobile-chat-button" class="mobile-chat-button" type="button" title="Chat"
|
||||
aria-label="Open chat">💬<span id="mobile-chat-unread" class="mobile-chat-unread" hidden></span></button>
|
||||
<span id="mobile-connection-status" class="mobile-connection-status" title="WebSocket status"><span
|
||||
id="mobile-status-dot" class="status__dot"></span><span
|
||||
id="mobile-status-text">Connecting…</span></span>
|
||||
</div>
|
||||
<div id="toast" class="toast"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user