| 1 | //! The in-browser file editor. |
| 2 | //! |
| 3 | //! A form with a `<textarea>` in it. That is the whole page, and it works with |
| 4 | //! scripting disabled — CodeMirror replaces the textarea when `/assets/editor.js` |
| 5 | //! loads and copies its content back on submit (spec §7: "Progressive |
| 6 | //! enhancement is a hard requirement"). |
| 7 | //! |
| 8 | //! Nothing here is conditional on JavaScript being available. There is no |
| 9 | //! "loading" state and no hidden fallback: the fallback *is* the page. |
| 10 | |
| 11 | use maud::{html, Markup}; |
| 12 | |
| 13 | use crate::repo_ctx::RepoContext; |
| 14 | use crate::views::repo::breadcrumbs; |
| 15 | |
| 16 | pub struct Editor<'a> { |
| 17 | pub bookmark: &'a str, |
| 18 | /// The revision the edit is composed against, submitted back so a |
| 19 | /// concurrent edit is detected rather than overwritten. |
| 20 | pub tip: &'a str, |
| 21 | pub path: &'a str, |
| 22 | pub content: &'a str, |
| 23 | /// The grammar name CodeMirror should use, when we have one. |
| 24 | pub language: Option<&'a str>, |
| 25 | pub author: &'a str, |
| 26 | pub csrf: &'a str, |
| 27 | pub error: Option<&'a str>, |
| 28 | } |
| 29 | |
| 30 | pub fn editor(ctx: &RepoContext, e: Editor<'_>) -> Markup { |
| 31 | let base = ctx.base(); |
| 32 | let action = format!("{base}/edit/{}/{}", e.bookmark, e.path); |
| 33 | let cancel = format!("{base}/blob/{}/{}", e.bookmark, e.path); |
| 34 | |
| 35 | html! { |
| 36 | div .tree-crumbs { |
| 37 | span .btn.btn-mono.is-static { (e.bookmark) } |
| 38 | (breadcrumbs(&base, e.bookmark, e.path)) |
| 39 | span .spacer {} |
| 40 | a .btn.btn-mono href=(cancel) { "cancel" } |
| 41 | } |
| 42 | |
| 43 | @if let Some(msg) = e.error { |
| 44 | div .banner.banner-error role="alert" { (msg) } |
| 45 | } |
| 46 | |
| 47 | form method="post" action=(action) data-editor-form="1" { |
| 48 | input type="hidden" name="_csrf" value=(e.csrf); |
| 49 | // What the edit was composed against. The server refuses the save |
| 50 | // if the bookmark has moved since. |
| 51 | input type="hidden" name="tip" value=(e.tip); |
| 52 | |
| 53 | div .editor-shell { |
| 54 | div .editor-bar { |
| 55 | span .mono { (e.path) } |
| 56 | @if let Some(l) = e.language { |
| 57 | span .label-condensed { (l) } |
| 58 | } |
| 59 | span .spacer {} |
| 60 | span .editor-status.mono { "editing on " (e.bookmark) } |
| 61 | } |
| 62 | |
| 63 | label .sr-only for="content" { "Contents of " (e.path) } |
| 64 | div .editor-host { |
| 65 | textarea id="content" name="content" rows="24" |
| 66 | data-editor="1" |
| 67 | data-language=[e.language] |
| 68 | spellcheck="false" |
| 69 | autocapitalize="off" autocomplete="off" |
| 70 | wrap="off" { (e.content) } |
| 71 | } |
| 72 | |
| 73 | div .editor-foot { |
| 74 | label .sr-only for="message" { "Commit message" } |
| 75 | input type="text" id="message" name="message" maxlength="500" |
| 76 | placeholder=(format!("Update {}", e.path)); |
| 77 | button .btn.btn-primary type="submit" { "Commit changes" } |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | p .hint.measure { |
| 83 | "Committed to " code { (e.bookmark) } " as " (e.author) |
| 84 | ". Editing here writes a commit on top of the bookmark — it never rewrites \ |
| 85 | history, so a protected bookmark accepts it, and it is indexed like any push." |
| 86 | } |
| 87 | } |
| 88 | } |
88 lines · Rust