| 1 | {{define "content"}} |
| 2 | {{/* Adding content, on its own page (2026-09-29, Paul: "remove the Add |
| 3 | forms from the index … that should be a separate Add content form"). |
| 4 | The form, the drop zone, the upload tray and the outcome message moved |
| 5 | here unchanged; POST /documents still receives them, and now redirects |
| 6 | back HERE with its outcome rather than to the index. The index keeps |
| 7 | search and the document list, and links here. */}} |
| 8 | <nav rst-breadcrumb><a href="/">← Back to search</a></nav> |
| 9 | <section> |
| 10 | <h1>Add content</h1> |
| 11 | <form method="post" action="/documents" enctype="multipart/form-data" rst-stack> |
| 12 | <label for="title">Title <span rst-muted rst-hint>{{.Drop.TitleHint}}</span></label> |
| 13 | <input type="text" id="title" name="title"> |
| 14 | <label for="body">Paste text</label> |
| 15 | <textarea id="body" name="body" rows="8" placeholder="Paste a document, notes, minutes…"></textarea> |
| 16 | <label for="file">{{.Drop.FileLabel}}</label> |
| 17 | {{/* The drop zone WRAPS the file input, it does not replace it. With |
| 18 | no script the div is an unstyled box, the hint and the note stay |
| 19 | hidden, and this is the same form it was: choose files, press |
| 20 | Store it. static/drop.js adds rst-dropzone="ready", which is what |
| 21 | the styling keys off — so a page whose script did not load never |
| 22 | draws a drop target that cannot take a drop. |
| 23 | |
| 24 | What a DROP does has changed: it no longer fills this input and |
| 25 | wait for Store it. Each dropped file is posted on its own, in |
| 26 | the background, to this form's own action, and the tray below |
| 27 | the form says how each one went. Choosing files here and |
| 28 | pressing Store it is untouched and still posts the whole form. |
| 29 | |
| 30 | The data attributes are the handler's own limits and the |
| 31 | handler's own words (see dropZone in handlers.go). The browser |
| 32 | reads them from here rather than carrying a second copy, so it |
| 33 | cannot come to refuse what the server would accept. |
| 34 | |
| 35 | The input is `multiple`, and that is a plain HTML affordance |
| 36 | with no script behind it: createDocument iterates every part |
| 37 | named "file" and stores one document each, so a scriptless |
| 38 | chooser that hands it five files gets five documents. It was |
| 39 | deliberately single-file while the handler read only the first |
| 40 | part — four of five would have vanished without a word. */}} |
| 41 | {{with .Drop}} |
| 42 | <div rst-dropzone |
| 43 | data-max-bytes="{{.MaxBytes}}" |
| 44 | data-max-total-bytes="{{.MaxTotalBytes}}" |
| 45 | data-max-files="{{.MaxFiles}}" |
| 46 | data-sniff-bytes="{{.SniffBytes}}" |
| 47 | data-max-in-flight="{{.MaxInFlight}}" |
| 48 | data-refresh-header="{{.RefreshHeader}}" |
| 49 | data-msg-toomany="{{.TooManyFiles}}" |
| 50 | data-msg-toototal="{{.TooMuchAtOnce}}" |
| 51 | data-msg-folder="{{.Folder}}" |
| 52 | data-msg-blocked="{{.Blocked}}" |
| 53 | data-msg-waiting="{{.Waiting}}" |
| 54 | data-msg-uploading="{{.Uploading}}" |
| 55 | data-msg-storing="{{.Storing}}" |
| 56 | data-msg-row-stored="{{.RowStored}}" |
| 57 | data-msg-stored-one="{{.StoredOne}}" |
| 58 | data-msg-stored-many="{{.StoredMany}}" |
| 59 | data-msg-none-stored="{{.NoneStored}}" |
| 60 | data-msg-fresh="{{.Fresh}}" |
| 61 | data-msg-refused-one="{{.RefusedOne}}" |
| 62 | data-msg-refused-many="{{.RefusedMany}}" |
| 63 | data-msg-unsure-one="{{.UnsureOne}}" |
| 64 | data-msg-unsure-many="{{.UnsureMany}}" |
| 65 | data-reason-toobig="{{.ReasonTooBig}}" |
| 66 | data-reason-binary="{{.ReasonBinary}}" |
| 67 | data-reason-empty="{{.ReasonEmpty}}" |
| 68 | data-reason-unreadable="{{.ReasonUnreadable}}" |
| 69 | data-reason-noanswer="{{.ReasonNoAnswer}}"> |
| 70 | <input type="file" id="file" name="file" multiple accept=".txt,.md,.markdown,.csv,text/plain,text/markdown"> |
| 71 | {{/* Muted by memoria.css on [rst-dropzone-hint]. (Not the design |
| 72 | system's muted-cell utility: that is a class, and spelled as an |
| 73 | attribute it styled nothing at all — nineteen places looked |
| 74 | muted in the markup and read as body text on the screen. They |
| 75 | are rst-muted now, which memoria.css styles.) |
| 76 | |
| 77 | Both paragraphs are `hidden` so a scriptless page promises no |
| 78 | drop target. drop.js unhides the note while it is still EMPTY, |
| 79 | before it ever writes into it: a live region that enters the |
| 80 | accessibility tree in the same tick as its first message is |
| 81 | commonly not announced at all, and a refusal nobody hears is |
| 82 | the one outcome this feature must not have. */}} |
| 83 | <p rst-dropzone-hint hidden>{{.Hint}}</p> |
| 84 | <p rst-dropzone-note role="status" aria-live="polite" hidden></p> |
| 85 | </div> |
| 86 | {{end}} |
| 87 | <div rst-form-bar><button rst-btn="primary">Store it</button></div> |
| 88 | </form> |
| 89 | {{if .Flash}}<div rst-callout rst-tone="{{.FlashKind | tone}}"{{if eq .FlashKind "danger"}} role="alert"{{end}}><div rst-callout-body><p>{{.Flash}}</p></div></div>{{end}} |
| 90 | |
| 91 | {{/* The upload tray. It lives OUTSIDE the form on purpose: it is not |
| 92 | something the form submits, and a scriptless browser must find the |
| 93 | form exactly as it was. Scriptless it is `hidden` and empty, so it |
| 94 | is not on the page at all. |
| 95 | |
| 96 | drop.js unhides it — still empty — at its ready step, for the same |
| 97 | reason it unhides the note there: [rst-tray-status] is the live |
| 98 | region that announces what a drop did, and a live region that |
| 99 | enters the accessibility tree in the same tick as its first |
| 100 | message is commonly not announced at all. Only [rst-tray-status] |
| 101 | is live. The rows are not, and must not be: five files uploading |
| 102 | is five rows changing several times a second, and a live region |
| 103 | over that is a screen reader nobody can use. |
| 104 | |
| 105 | Every word here comes from dropZone in handlers.go, like the data |
| 106 | attributes above — the tray and the flash sentence a scriptless |
| 107 | browser sees are the same strings, so they cannot drift. |
| 108 | |
| 109 | The document list is on the index, not here, so once anything is |
| 110 | stored the tray points there with a real link. Nothing follows it |
| 111 | on its own: someone mid-drop does not want the ground moving under |
| 112 | them. When a drop settles drop.js re-fetches this page and swaps |
| 113 | its one rst-refresh region, the passage count in the footer. */}} |
| 114 | {{with .Drop}} |
| 115 | <div rst-tray hidden role="region" aria-labelledby="tray-title"> |
| 116 | <div rst-tray-head> |
| 117 | <span rst-tray-title id="tray-title">{{.TrayTitle}}</span> |
| 118 | <button type="button" rst-btn="quiet compact" rst-tray-dismiss>{{.TrayDismiss}}</button> |
| 119 | </div> |
| 120 | <ul rst-tray-list></ul> |
| 121 | <p rst-tray-status role="status" aria-live="polite"></p> |
| 122 | <p rst-tray-listed hidden>{{.Listed}} <a href="/">{{.ViewDocs}}</a></p> |
| 123 | </div> |
| 124 | {{end}} |
| 125 | </section> |
| 126 | {{end}} |
| 127 | |