| 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