new functions
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
__pycache__
|
||||
/target
|
||||
rustpad.db
|
||||
rustpad.db-shm
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
ALTER TABLE published_pages ADD COLUMN allow_task_updates BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE published_pages ADD COLUMN allow_task_updates BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE published_pages ADD COLUMN allow_task_updates INTEGER NOT NULL DEFAULT 0;
|
||||
+43
-2
@@ -27,6 +27,7 @@ pub struct PublicPageResponse {
|
||||
title: String,
|
||||
content: String,
|
||||
updated_at: String,
|
||||
allow_task_updates: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -48,6 +49,20 @@ pub struct PasswordRequest {
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PublishRequest {
|
||||
#[serde(default)]
|
||||
password: Option<String>,
|
||||
#[serde(default)]
|
||||
allow_task_updates: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PublicTaskUpdateRequest {
|
||||
source_line: usize,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateNoteRequest {
|
||||
name: String,
|
||||
@@ -100,6 +115,7 @@ pub struct NoteInfo {
|
||||
title: String,
|
||||
protected: bool,
|
||||
note_protected: bool,
|
||||
allow_public_task_updates: bool,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -206,6 +222,7 @@ pub async fn note_info(
|
||||
title: note.title,
|
||||
protected: workspace.password_hash.is_some(),
|
||||
note_protected: note.protected,
|
||||
allow_public_task_updates: db::note_public_task_updates(&state.db, note.id).await?,
|
||||
created_at: db::normalize_timestamp(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
}))
|
||||
@@ -374,6 +391,7 @@ pub struct PadInfo {
|
||||
slug: String,
|
||||
title: String,
|
||||
protected: bool,
|
||||
allow_public_task_updates: bool,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -410,6 +428,7 @@ pub async fn pad_info(
|
||||
slug: pad.slug,
|
||||
title: pad.title,
|
||||
protected: pad.password_hash.is_some(),
|
||||
allow_public_task_updates: db::pad_public_task_updates(&state.db, pad.id).await?,
|
||||
created_at: db::normalize_timestamp(&pad.created_at),
|
||||
updated_at: db::normalize_timestamp(&pad.updated_at),
|
||||
}))
|
||||
@@ -418,20 +437,22 @@ pub async fn pad_info(
|
||||
pub async fn publish_pad_page(
|
||||
State(state): State<SharedState>,
|
||||
Path(slug): Path<String>,
|
||||
Json(payload): Json<PasswordRequest>,
|
||||
Json(payload): Json<PublishRequest>,
|
||||
) -> Result<Json<PublishResponse>, ApiError> {
|
||||
let pad = authorized_pad(&state, &slug, payload.password.as_deref()).await?;
|
||||
let token = db::publish_pad(&state.db, pad.id).await?;
|
||||
db::set_pad_public_task_updates(&state.db, pad.id, payload.allow_task_updates).await?;
|
||||
Ok(Json(PublishResponse { url: format!("/s/{token}") }))
|
||||
}
|
||||
|
||||
pub async fn publish_note_page(
|
||||
State(state): State<SharedState>,
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
Json(payload): Json<PasswordRequest>,
|
||||
Json(payload): Json<PublishRequest>,
|
||||
) -> Result<Json<PublishResponse>, ApiError> {
|
||||
let (_, note) = authorized_note(&state, &workspace_slug, ¬e_slug, payload.password.as_deref()).await?;
|
||||
let token = db::publish_note(&state.db, note.id).await?;
|
||||
db::set_note_public_task_updates(&state.db, note.id, payload.allow_task_updates).await?;
|
||||
Ok(Json(PublishResponse { url: format!("/s/{token}") }))
|
||||
}
|
||||
|
||||
@@ -446,6 +467,23 @@ pub async fn public_page(
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
updated_at: db::normalize_timestamp(&page.updated_at),
|
||||
allow_task_updates: page.allow_task_updates,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn update_public_task(
|
||||
State(state): State<SharedState>,
|
||||
Path(token): Path<String>,
|
||||
Json(payload): Json<PublicTaskUpdateRequest>,
|
||||
) -> Result<Json<PublicPageResponse>, ApiError> {
|
||||
let current = db::find_published_page(&state.db, &token).await?.ok_or_else(ApiError::not_found_note)?;
|
||||
if !current.allow_task_updates { return Err(ApiError::forbidden("Task updates are disabled for this page")); }
|
||||
let page = db::update_public_task(&state.db, &token, payload.source_line, payload.checked).await?.ok_or_else(ApiError::not_found_note)?;
|
||||
Ok(Json(PublicPageResponse {
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
updated_at: db::normalize_timestamp(&page.updated_at),
|
||||
allow_task_updates: page.allow_task_updates,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -767,6 +805,9 @@ impl ApiError {
|
||||
message: "Invalid password".into(),
|
||||
}
|
||||
}
|
||||
fn forbidden(message: &str) -> Self {
|
||||
Self { status: StatusCode::FORBIDDEN, message: message.into() }
|
||||
}
|
||||
fn not_found_workspace() -> Self {
|
||||
Self {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
|
||||
@@ -20,6 +20,7 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
|
||||
.route("/f/{token}/{filename}", get(api::download_file))
|
||||
.route("/files/{directory}/{filename}", get(api::download_legacy_file))
|
||||
.route("/api/public/{token}", get(api::public_page))
|
||||
.route("/api/public/{token}/tasks", post(api::update_public_task))
|
||||
.route("/api/pads", post(api::create_pad))
|
||||
.route("/api/pads/{slug}", get(api::pad_info))
|
||||
.route("/api/pads/{slug}/history", post(api::pad_history))
|
||||
|
||||
@@ -382,14 +382,42 @@ pub async fn list_pad_revisions(
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, FromRow)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PublishedPage {
|
||||
pub token: String,
|
||||
pub pad_id: Option<i64>,
|
||||
pub note_id: Option<i64>,
|
||||
pub allow_task_updates: bool,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
struct PublishedPageRow {
|
||||
token: String,
|
||||
pad_id: Option<i64>,
|
||||
note_id: Option<i64>,
|
||||
allow_task_updates: i64,
|
||||
title: String,
|
||||
content: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
impl From<PublishedPageRow> for PublishedPage {
|
||||
fn from(value: PublishedPageRow) -> Self {
|
||||
Self {
|
||||
token: value.token,
|
||||
pad_id: value.pad_id,
|
||||
note_id: value.note_id,
|
||||
allow_task_updates: value.allow_task_updates != 0,
|
||||
title: value.title,
|
||||
content: value.content,
|
||||
updated_at: value.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish_pad(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
|
||||
if let Some(token) = sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q017))
|
||||
.bind(pad_id)
|
||||
@@ -425,10 +453,63 @@ pub async fn publish_note(pool: &Database, note_id: i64) -> Result<String, sqlx:
|
||||
}
|
||||
|
||||
pub async fn find_published_page(pool: &Database, token: &str) -> Result<Option<PublishedPage>, sqlx::Error> {
|
||||
sqlx::query_as::<_, PublishedPage>(queries::get(pool.kind(), queries::Q021))
|
||||
.bind(token)
|
||||
.fetch_optional(pool.pool())
|
||||
.await
|
||||
Ok(sqlx::query_as::<_, PublishedPageRow>(queries::get(pool.kind(), queries::Q021))
|
||||
.bind(token)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
.map(Into::into))
|
||||
}
|
||||
|
||||
pub async fn pad_public_task_updates(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
|
||||
let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q044))
|
||||
.bind(pad_id)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
Ok(value != 0)
|
||||
}
|
||||
|
||||
pub async fn note_public_task_updates(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
|
||||
let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q045))
|
||||
.bind(note_id)
|
||||
.fetch_optional(pool.pool())
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
Ok(value != 0)
|
||||
}
|
||||
|
||||
pub async fn set_pad_public_task_updates(pool: &Database, pad_id: i64, allow: bool) -> Result<(), sqlx::Error> {
|
||||
publish_pad(pool, pad_id).await?;
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q040)).bind(if allow { 1i64 } else { 0i64 }).bind(pad_id).execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_note_public_task_updates(pool: &Database, note_id: i64, allow: bool) -> Result<(), sqlx::Error> {
|
||||
publish_note(pool, note_id).await?;
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q041)).bind(if allow { 1i64 } else { 0i64 }).bind(note_id).execute(pool.pool()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_public_task(pool: &Database, token: &str, source_line: usize, checked: bool) -> Result<Option<PublishedPage>, sqlx::Error> {
|
||||
let Some(mut page) = find_published_page(pool, token).await? else { return Ok(None); };
|
||||
if !page.allow_task_updates || source_line == 0 { return Ok(Some(page)); }
|
||||
let mut lines: Vec<String> = page.content.split('\n').map(str::to_owned).collect();
|
||||
let Some(line) = lines.get_mut(source_line - 1) else { return Ok(Some(page)); };
|
||||
let bytes = line.as_bytes();
|
||||
let mut i = 0usize;
|
||||
while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; }
|
||||
if i >= bytes.len() || !matches!(bytes[i], b'-' | b'*' | b'+') { return Ok(Some(page)); }
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; }
|
||||
if i + 2 >= bytes.len() || bytes[i] != b'[' || !matches!(bytes[i + 1], b' ' | b'x' | b'X') || bytes[i + 2] != b']' { return Ok(Some(page)); }
|
||||
line.replace_range(i + 1..i + 2, if checked { "x" } else { " " });
|
||||
page.content = lines.join("\n");
|
||||
if let Some(id) = page.pad_id {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q042)).bind(&page.content).bind(id).execute(pool.pool()).await?;
|
||||
} else if let Some(id) = page.note_id {
|
||||
sqlx::query(queries::get(pool.kind(), queries::Q043)).bind(&page.content).bind(id).execute(pool.pool()).await?;
|
||||
}
|
||||
find_published_page(pool, token).await
|
||||
}
|
||||
|
||||
pub async fn pad_file_token(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
|
||||
|
||||
+9
-1
@@ -21,7 +21,7 @@ pub const Q017: &str = "SELECT token FROM published_pages WHERE pad_id = ?";
|
||||
pub const Q018: &str = "INSERT INTO published_pages (token, pad_id) VALUES (?, ?)";
|
||||
pub const Q019: &str = "SELECT token FROM published_pages WHERE note_id = ?";
|
||||
pub const Q020: &str = "INSERT INTO published_pages (token, note_id) VALUES (?, ?)";
|
||||
pub const Q021: &str = "SELECT pp.token, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?";
|
||||
pub const Q021: &str = "SELECT pp.token, pp.pad_id, pp.note_id, CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?";
|
||||
pub const Q022: &str = "SELECT file_token FROM pads WHERE id = ?";
|
||||
pub const Q023: &str = "UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL";
|
||||
pub const Q024: &str = "SELECT file_token FROM notes WHERE id = ?";
|
||||
@@ -65,3 +65,11 @@ pub fn get(kind: DatabaseKind, query: &'static str) -> &'static str {
|
||||
cache.insert(cache_key, converted);
|
||||
converted
|
||||
}
|
||||
|
||||
pub const Q040: &str = "UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?";
|
||||
pub const Q041: &str = "UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?";
|
||||
pub const Q042: &str = "UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
pub const Q043: &str = "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
||||
|
||||
pub const Q044: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?";
|
||||
pub const Q045: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?";
|
||||
|
||||
@@ -398,3 +398,74 @@ dialog::backdrop { background: rgba(4,6,9,.82); }
|
||||
.note-delete-button:disabled { cursor: not-allowed; color: var(--muted-2); opacity: .55; }
|
||||
.notes-view-switch button { cursor: pointer; }
|
||||
.notes-view-switch button.active { cursor: default; }
|
||||
|
||||
/* Preview editing and source line numbers. */
|
||||
.preview { padding: 12px 24px 12px 62px; line-height: 1.32; }
|
||||
.preview-source-line { position: relative; min-height: 1.32em; }
|
||||
.preview-source-line::before { content: attr(data-source-line); position: absolute; right: calc(100% + 18px); width: 32px; color: #596270; text-align: right; font: 400 .72rem/1.32 ui-monospace, SFMono-Regular, Consolas, monospace; user-select: none; }
|
||||
.preview-editable { border-radius: 4px; outline: none; cursor: text; }
|
||||
.preview-editable:hover { background: rgba(255,255,255,.025); }
|
||||
.preview-editable:focus { background: rgba(124,104,238,.08); box-shadow: 0 0 0 1px rgba(124,104,238,.25); }
|
||||
.markdown-body p { margin: .18em 0; }
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin: .48em 0 .18em; }
|
||||
.markdown-body blockquote { margin: .3em 0; }
|
||||
.markdown-body ul, .markdown-body ol { margin: .22em 0; }
|
||||
.compact-editor .preview { line-height: 1.24; }
|
||||
.hide-line-numbers .preview { padding-left: 24px; }
|
||||
.hide-line-numbers .preview-source-line::before { display: none; }
|
||||
|
||||
|
||||
/* Extended Markdown */
|
||||
.markdown-body .table-wrap { overflow-x: auto; margin: .7em 0; }
|
||||
.markdown-body table { width: 100%; border-collapse: collapse; }
|
||||
.markdown-body th, .markdown-body td { padding: .55em .7em; border: 1px solid var(--border); vertical-align: top; }
|
||||
.markdown-body th { background: var(--surface-2); color: var(--text); }
|
||||
.markdown-body mark { padding: .05em .18em; border-radius: 3px; background: #6d5b16; color: #fff2a8; }
|
||||
.markdown-body sub, .markdown-body sup { line-height: 0; }
|
||||
.markdown-body dl { margin: .7em 0; }
|
||||
.markdown-body dt { font-weight: 750; }
|
||||
.markdown-body dd { margin: .25em 0 .65em 1.5em; color: #b9c2cf; }
|
||||
|
||||
.markdown-body .task-list{margin:0;padding:0;list-style:none}
|
||||
.markdown-body .task-list-item{position:relative;display:grid;grid-template-columns:1em minmax(0,1fr);grid-template-rows:1.32em;column-gap:.45em;align-items:center;min-height:1.32em;margin:0;padding:0;line-height:1.32}
|
||||
.markdown-body .task-list-item::before{position:absolute;top:0;right:calc(100% + 18px);width:32px;height:1.32em;line-height:1.32em;transform:none}
|
||||
.markdown-body .task-checkbox{grid-column:1;grid-row:1;width:1em;height:1em;margin:0;align-self:center;accent-color:var(--accent);cursor:pointer}
|
||||
.markdown-body .task-list-item>span{grid-column:2;grid-row:1;display:block;min-width:0;margin:0;padding:0;line-height:1.32}
|
||||
.hide-line-numbers .markdown-body .task-list-item::before{display:none}
|
||||
.compact-editor .markdown-body .task-list-item{grid-template-rows:1.24em;min-height:1.24em;line-height:1.24}
|
||||
.compact-editor .markdown-body .task-list-item::before,.compact-editor .markdown-body .task-list-item>span{height:1.24em;line-height:1.24}
|
||||
|
||||
.markdown-body .task-list-item>span{min-width:0}
|
||||
.markdown-body .footnotes { margin-top: 1.5em; color: var(--muted); font-size: .88em; }
|
||||
.markdown-body .footnotes ol { padding-left: 1.5em; }
|
||||
.markdown-body .footnote-backref { text-decoration: none; }
|
||||
.shortcuts-panel { width: 100%; }
|
||||
.shortcut-grid { display: grid; grid-template-columns: max-content 1fr; gap: 10px 18px; align-items: center; }
|
||||
.shortcut-grid kbd { padding: 5px 8px; border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 6px; background: #0d1015; color: #e6eaf0; font: 600 .76rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||
.shortcut-grid span { color: #c3cad4; font-size: .82rem; }
|
||||
@media (max-width: 520px) {
|
||||
.shortcut-grid { grid-template-columns: 1fr; gap: 5px; }
|
||||
.shortcut-grid span { margin-bottom: 7px; }
|
||||
}
|
||||
|
||||
.footer-left, .footer-right { display:flex; align-items:center; gap:6px; min-width:0; }
|
||||
.footer-status.status { min-height:0; padding:0; font-size:inherit; }
|
||||
.markdown-more { position:relative; }
|
||||
.markdown-more > summary { display:flex; align-items:center; min-height:34px; padding:0 9px; border:1px solid transparent; border-radius:7px; color:#b8c0cc; font-size:.78rem; list-style:none; cursor:pointer; }
|
||||
.markdown-more > summary::-webkit-details-marker { display:none; }
|
||||
.markdown-more > summary:hover, .markdown-more[open] > summary { border-color:var(--border); background:var(--surface-2); color:white; }
|
||||
.markdown-more-menu { position:absolute; top:calc(100% + 8px); left:0; z-index:30; display:grid; grid-template-columns:repeat(2,minmax(120px,1fr)); gap:4px; width:290px; padding:8px; border:1px solid var(--border-strong); border-radius:9px; background:#11151c; box-shadow:0 14px 40px rgba(0,0,0,.4); }
|
||||
.editor-toolbar .markdown-more-menu button { justify-content:flex-start; text-align:left; }
|
||||
.hljs { color:#d7dae0; }
|
||||
.hljs-keyword,.hljs-selector-tag,.hljs-literal { color:#c792ea; }
|
||||
.hljs-string,.hljs-attr { color:#c3e88d; }
|
||||
.hljs-number,.hljs-symbol { color:#f78c6c; }
|
||||
.hljs-comment { color:#697383; font-style:italic; }
|
||||
.public-content .task-checkbox { pointer-events:auto; }
|
||||
@media (max-width:720px){.footer-left,.footer-right{flex-wrap:wrap}.markdown-more-menu{position:fixed;left:12px;right:12px;top:auto;bottom:58px;width:auto;}}
|
||||
.public-task-toggle { display:inline-flex; align-items:center; gap:7px; color:var(--muted); font-size:.76rem; white-space:nowrap; }
|
||||
.public-task-toggle input { width:15px; min-height:15px; height:15px; margin:0; accent-color:var(--accent); }
|
||||
.public-page .preview-source-line { cursor:default; }
|
||||
.public-page .task-checkbox:not(:disabled) { cursor:pointer; }
|
||||
.public-page .task-checkbox:disabled { cursor:not-allowed; opacity:.55; }
|
||||
|
||||
|
||||
+73
-22
@@ -1,26 +1,77 @@
|
||||
function selection(editor) {
|
||||
return { start: editor.selectionStart, end: editor.selectionEnd };
|
||||
}
|
||||
|
||||
function toggleWrap(editor, before, after = before, placeholder = "tekst") {
|
||||
let { start, end } = selection(editor);
|
||||
const value = editor.value;
|
||||
const selected = value.slice(start, end);
|
||||
|
||||
if (start >= before.length && value.slice(start - before.length, start) === before && value.slice(end, end + after.length) === after) {
|
||||
editor.setRangeText(selected, start - before.length, end + after.length, "select");
|
||||
return;
|
||||
}
|
||||
if (selected.startsWith(before) && selected.endsWith(after) && selected.length >= before.length + after.length) {
|
||||
editor.setRangeText(selected.slice(before.length, -after.length), start, end, "select");
|
||||
return;
|
||||
}
|
||||
|
||||
const text = selected || placeholder;
|
||||
editor.setRangeText(before + text + after, start, end, "select");
|
||||
if (!selected) editor.setSelectionRange(start + before.length, start + before.length + text.length);
|
||||
}
|
||||
|
||||
function togglePrefix(editor, prefixFactory) {
|
||||
const { start, end } = selection(editor);
|
||||
const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1;
|
||||
const selected = editor.value.slice(lineStart, end);
|
||||
const lines = selected.split("\n");
|
||||
const prefixes = lines.map((_, index) => typeof prefixFactory === "function" ? prefixFactory(index) : prefixFactory);
|
||||
const allPrefixed = lines.every((line, index) => line.startsWith(prefixes[index]));
|
||||
const replacement = lines.map((line, index) => allPrefixed ? line.slice(prefixes[index].length) : prefixes[index] + line).join("\n");
|
||||
editor.setRangeText(replacement, lineStart, end, "select");
|
||||
}
|
||||
|
||||
export function applyFormat(editor, format) {
|
||||
const wrap = (before, after = before, placeholder = "tekst") => {
|
||||
const start = editor.selectionStart, end = editor.selectionEnd;
|
||||
const selected = editor.value.slice(start, end) || placeholder;
|
||||
editor.setRangeText(before + selected + after, start, end, "select");
|
||||
};
|
||||
const prefix = (value) => {
|
||||
const start = editor.selectionStart, end = editor.selectionEnd;
|
||||
const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1;
|
||||
const selected = editor.value.slice(lineStart, end);
|
||||
editor.setRangeText(selected.split("\n").map((line, index) => typeof value === "function" ? value(index) + line : value + line).join("\n"), lineStart, end, "select");
|
||||
};
|
||||
if (format === "bold") wrap("**");
|
||||
if (format === "italic") wrap("*");
|
||||
if (format === "strike") wrap("~~");
|
||||
if (format === "heading" || format === "heading2") prefix("## ");
|
||||
if (format === "heading1") prefix("# ");
|
||||
if (format === "heading3") prefix("### ");
|
||||
if (format === "heading4") prefix("#### ");
|
||||
if (format === "bullet") prefix("- ");
|
||||
if (format === "number") prefix((index) => `${index + 1}. `);
|
||||
if (format === "quote") prefix("> ");
|
||||
if (format === "link") wrap("[", "](https://)", "description");
|
||||
if (format === "bold") toggleWrap(editor, "**");
|
||||
if (format === "italic") toggleWrap(editor, "*");
|
||||
if (format === "strike") toggleWrap(editor, "~~");
|
||||
if (format === "heading" || format === "heading2") togglePrefix(editor, "## ");
|
||||
if (format === "heading1") togglePrefix(editor, "# ");
|
||||
if (format === "heading3") togglePrefix(editor, "### ");
|
||||
if (format === "heading4") togglePrefix(editor, "#### ");
|
||||
if (format === "bullet") togglePrefix(editor, "- ");
|
||||
if (format === "number") togglePrefix(editor, index => `${index + 1}. `);
|
||||
if (format === "task") togglePrefix(editor, "- [ ] ");
|
||||
if (format === "quote") togglePrefix(editor, "> ");
|
||||
if (format === "link") toggleWrap(editor, "[", "](https://)", "description");
|
||||
if (format === "inline-code") toggleWrap(editor, "`", "`", "code");
|
||||
if (format === "highlight") toggleWrap(editor, "==", "==", "important");
|
||||
if (format === "subscript") toggleWrap(editor, "~", "~", "2");
|
||||
if (format === "superscript") toggleWrap(editor, "^", "^", "2");
|
||||
if (format === "codeblock") toggleWrap(editor, "```text\n", "\n```", "code");
|
||||
if (format === "table") toggleWrap(editor, "| Column 1 | Column 2 |\n| --- | --- |\n| ", " | value |", "value");
|
||||
if (format === "footnote") toggleWrap(editor, "", "[^1]\n\n[^1]: Footnote text", "Text with footnote");
|
||||
if (format === "definition") toggleWrap(editor, "", "\n: Definition", "Term");
|
||||
if (format === "horizontal-rule") toggleWrap(editor, "\n---\n", "", "");
|
||||
editor.focus();
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
|
||||
export function bindFormatShortcuts(editor) {
|
||||
editor.addEventListener("keydown", event => {
|
||||
const primary = event.ctrlKey || event.metaKey;
|
||||
let format = null;
|
||||
if (primary && !event.shiftKey && event.key.toLowerCase() === "b") format = "bold";
|
||||
else if (primary && !event.shiftKey && event.key.toLowerCase() === "i") format = "italic";
|
||||
else if (primary && event.shiftKey && event.key.toLowerCase() === "x") format = "strike";
|
||||
else if (primary && !event.shiftKey && event.key.toLowerCase() === "k") format = "link";
|
||||
else if (primary && event.shiftKey && event.key === "7") format = "number";
|
||||
else if (primary && event.shiftKey && event.key === "8") format = "bullet";
|
||||
else if (primary && event.shiftKey && event.key === "9") format = "task";
|
||||
else if (event.altKey && /^[1-4]$/.test(event.key)) format = `heading${event.key}`;
|
||||
if (!format) return;
|
||||
event.preventDefault();
|
||||
applyFormat(editor, format);
|
||||
});
|
||||
}
|
||||
|
||||
+156
-23
@@ -4,48 +4,181 @@ function escapeHtml(value) {
|
||||
|
||||
function safeUrl(value) {
|
||||
const url = String(value).trim();
|
||||
if (/^(https?:\/\/|\/|\.\/|\.\.\/|#)/i.test(url)) return escapeHtml(url);
|
||||
if (/^(https?:\/\/|mailto:|\/|\.\/|\.\.\/|#)/i.test(url)) return escapeHtml(url);
|
||||
return "#";
|
||||
}
|
||||
|
||||
const emoji = {
|
||||
smile:"😄", joy:"😂", heart:"❤️", thumbs_up:"👍", thumbsup:"👍", thumbs_down:"👎",
|
||||
fire:"🔥", rocket:"🚀", tada:"🎉", warning:"⚠️", white_check_mark:"✅", x:"❌",
|
||||
eyes:"👀", bulb:"💡", memo:"📝", pushpin:"📌", bug:"🐛", sparkles:"✨",
|
||||
thinking:"🤔", clap:"👏", ok_hand:"👌", pray:"🙏", muscle:"💪", coffee:"☕",
|
||||
tent:"⛺", star:"⭐", checkered_flag:"🏁"
|
||||
};
|
||||
|
||||
function inline(value) {
|
||||
const tokens = [];
|
||||
let html = escapeHtml(value);
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => {
|
||||
const token = `\u0000IMG${tokens.length}\u0000`;
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
tokens.push(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async"${titleAttr}>`);
|
||||
const stash = html => {
|
||||
const token = `\u0000T${tokens.length}\u0000`;
|
||||
tokens.push(html);
|
||||
return token;
|
||||
};
|
||||
let html = escapeHtml(value);
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, (_, code) => stash(`<code>${code}</code>`));
|
||||
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"${titleAttr}>`);
|
||||
});
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => {
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
return stash(`<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer"${titleAttr}>${label}</a>`);
|
||||
});
|
||||
html = html.replace(/\[\^([^\]\s]+)\]/g, (_, id) => stash(`<sup class="footnote-ref"><a href="#fn-${escapeHtml(id)}" id="fnref-${escapeHtml(id)}">?</a></sup>`));
|
||||
|
||||
html = html
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
||||
.replace(/==([^=]+)==/g, "<mark>$1</mark>")
|
||||
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
||||
.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => {
|
||||
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
||||
return `<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer"${titleAttr}>${label}</a>`;
|
||||
});
|
||||
return html.replace(/\u0000IMG(\d+)\u0000/g, (_, index) => tokens[Number(index)] || "");
|
||||
.replace(/(?<!~)~([^~\n]+)~(?!~)/g, "<sub>$1</sub>")
|
||||
.replace(/\^([^^\n]+)\^/g, "<sup>$1</sup>")
|
||||
.replace(/:([a-z0-9_+-]+):/gi, (match, name) => emoji[name] || match);
|
||||
|
||||
html = html.replace(/(^|[\s(])((?:https?:\/\/|mailto:)[^\s<]+)/gi, (match, prefix, url) => {
|
||||
const clean = url.replace(/[.,!?;:]+$/, "");
|
||||
const suffix = url.slice(clean.length);
|
||||
return `${prefix}${stash(`<a href="${safeUrl(clean)}" target="_blank" rel="noopener noreferrer">${clean}</a>`)}${suffix}`;
|
||||
});
|
||||
|
||||
return html.replace(/\u0000T(\d+)\u0000/g, (_, index) => tokens[Number(index)] || "");
|
||||
}
|
||||
|
||||
const attrs = (line, editable = false, prefix = "", suffix = "") => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
|
||||
const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line);
|
||||
|
||||
function splitTableRow(line) {
|
||||
let value = line.trim();
|
||||
if (value.startsWith("|")) value = value.slice(1);
|
||||
if (value.endsWith("|")) value = value.slice(0, -1);
|
||||
return value.split("|").map(cell => cell.trim().replace(/|/g, "|"));
|
||||
}
|
||||
|
||||
function tableDelimiter(line) {
|
||||
const cells = splitTableRow(line);
|
||||
if (!cells.length || !cells.every(cell => /^:?-{3,}:?$/.test(cell))) return null;
|
||||
return cells.map(cell => cell.startsWith(":") && cell.endsWith(":") ? "center" : cell.endsWith(":") ? "right" : "left");
|
||||
}
|
||||
|
||||
export function renderMarkdown(source) {
|
||||
let html = "", inCode = false, language = "", code = [], list = null;
|
||||
let html = "", inCode = false, fence = "", language = "", code = [], codeStart = 0, list = null;
|
||||
const lines = String(source).split("\n");
|
||||
const footnotes = new Map();
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const match = lines[i].match(/^\[\^([^\]\s]+)\]:\s*(.*)$/);
|
||||
if (!match) continue;
|
||||
const body = [match[2]];
|
||||
let j = i + 1;
|
||||
while (j < lines.length && /^(?: {4}|\t)/.test(lines[j])) {
|
||||
body.push(lines[j].replace(/^(?: {4}|\t)/, ""));
|
||||
lines[j] = "";
|
||||
j++;
|
||||
}
|
||||
footnotes.set(match[1], body.join("\n"));
|
||||
lines[i] = "";
|
||||
}
|
||||
|
||||
const closeList = () => { if (list) { html += `</${list}>`; list = null; } };
|
||||
const closeCode = () => {
|
||||
const body = escapeHtml(code.join("\n"));
|
||||
html += language.toLowerCase() === "mermaid"
|
||||
? `<div class="mermaid">${body}</div>`
|
||||
: `<pre><code class="language-${escapeHtml(language)}">${body}</code></pre>`;
|
||||
code = []; language = "";
|
||||
? `<div class="mermaid preview-source-line" data-source-line="${codeStart + 1}">${body}</div>`
|
||||
: `<pre${attrs(codeStart)}><code class="language-${escapeHtml(language)}">${body}</code></pre>`;
|
||||
code = []; language = ""; fence = "";
|
||||
};
|
||||
for (const line of String(source).split("\n")) {
|
||||
if (line.startsWith("```")) { closeList(); if (inCode) closeCode(); else language = line.slice(3).trim(); inCode = !inCode; continue; }
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index];
|
||||
const fenceMatch = line.match(/^(```+|~~~+)\s*([^\s]*)\s*$/);
|
||||
if (fenceMatch) {
|
||||
closeList();
|
||||
if (inCode && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) closeCode();
|
||||
else if (!inCode) { fence = fenceMatch[1]; language = fenceMatch[2] || ""; codeStart = index; }
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode) { code.push(line); continue; }
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/), ul = line.match(/^\s*[-*+]\s+(.+)$/), ol = line.match(/^\s*\d+\.\s+(.+)$/);
|
||||
if (heading) { closeList(); const n = heading[1].length; html += `<h${n}>${inline(heading[2])}</h${n}>`; }
|
||||
else if (ul || ol) { const type = ul ? "ul" : "ol"; if (list !== type) { closeList(); html += `<${type}>`; list = type; } html += `<li>${inline((ul || ol)[1])}</li>`; }
|
||||
else { closeList(); if (/^---+$/.test(line)) html += "<hr>"; else if (line.startsWith("> ")) html += `<blockquote>${inline(line.slice(2))}</blockquote>`; else if (line.trim()) html += `<p>${inline(line)}</p>`; else html += "<br>"; }
|
||||
|
||||
const delimiter = index + 1 < lines.length ? tableDelimiter(lines[index + 1]) : null;
|
||||
if (line.includes("|") && delimiter) {
|
||||
closeList();
|
||||
const headers = splitTableRow(line);
|
||||
html += `<div class="table-wrap preview-source-line" data-source-line="${index + 1}"><table><thead><tr>`;
|
||||
headers.forEach((cell, i) => html += `<th style="text-align:${delimiter[i] || "left"}">${inline(cell)}</th>`);
|
||||
html += `</tr></thead><tbody>`;
|
||||
index += 2;
|
||||
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
||||
const cells = splitTableRow(lines[index]);
|
||||
html += `<tr>`;
|
||||
headers.forEach((_, i) => html += `<td style="text-align:${delimiter[i] || "left"}">${inline(cells[i] || "")}</td>`);
|
||||
html += `</tr>`;
|
||||
index++;
|
||||
}
|
||||
html += `</tbody></table></div>`;
|
||||
index--;
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = line.match(/^(#{1,6})\s+(.+?)(?:\s+\{#([A-Za-z][\w:.-]*)\})?\s*$/);
|
||||
const task = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.+)$/);
|
||||
const ul = line.match(/^\s*[-*+]\s+(.+)$/);
|
||||
const ol = line.match(/^\s*\d+\.\s+(.+)$/);
|
||||
|
||||
if (heading) {
|
||||
closeList();
|
||||
const n = heading[1].length;
|
||||
const id = heading[3] ? ` id="${escapeHtml(heading[3])}"` : "";
|
||||
const suffix = heading[3] ? ` {#${heading[3]}}` : "";
|
||||
html += `<h${n}${id}${attrs(index, true, `${heading[1]} `, suffix)}>${inline(heading[2])}</h${n}>`;
|
||||
} else if (task) {
|
||||
if (list !== "ul") { closeList(); html += `<ul class="task-list">`; list = "ul"; }
|
||||
const checked = task[2].toLowerCase() === "x";
|
||||
html += `<li class="preview-source-line task-list-item" data-source-line="${index + 1}"><input type="checkbox" class="task-checkbox" data-source-line="${index + 1}"${checked ? " checked" : ""}><span>${inline(task[3])}</span></li>`;
|
||||
} else if (ul || ol) {
|
||||
const type = ul ? "ul" : "ol";
|
||||
if (list !== type) { closeList(); html += `<${type}>`; list = type; }
|
||||
html += `<li${attrs(index)}>${inline((ul || ol)[1])}</li>`;
|
||||
} else {
|
||||
closeList();
|
||||
const definition = index + 1 < lines.length && /^:\s+/.test(lines[index + 1]);
|
||||
if (line.trim() && definition) {
|
||||
html += `<dl${attrs(index)}><dt>${inline(line)}</dt>`;
|
||||
while (index + 1 < lines.length && /^:\s+/.test(lines[index + 1])) {
|
||||
index++;
|
||||
html += `<dd data-source-line="${index + 1}">${inline(lines[index].replace(/^:\s+/, ""))}</dd>`;
|
||||
}
|
||||
html += `</dl>`;
|
||||
} else if (/^---+$/.test(line.trim())) html += `<hr${attrs(index)}>`;
|
||||
else if (line.startsWith("> ")) html += `<blockquote${attrs(index)}>${inline(line.slice(2))}</blockquote>`;
|
||||
else if (line.trim()) html += `<p${attrs(index, isPlainText(line))}>${inline(line)}</p>`;
|
||||
else html += `<div${attrs(index, true)}><br></div>`;
|
||||
}
|
||||
}
|
||||
closeList(); if (inCode) closeCode(); return html;
|
||||
|
||||
closeList();
|
||||
if (inCode) closeCode();
|
||||
|
||||
if (footnotes.size) {
|
||||
html = html.replace(/<sup class="footnote-ref"><a href="#fn-([^"]+)" id="fnref-\1">\?<\/a><\/sup>/g, (_, id) => {
|
||||
const order = [...footnotes.keys()].indexOf(id) + 1;
|
||||
return `<sup class="footnote-ref"><a href="#fn-${id}" id="fnref-${id}">${order || "?"}</a></sup>`;
|
||||
});
|
||||
html += `<section class="footnotes"><hr><ol>`;
|
||||
for (const [id, body] of footnotes) {
|
||||
html += `<li id="fn-${escapeHtml(id)}">${body.split("\n").map(part => inline(part)).join("<br>")} <a class="footnote-backref" href="#fnref-${escapeHtml(id)}" aria-label="Back to reference">↩</a></li>`;
|
||||
}
|
||||
html += `</ol></section>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
+9
-8
@@ -1,6 +1,6 @@
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { applyFormat } from "@rustpad/editor-format";
|
||||
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
import { prepareImageFile } from "./image-upload.js";
|
||||
import { getNickname, getPassword, setNickname, setPassword } from "@rustpad/session";
|
||||
@@ -10,17 +10,18 @@ import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url
|
||||
const parts=location.pathname.split("/").filter(Boolean), workspaceSlug=parts[1], noteSlug=parts[3];
|
||||
const editor=document.querySelector("#editor"), preview=document.querySelector("#preview"), editorWorkspace=document.querySelector("#editor-workspace"), gutter=document.querySelector("#line-gutter"), ownerLabels=document.querySelector("#owner-labels");
|
||||
const modeToggle=document.querySelector("#mode-toggle"), passwordDialog=document.querySelector("#password-dialog"), identityDialog=document.querySelector("#identity-dialog");
|
||||
const compactToggle=document.querySelector("#compact-toggle"), fontFamily=document.querySelector("#font-family"), fontSize=document.querySelector("#font-size");
|
||||
const compactToggle=document.querySelector("#compact-toggle"), publicTaskUpdates=document.querySelector("#public-task-updates"), fontFamily=document.querySelector("#font-family"), fontSize=document.querySelector("#font-size");
|
||||
let password=getPassword(workspaceSlug), nickname=getNickname(), info, socket, saveTimer, applyingRemote=false, uiState=readEditorState(), owners=[];
|
||||
const lineToggle=document.querySelector("#line-numbers-toggle"); lineToggle.checked=localStorage.getItem("rustpad:line-numbers")!=="off";
|
||||
compactToggle.checked=localStorage.getItem("rustpad:compact")==="on";
|
||||
compactToggle.checked=localStorage.getItem("rustpad:compact")!=="off";
|
||||
fontFamily.value=localStorage.getItem("rustpad:font-family")||"mono";
|
||||
fontSize.value=localStorage.getItem("rustpad:font-size")||"18";
|
||||
fontSize.value=localStorage.getItem("rustpad:font-size")||"14";
|
||||
function colorFor(name){let h=0;for(const c of name||"?")h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 70% 62%)`;}
|
||||
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
|
||||
function setStatus(kind,text){document.querySelector("#status-dot").className=`status__dot${kind?` is-${kind}`:""}`;document.querySelector("#status-text").textContent=text;}
|
||||
function updateAddressLabel(){document.querySelector("#note-url").textContent=`${location.pathname}${location.search}`;}
|
||||
async function renderMermaid(){const nodes=preview.querySelectorAll(".mermaid");if(!nodes.length)return;try{const {default:mermaid}=await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs");mermaid.initialize({startOnLoad:false,theme:"dark",securityLevel:"strict"});await mermaid.run({nodes:[...nodes]});}catch{nodes.forEach(n=>n.insertAdjacentHTML("beforebegin",'<p class="error">Failed to load Mermaid.</p>'));}}
|
||||
async function renderCodeHighlight(){const nodes=preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)');if(!nodes.length)return;try{const hljs=await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm");nodes.forEach(node=>hljs.default.highlightElement(node));}catch{}}
|
||||
function renderGutter(){
|
||||
const lineCount=Math.max(1,(editor.value.match(/\n/g)||[]).length+1);
|
||||
const lines=Array.from({length:lineCount});
|
||||
@@ -40,7 +41,7 @@ function renderGutter(){
|
||||
document.body.classList.toggle("hide-line-numbers",!lineToggle.checked);
|
||||
}
|
||||
function escapeHtml(v){return String(v).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));}function formatDate(value){const raw=String(value??"").trim();let normalized=raw;if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}$/.test(normalized))normalized=normalized.replace(" ","T")+":00";else if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/.test(normalized))normalized=normalized.replace(" ","T");else if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(normalized))normalized=normalized.replace(" ","T")+"Z";const date=new Date(normalized);return Number.isNaN(date.getTime())?raw:date.toLocaleString("pl-PL",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"});}
|
||||
function render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview";renderMermaid();}else{preview.classList.add("preview--raw");preview.textContent=editor.value;document.querySelector("#preview-label").textContent="Source text";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
|
||||
function render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview · text and headings are editable";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}" contenteditable="true" spellcheck="true">${escapeHtml(line)||"<br>"}</div>`).join("");document.querySelector("#preview-label").textContent="Text preview · editable";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
|
||||
function applyUi({write=false,replace=false}={}){editorWorkspace.className=`workspace view-${uiState.view} editor-workspace-font-${fontFamily.value}`;editorWorkspace.style.setProperty("--editor-font-size",`${fontSize.value}px`);document.body.classList.toggle("compact-editor",compactToggle.checked);document.querySelectorAll("[data-view]").forEach(b=>{const a=b.dataset.view===uiState.view;b.classList.toggle("active",a);b.setAttribute("aria-pressed",String(a));});const markdown=uiState.mode==="markdown";modeToggle.classList.toggle("active",markdown);modeToggle.textContent=markdown?"Markdown":"Text";render();if(write)writeEditorState(uiState,{replace});updateAddressLabel();}
|
||||
function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null)return;const start=editor.selectionStart,end=editor.selectionEnd;applyingRemote=true;editor.value=content;try{owners=JSON.parse(ownerMap||"[]");}catch{owners=[];}editor.setSelectionRange(Math.min(start,content.length),Math.min(end,content.length));applyingRemote=false;render();}
|
||||
function connect(){socket?.stop();socket=new NoteSocket({workspaceSlug,noteSlug,password,nickname,onStatus:s=>setStatus(s==="online"?"online":s==="offline"?"offline":null,s==="online"?"Connected":s==="offline"?"Reconnecting…":"Connecting…"),onAuthenticated:m=>{if(passwordDialog.open)passwordDialog.close();applyRemote(m.content,m.owner_map);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"})}`;},onError:m=>{document.querySelector("#password-error").textContent=m;if(info?.protected&&!passwordDialog.open)passwordDialog.showModal();}});socket.connect();}
|
||||
@@ -55,11 +56,11 @@ async function loadFiles({open=false}={}){
|
||||
if(open&&!document.querySelector("#files-dialog").open)document.querySelector("#files-dialog").showModal();
|
||||
}catch(error){toast(error.message);}
|
||||
}
|
||||
async function initialize(){try{info=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`);document.querySelector("#back-workspace").href=`/w/${encodeURIComponent(workspaceSlug)}`;document.title=`${info.title} · ${info.workspace_title}`;applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));document.querySelector("#delete-note").hidden=info.note_protected;if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
|
||||
async function initialize(){try{info=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}`);document.querySelector("#back-workspace").href=`/w/${encodeURIComponent(workspaceSlug)}`;document.title=`${info.title} · ${info.workspace_title}`;publicTaskUpdates.checked=Boolean(info.allow_public_task_updates);applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));document.querySelector("#delete-note").hidden=info.note_protected;if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
|
||||
document.querySelector("#identity-form").addEventListener("submit",e=>{e.preventDefault();nickname=document.querySelector("#nickname").value.trim();setNickname(nickname);identityDialog.close();document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));document.querySelector("#delete-note").hidden=info.note_protected;if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}});
|
||||
document.querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>{uiState={...uiState,view:b.dataset.view};applyUi({write:true});}));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();});compactToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:compact",compactToggle.checked?"on":"off");applyUi();});fontFamily.addEventListener("change",()=>{localStorage.setItem("rustpad:font-family",fontFamily.value);applyUi();});fontSize.addEventListener("change",()=>{localStorage.setItem("rustpad:font-size",fontSize.value);applyUi();});
|
||||
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)));
|
||||
document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null})});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);}});
|
||||
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);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();}});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 prefix=target.dataset.sourcePrefix||"",suffix=target.dataset.sourceSuffix||"";const next=prefix+(target.innerText||"").replace(/\n/g," ")+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});
|
||||
publicTaskUpdates.addEventListener("change",async()=>{publicTaskUpdates.disabled=true;try{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null,allow_task_updates:publicTaskUpdates.checked})});toast(publicTaskUpdates.checked?"Public task updates enabled":"Public task updates disabled");}catch(error){publicTaskUpdates.checked=!publicTaskUpdates.checked;toast(error.message);}finally{publicTaskUpdates.disabled=false;}});document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null,allow_task_updates:publicTaskUpdates.checked})});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);}});
|
||||
editor.addEventListener("scroll",()=>{gutter.scrollTop=editor.scrollTop;renderGutter();});editor.addEventListener("input",()=>{const newLines=editor.value.split("\n").length;const cursorLine=editor.value.slice(0,editor.selectionStart).split("\n").length-1;while(owners.length<newLines)owners.push(nickname);owners=owners.slice(0,newLines);owners[cursorLine]=nickname;render();if(applyingRemote)return;clearTimeout(saveTimer);document.querySelector("#save-state").textContent="Saving…";saveTimer=setTimeout(()=>socket?.update(editor.value,JSON.stringify(owners)),250);});
|
||||
document.querySelector("#password-form").addEventListener("submit",e=>{e.preventDefault();password=document.querySelector("#open-password").value;setPassword(workspaceSlug,password);document.querySelector("#password-error").textContent="";loadFiles();connect();});
|
||||
const historyPanel=document.querySelector("#history-panel");document.querySelector("#history-button").addEventListener("click",async()=>{historyPanel.classList.add("open");historyPanel.setAttribute("aria-hidden","false");document.body.classList.add("history-open");const list=document.querySelector("#history-list");list.innerHTML='<p class="empty">Loading…</p>';try{const revisions=await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/history`,{method:"POST",body:JSON.stringify({password:password||null})});list.innerHTML=revisions.length?revisions.map((r,i)=>{const snippet=escapeHtml(r.content.trim().split("\n").slice(0,3).join(" · ").slice(0,150)||"Empty note");const author=r.author||"Unknown author";return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`;}).join(""):'<p class="empty">No history yet.</p>';for(const r of revisions){list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click",()=>{const el=list.querySelector(`#preview-${r.id}`);el.hidden=!el.hidden;el.textContent=r.content;});list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click",async()=>{await api(`/api/workspaces/${encodeURIComponent(workspaceSlug)}/notes/${encodeURIComponent(noteSlug)}/restore`,{method:"POST",body:JSON.stringify({password:password||null,revision_id:r.id})});toast("Version restored");});}}catch(e){list.innerHTML=`<p class="error">${escapeHtml(e.message)}</p>`;}});document.querySelector("#close-history").addEventListener("click",()=>{historyPanel.classList.remove("open");historyPanel.setAttribute("aria-hidden","true");document.body.classList.remove("history-open");});
|
||||
|
||||
+9
-8
@@ -1,6 +1,6 @@
|
||||
import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { applyFormat } from "@rustpad/editor-format";
|
||||
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
import { prepareImageFile } from "./image-upload.js";
|
||||
import { getNickname, setNickname } from "@rustpad/session";
|
||||
@@ -10,17 +10,18 @@ import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url
|
||||
const slug=location.pathname.split("/").filter(Boolean)[1];
|
||||
const editor=document.querySelector("#editor"), preview=document.querySelector("#preview"), editorWorkspace=document.querySelector("#editor-workspace"), gutter=document.querySelector("#line-gutter"), ownerLabels=document.querySelector("#owner-labels");
|
||||
const modeToggle=document.querySelector("#mode-toggle"), passwordDialog=document.querySelector("#password-dialog"), identityDialog=document.querySelector("#identity-dialog");
|
||||
const compactToggle=document.querySelector("#compact-toggle"), fontFamily=document.querySelector("#font-family"), fontSize=document.querySelector("#font-size");
|
||||
const compactToggle=document.querySelector("#compact-toggle"), publicTaskUpdates=document.querySelector("#public-task-updates"), fontFamily=document.querySelector("#font-family"), fontSize=document.querySelector("#font-size");
|
||||
let password=sessionStorage.getItem(`rustpad:pad:${slug}:password`)||"", nickname=getNickname(), info, socket, saveTimer, applyingRemote=false, uiState=readEditorState(), owners=[];
|
||||
const lineToggle=document.querySelector("#line-numbers-toggle"); lineToggle.checked=localStorage.getItem("rustpad:line-numbers")!=="off";
|
||||
compactToggle.checked=localStorage.getItem("rustpad:compact")==="on";
|
||||
compactToggle.checked=localStorage.getItem("rustpad:compact")!=="off";
|
||||
fontFamily.value=localStorage.getItem("rustpad:font-family")||"mono";
|
||||
fontSize.value=localStorage.getItem("rustpad:font-size")||"18";
|
||||
fontSize.value=localStorage.getItem("rustpad:font-size")||"14";
|
||||
function colorFor(name){let h=0;for(const c of name||"?")h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 70% 62%)`;}
|
||||
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
|
||||
function setStatus(kind,text){document.querySelector("#status-dot").className=`status__dot${kind?` is-${kind}`:""}`;document.querySelector("#status-text").textContent=text;}
|
||||
function updateAddressLabel(){document.querySelector("#pad-url").textContent=`${location.pathname}${location.search}`;}
|
||||
async function renderMermaid(){const nodes=preview.querySelectorAll(".mermaid");if(!nodes.length)return;try{const {default:mermaid}=await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs");mermaid.initialize({startOnLoad:false,theme:"dark",securityLevel:"strict"});await mermaid.run({nodes:[...nodes]});}catch{nodes.forEach(n=>n.insertAdjacentHTML("beforebegin",'<p class="error">Failed to load Mermaid.</p>'));}}
|
||||
async function renderCodeHighlight(){const nodes=preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)');if(!nodes.length)return;try{const hljs=await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm");nodes.forEach(node=>hljs.default.highlightElement(node));}catch{}}
|
||||
function renderGutter(){
|
||||
const lineCount=Math.max(1,(editor.value.match(/\n/g)||[]).length+1);
|
||||
const lines=Array.from({length:lineCount});
|
||||
@@ -40,7 +41,7 @@ function renderGutter(){
|
||||
document.body.classList.toggle("hide-line-numbers",!lineToggle.checked);
|
||||
}
|
||||
function escapeHtml(v){return String(v).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));}function formatDate(value){const raw=String(value??"").trim();let normalized=raw;if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}$/.test(normalized))normalized=normalized.replace(" ","T")+":00";else if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/.test(normalized))normalized=normalized.replace(" ","T");else if(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(normalized))normalized=normalized.replace(" ","T")+"Z";const date=new Date(normalized);return Number.isNaN(date.getTime())?raw:date.toLocaleString("pl-PL",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"});}
|
||||
function render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview";renderMermaid();}else{preview.classList.add("preview--raw");preview.textContent=editor.value;document.querySelector("#preview-label").textContent="Source text";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
|
||||
function render(){if(uiState.mode==="markdown"){preview.classList.remove("preview--raw");preview.innerHTML=renderMarkdown(editor.value);document.querySelector("#preview-label").textContent="Markdown + Mermaid preview · text and headings are editable";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}" contenteditable="true" spellcheck="true">${escapeHtml(line)||"<br>"}</div>`).join("");document.querySelector("#preview-label").textContent="Text preview · editable";}document.querySelector("#characters").textContent=`${editor.value.length} characters`;document.querySelector("#words").textContent=`${editor.value.trim()?editor.value.trim().split(/\s+/).length:0} words`;renderGutter();}
|
||||
function applyUi({write=false,replace=false}={}){editorWorkspace.className=`workspace view-${uiState.view} editor-workspace-font-${fontFamily.value}`;editorWorkspace.style.setProperty("--editor-font-size",`${fontSize.value}px`);document.body.classList.toggle("compact-editor",compactToggle.checked);document.querySelectorAll("[data-view]").forEach(b=>{const a=b.dataset.view===uiState.view;b.classList.toggle("active",a);b.setAttribute("aria-pressed",String(a));});const markdown=uiState.mode==="markdown";modeToggle.classList.toggle("active",markdown);modeToggle.textContent=markdown?"Markdown":"Text";render();if(write)writeEditorState(uiState,{replace});updateAddressLabel();}
|
||||
function applyRemote(content,ownerMap){if(content===editor.value&&ownerMap==null)return;const start=editor.selectionStart,end=editor.selectionEnd;applyingRemote=true;editor.value=content;try{owners=JSON.parse(ownerMap||"[]");}catch{owners=[];}editor.setSelectionRange(Math.min(start,content.length),Math.min(end,content.length));applyingRemote=false;render();}
|
||||
|
||||
@@ -53,11 +54,11 @@ async function loadFiles({open=false}={}){
|
||||
}catch(error){if(open)toast(error.message);}
|
||||
}
|
||||
function connect(){socket?.stop();socket=new PadSocket({slug,password,nickname,onStatus:s=>setStatus(s==="online"?"online":s==="offline"?"offline":null,s==="online"?"Connected":s==="offline"?"Reconnecting…":"Connecting…"),onAuthenticated:m=>{if(passwordDialog.open)passwordDialog.close();applyRemote(m.content,m.owner_map);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"})}`;},onError:m=>{document.querySelector("#password-error").textContent=m;if(info?.protected&&!passwordDialog.open)passwordDialog.showModal();}});socket.connect();}
|
||||
async function initialize(){try{info=await api(`/api/pads/${encodeURIComponent(slug)}`);document.title=`${info.title} · RustPad`;applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
|
||||
async function initialize(){try{info=await api(`/api/pads/${encodeURIComponent(slug)}`);document.title=`${info.title} · RustPad`;publicTaskUpdates.checked=Boolean(info.allow_public_task_updates);applyUi({write:true,replace:true});if(!nickname){identityDialog.showModal();return;}document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}}catch(e){document.body.innerHTML=`<main class="error-page"><div><h1>Note not found</h1><p>${escapeHtml(e.message)}</p></div></main>`;}}
|
||||
document.querySelector("#identity-form").addEventListener("submit",e=>{e.preventDefault();nickname=document.querySelector("#nickname").value.trim();setNickname(nickname);identityDialog.close();document.querySelector("#current-user").textContent=nickname;document.querySelector("#current-user").style.setProperty("--owner",colorFor(nickname));if(info.protected&&!password)passwordDialog.showModal();else{loadFiles();connect();}});
|
||||
document.querySelectorAll("[data-view]").forEach(b=>b.addEventListener("click",()=>{uiState={...uiState,view:b.dataset.view};applyUi({write:true});}));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();});compactToggle.addEventListener("change",()=>{localStorage.setItem("rustpad:compact",compactToggle.checked?"on":"off");applyUi();});fontFamily.addEventListener("change",()=>{localStorage.setItem("rustpad:font-family",fontFamily.value);applyUi();});fontSize.addEventListener("change",()=>{localStorage.setItem("rustpad:font-size",fontSize.value);applyUi();});
|
||||
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)));
|
||||
document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/pads/${encodeURIComponent(slug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null})});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);}});
|
||||
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);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();}});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 prefix=target.dataset.sourcePrefix||"",suffix=target.dataset.sourceSuffix||"";const next=prefix+(target.innerText||"").replace(/\n/g," ")+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});
|
||||
publicTaskUpdates.addEventListener("change",async()=>{publicTaskUpdates.disabled=true;try{await api(`/api/pads/${encodeURIComponent(slug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null,allow_task_updates:publicTaskUpdates.checked})});toast(publicTaskUpdates.checked?"Public task updates enabled":"Public task updates disabled");}catch(error){publicTaskUpdates.checked=!publicTaskUpdates.checked;toast(error.message);}finally{publicTaskUpdates.disabled=false;}});document.querySelector("#publish-page").addEventListener("click",async()=>{try{const result=await api(`/api/pads/${encodeURIComponent(slug)}/publish`,{method:"POST",body:JSON.stringify({password:password||null,allow_task_updates:publicTaskUpdates.checked})});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);}});
|
||||
editor.addEventListener("scroll",()=>{gutter.scrollTop=editor.scrollTop;renderGutter();});editor.addEventListener("input",()=>{const newLines=editor.value.split("\n").length;const cursorLine=editor.value.slice(0,editor.selectionStart).split("\n").length-1;while(owners.length<newLines)owners.push(nickname);owners=owners.slice(0,newLines);owners[cursorLine]=nickname;render();if(applyingRemote)return;clearTimeout(saveTimer);document.querySelector("#save-state").textContent="Saving…";saveTimer=setTimeout(()=>socket?.update(editor.value,JSON.stringify(owners)),250);});
|
||||
document.querySelector("#password-form").addEventListener("submit",e=>{e.preventDefault();password=document.querySelector("#open-password").value;sessionStorage.setItem(`rustpad:pad:${slug}:password`,password);document.querySelector("#password-error").textContent="";connect();});
|
||||
const historyPanel=document.querySelector("#history-panel");document.querySelector("#history-button").addEventListener("click",async()=>{historyPanel.classList.add("open");historyPanel.setAttribute("aria-hidden","false");document.body.classList.add("history-open");const list=document.querySelector("#history-list");list.innerHTML='<p class="empty">Loading…</p>';try{const revisions=await api(`/api/pads/${encodeURIComponent(slug)}/history`,{method:"POST",body:JSON.stringify({password:password||null})});list.innerHTML=revisions.length?revisions.map((r,i)=>{const snippet=escapeHtml(r.content.trim().split("\n").slice(0,3).join(" · ").slice(0,150)||"Empty note");const author=r.author||"Unknown author";return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`;}).join(""):'<p class="empty">No history yet.</p>';for(const r of revisions){list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click",()=>{const el=list.querySelector(`#preview-${r.id}`);el.hidden=!el.hidden;el.textContent=r.content;});list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click",async()=>{await api(`/api/pads/${encodeURIComponent(slug)}/restore`,{method:"POST",body:JSON.stringify({password:password||null,revision_id:r.id})});toast("Version restored");});}}catch(e){list.innerHTML=`<p class="error">${escapeHtml(e.message)}</p>`;}});document.querySelector("#close-history").addEventListener("click",()=>{historyPanel.classList.remove("open");historyPanel.setAttribute("aria-hidden","true");document.body.classList.remove("history-open");});
|
||||
|
||||
+10
-3
@@ -2,10 +2,17 @@ import { api } from "@rustpad/api";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { renderMarkdown } from "@rustpad/markdown";
|
||||
|
||||
const token = location.pathname.split("/").filter(Boolean)[1];
|
||||
const content = document.querySelector("#public-content");
|
||||
const token=location.pathname.split("/").filter(Boolean)[1];
|
||||
const content=document.querySelector("#public-content");
|
||||
function toast(text){const el=document.querySelector("#toast");el.textContent=text;el.classList.add("visible");setTimeout(()=>el.classList.remove("visible"),1800);}
|
||||
async function renderMermaid(){const nodes=content.querySelectorAll(".mermaid");if(!nodes.length)return;try{const {default:mermaid}=await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs");mermaid.initialize({startOnLoad:false,theme:"dark",securityLevel:"strict"});await mermaid.run({nodes:[...nodes]});}catch{nodes.forEach(n=>n.insertAdjacentHTML("beforebegin",'<p class="error">Failed to load Mermaid.</p>'));}}
|
||||
async function initialize(){try{const page=await api(`/api/public/${encodeURIComponent(token)}`);document.querySelector("#public-title").textContent=page.title;document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")}`;document.title=`${page.title} · RustPad`;content.innerHTML=renderMarkdown(page.content);await renderMermaid();}catch(error){content.innerHTML=`<p class="error">${String(error.message)}</p>`;}}
|
||||
async function renderCodeHighlight(){const blocks=content.querySelectorAll('pre code[class^="language-"]');if(!blocks.length)return;try{const hljs=await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm");blocks.forEach(block=>hljs.default.highlightElement(block));}catch{}}
|
||||
function lockPublicContent(allowTaskUpdates){
|
||||
content.querySelectorAll('[contenteditable]').forEach(node=>node.removeAttribute('contenteditable'));
|
||||
content.querySelectorAll('.preview-editable').forEach(node=>node.classList.remove('preview-editable'));
|
||||
content.querySelectorAll('.task-checkbox').forEach(box=>{box.disabled=!allowTaskUpdates;box.title=allowTaskUpdates?'Update this task':'Task updates are disabled by the owner';});
|
||||
}
|
||||
async function initialize(){try{const page=await api(`/api/public/${encodeURIComponent(token)}`);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);lockPublicContent(page.allow_task_updates);await Promise.all([renderMermaid(),renderCodeHighlight()]);}catch(error){content.innerHTML=`<p class="error">${String(error.message)}</p>`;}}
|
||||
content.addEventListener("change",async event=>{const box=event.target.closest(".task-checkbox");if(!box||box.disabled)return;const previous=!box.checked;box.disabled=true;try{const page=await api(`/api/public/${encodeURIComponent(token)}/tasks`,{method:"POST",body:JSON.stringify({source_line:Number(box.dataset.sourceLine),checked:box.checked})});document.querySelector("#public-meta").textContent=`Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`;toast("Task saved");}catch(error){box.checked=previous;toast(error.message);}finally{box.disabled=false;}});
|
||||
document.querySelector("#copy-public-link").addEventListener("click",async()=>{try{await copyText(location.href);toast("Link copied");}catch(error){toast(error.message);}});
|
||||
initialize();
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
<!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><link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__"><script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/note.js?v=__ASSET_VERSION__"></script></head>
|
||||
<body class="pad-page"><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><div class="header-actions"><span id="current-user" class="user-chip"></span><div class="status"><span id="status-dot" class="status__dot"></span><span id="status-text">Connecting…</span></div><button id="copy-link" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><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></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">1. List</button><button data-format="quote">Quote</button><button data-format="link">Link</button></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">14</option><option value="16">16</option><option value="18" selected>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> Lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox"> Compact</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</div><div class="editor-shell"><div id="line-gutter" class="line-gutter" 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><span id="characters">0 characters</span> · <span id="words">0 words</span></div><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="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" class="dialog-panel"><h2>What should we call you?</h2><p class="dialog-copy">Your name will be shown next to changes and remembered on this device.</p><input id="nickname" maxlength="40" autocomplete="nickname" required placeholder="Name or nickname"><button class="primary-button">Open note</button></form></dialog>
|
||||
<body class="pad-page"><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><div class="header-actions"><span id="current-user" class="user-chip"></span><button id="copy-link" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><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><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></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 class="markdown-more"><summary title="Extended Markdown">More</summary><div class="markdown-more-menu"><button type="button" data-format="inline-code">Inline code</button><button type="button" data-format="codeblock">Code block</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> Lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</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</div><div class="editor-shell"><div id="line-gutter" class="line-gutter" 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-status status"><span id="status-dot" class="status__dot"></span><span id="status-text">Connecting…</span></span></div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button" aria-haspopup="dialog">Shortcuts</button> · <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+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" class="dialog-panel"><h2>What should we call you?</h2><p class="dialog-copy">Your name will be shown next to changes and remembered on this device.</p><input id="nickname" maxlength="40" autocomplete="nickname" required placeholder="Name or nickname"><button class="primary-button">Open note</button></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 id="back-workspace" class="dialog-link" href="/">Back</a></form></dialog><div id="toast" class="toast"></div></body></html>
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
<!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>__PAD_TITLE__ · RustPad</title><link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__"><script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/pad.js?v=__ASSET_VERSION__"></script></head>
|
||||
<body class="pad-page"><header class="app-header"><div class="app-header__main"><a class="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></div><div class="header-actions"><span id="current-user" class="user-chip"></span><div class="status"><span id="status-dot" class="status__dot"></span><span id="status-text">Connecting…</span></div><button id="copy-link" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><button id="files-button" class="secondary-button">Files</button><button id="history-button" class="secondary-button">History</button></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">1. List</button><button data-format="quote">Quote</button><button data-format="link">Link</button></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">14</option><option value="16">16</option><option value="18" selected>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> Lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox"> Compact</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</div><div class="editor-shell"><div id="line-gutter" class="line-gutter" 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><span id="characters">0 characters</span> · <span id="words">0 words</span></div><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="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" class="dialog-panel"><h2>What should we call you?</h2><p class="dialog-copy">Your name will be shown next to changes and remembered on this device.</p><input id="nickname" maxlength="40" autocomplete="nickname" required placeholder="Name or nickname"><button class="primary-button">Open note</button></form></dialog>
|
||||
<body class="pad-page"><header class="app-header"><div class="app-header__main"><a class="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></div><div class="header-actions"><span id="current-user" class="user-chip"></span><button id="copy-link" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><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><button id="files-button" class="secondary-button">Files</button><button id="history-button" class="secondary-button">History</button></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 class="markdown-more"><summary title="Extended Markdown">More</summary><div class="markdown-more-menu"><button type="button" data-format="inline-code">Inline code</button><button type="button" data-format="codeblock">Code block</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> Lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</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</div><div class="editor-shell"><div id="line-gutter" class="line-gutter" 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-status status"><span id="status-dot" class="status__dot"></span><span id="status-text">Connecting…</span></span></div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button" aria-haspopup="dialog">Shortcuts</button> · <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+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" class="dialog-panel"><h2>What should we call you?</h2><p class="dialog-copy">Your name will be shown next to changes and remembered on this device.</p><input id="nickname" maxlength="40" autocomplete="nickname" required placeholder="Name or nickname"><button class="primary-button">Open note</button></form></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 placeholder="Password"><p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a class="dialog-link" href="/">Back</a></form></dialog><div id="toast" class="toast"></div></body></html>
|
||||
|
||||
Reference in New Issue
Block a user