| 1 | {{/* editor — GET /d/{sqid}/edit, the editor (editor.go, spec §3.1). |
| 2 | |
| 3 | The three panes, server-rendered: the strip (every slide as a |
| 4 | server-drawn SVG thumbnail, each with its menu), the |
| 5 | stage (the switcher, the current slide's section, then its notes), |
| 6 | and the inspector (three sections of real form controls: the deck, |
| 7 | the slide, the selected block). The bundle (fixtures/editor-entry.js) |
| 8 | binds ProseMirror to the text blocks on the stage, fills the |
| 9 | inspector for the selection and turns every change into one drive |
| 10 | operation, keeps the strip current, and saves — it adds editing to |
| 11 | a page that already reads correctly, and it never renders a slide |
| 12 | of its own. |
| 13 | |
| 14 | Everything the bundle needs is a data attribute on [slides-editor]: |
| 15 | the deck, the log's seq, the current slide, the person's name and |
| 16 | colour, and every string the bundle will ever show. The bundle |
| 17 | carries no copy: a state changes which of these texts is on screen, |
| 18 | and nothing else. Everything the bundle builds more than once (a |
| 19 | strip item) is cloned from a <template> here, so its markup and |
| 20 | its words are this file's too. |
| 21 | |
| 22 | The inspector's controls are hidden and shown by the bundle |
| 23 | (hidden on a field whose block type or slide layout does not have |
| 24 | it); every one of them is a labelled control whether or not it is |
| 25 | on screen, and TestEditorInspectorMarkupIsFormsWithLabels holds it |
| 26 | to that. |
| 27 | |
| 28 | The <noscript> is spec §8's floor said out loud — the editor is |
| 29 | the one surface that needs scripts — and it points at the viewer, |
| 30 | which does not. */}} |
| 31 | {{define "page-head"}}<link rel="stylesheet" href="{{asset "static/slides.css"}}"> |
| 32 | <link rel="stylesheet" href="{{asset "static/editor.css"}}"> |
| 33 | <link rel="stylesheet" href="{{asset "static/estilo/save-status.css"}}"> |
| 34 | <script type="module" src="{{.BundleSrc}}"></script>{{end}} |
| 35 | {{define "content"}} |
| 36 | <div slides-editor |
| 37 | data-deck="{{.DeckSqid}}" |
| 38 | data-seq="{{.Seq}}" |
| 39 | data-slide="{{.SlideID}}" |
| 40 | data-client-name="{{.ClientName}}" |
| 41 | data-client-colour="{{.ClientColour}}" |
| 42 | data-save-status-module="{{asset "static/estilo/save-status.mjs"}}" |
| 43 | data-text-error="{{.L "status.error"}}" |
| 44 | data-text-current="{{.CurrentLabel}}" |
| 45 | data-text-slide="{{.SlideLabel}}" |
| 46 | data-text-phone-override="{{.L "block.phone.override"}}" |
| 47 | data-text-canvas-selected="{{.L "canvas.selected"}}" |
| 48 | data-text-presence-slide="{{.L "presence.slide"}}" |
| 49 | data-text-presence-block="{{.L "presence.block"}}" |
| 50 | data-text-presence-more="{{.L "presence.more"}}" |
| 51 | data-text-presence-joined="{{.L "presence.joined"}}" |
| 52 | data-text-presence-left="{{.L "presence.left"}}" |
| 53 | data-store="{{if .HasStore}}1{{else}}0{{end}}" |
| 54 | data-text-upload-uploading="{{.L "block.image.uploading"}}" |
| 55 | data-text-upload-checking="{{.L "block.image.checking"}}" |
| 56 | data-text-upload-done="{{.L "block.image.done"}}" |
| 57 | data-text-upload-failed="{{.L "block.image.failed"}}" |
| 58 | data-text-upload-toolarge="{{.L "block.image.toolarge"}}" |
| 59 | data-text-upload-type="{{.L "block.image.type"}}" |
| 60 | data-text-upload-altfirst="{{.L "block.image.altfirst"}}" |
| 61 | data-text-asset-remove="{{.L "assets.remove"}}" |
| 62 | {{- range .BlockTypes}} |
| 63 | data-type-{{.}}="{{$.L (printf "type.%s" .)}}" |
| 64 | {{- end}}> |
| 65 | <noscript> |
| 66 | <div rst-callout rst-tone="warning"> |
| 67 | <div rst-callout-body> |
| 68 | <p>{{.NoScriptBody}}</p> |
| 69 | <p><a rst-btn href="{{.BackHref}}">{{.NoScriptLink}}</a></p> |
| 70 | </div> |
| 71 | </div> |
| 72 | </noscript> |
| 73 | |
| 74 | <header slides-editor-bar> |
| 75 | {{template "back-nav" dict "Href" .BackHref "Label" .BackLabel}} |
| 76 | <h1 slides-editor-title>{{.Title}}</h1> |
| 77 | {{/* Every keyboard shortcut has a menu item: Ctrl+Enter, Ctrl+D, |
| 78 | Ctrl+Z and Shift+Ctrl+Z are all here as buttons, so somebody who |
| 79 | does not know the keys — or cannot press them — finds the same |
| 80 | commands by name. Present is a form, as it is on the viewer, |
| 81 | because pressing it writes (presentation.go). */}} |
| 82 | <details rst-dropdown slides-menu name="rst-menus"> |
| 83 | <summary rst-btn>{{.MenuLabel}} <span rst-caret aria-hidden="true">{{icon "chevron-down"}}</span></summary> |
| 84 | <div rst-dropdown-menu> |
| 85 | <button type="button" data-command="new-slide">{{.NewSlideLabel}}</button> |
| 86 | <button type="button" data-command="duplicate-slide"{{if not .HasSlides}} disabled{{end}}>{{.DuplicateLabel}}</button> |
| 87 | <button type="button" data-command="undo">{{.UndoLabel}}</button> |
| 88 | <button type="button" data-command="redo">{{.RedoLabel}}</button> |
| 89 | </div> |
| 90 | </details> |
| 91 | <form method="post" action="{{.PresentAction}}" slides-present-form> |
| 92 | <button rst-btn="primary" type="submit">{{.PresentLabel}}</button> |
| 93 | </form> |
| 94 | {{/* The save line is estilo's savestatus (save_status.go): text, |
| 95 | always, "Saved" at rest, and the bundle moves it through |
| 96 | "Saving…", "Offline, retrying" and the dropped-batch line with |
| 97 | estilo's own module. A slide that would not load is not a save |
| 98 | state, so it is said in a line of its own beside it, from |
| 99 | data-text-error above. Both are role="status": a change is read |
| 100 | out without stealing focus. */}} |
| 101 | {{template "estilo-save-status" .SaveStatus}} |
| 102 | <span slides-load-error role="status" aria-live="polite" aria-atomic="true"></span> |
| 103 | {{/* Presence's one announcement (spec §3.3): somebody arriving in |
| 104 | the deck or leaving it, said once and politely. Where each of |
| 105 | them IS shows on the strip and on the stage and is never spoken |
| 106 | — a live region that followed a colleague's cursor would make |
| 107 | the editor unusable with a screen reader. */}} |
| 108 | <p slides-presence-live class="rst-sr-only" aria-live="polite" aria-atomic="true"></p> |
| 109 | </header> |
| 110 | |
| 111 | {{/* One strip item, the shape the bundle clones for a slide it adds: |
| 112 | the thumbnail (an <img> of the server's SVG — inert, because it is |
| 113 | a picture of the slide and never a second copy of it in the tab |
| 114 | order; the src is left empty here, because strip.mjs fills it once |
| 115 | the slide exists on the server), the link that is the one control |
| 116 | for opening it, and the item's own menu. The menu's five commands are |
| 117 | the Slide section's, so a slide can be reordered, copied or |
| 118 | removed from where it is listed; the drag handle is the item |
| 119 | itself (draggable), with Alt+arrows and the menu as the keyboard's |
| 120 | equivalents. */}} |
| 121 | <template slides-strip-item-template> |
| 122 | <li slides-strip-item draggable="true"> |
| 123 | <div slides-thumb inert><img slides-thumb-img alt="" width="1920" height="1080"></div> |
| 124 | <a slides-strip-link href="#"> |
| 125 | <span slides-strip-label></span> |
| 126 | <span slides-strip-current>{{.CurrentLabel}}</span> |
| 127 | </a> |
| 128 | <details rst-dropdown slides-strip-menu name="rst-menus"> |
| 129 | <summary rst-btn="sm" aria-label="{{.L "strip.actions"}}"><span aria-hidden="true">{{icon "kebab"}}</span></summary> |
| 130 | <div rst-dropdown-menu> |
| 131 | <button type="button" data-slide-command="add-after">{{.L "slide.add"}}</button> |
| 132 | <button type="button" data-slide-command="duplicate">{{.DuplicateLabel}}</button> |
| 133 | <button type="button" data-slide-command="move-up">{{.L "slide.move.up"}}</button> |
| 134 | <button type="button" data-slide-command="move-down">{{.L "slide.move.down"}}</button> |
| 135 | <button type="button" data-slide-command="delete" class="rst-danger">{{.L "slide.delete"}}</button> |
| 136 | </div> |
| 137 | </details> |
| 138 | </li> |
| 139 | </template> |
| 140 | |
| 141 | <nav slides-strip-nav aria-label="{{.StripLabel}}"> |
| 142 | <p id="slides-strip-help" class="rst-sr-only">{{.L "strip.reorder"}}</p> |
| 143 | <ol slides-strip aria-describedby="slides-strip-help"> |
| 144 | {{range .Strip}} |
| 145 | <li slides-strip-item draggable="true" data-slide-id="{{.ID}}"{{if .Current}} data-current="true"{{end}}> |
| 146 | <div slides-thumb inert><img slides-thumb-img src="{{.Thumb}}" alt="{{.Label}}" width="1920" height="1080" loading="lazy"></div> |
| 147 | <a slides-strip-link href="#{{.ID}}"{{if .Current}} aria-current="true"{{end}}> |
| 148 | <span slides-strip-label>{{.Label}}</span> |
| 149 | <span slides-strip-current>{{$.CurrentLabel}}</span> |
| 150 | </a> |
| 151 | <details rst-dropdown slides-strip-menu name="rst-menus"> |
| 152 | <summary rst-btn="sm" aria-label="{{$.L "strip.actions"}}"><span aria-hidden="true">{{icon "kebab"}}</span></summary> |
| 153 | <div rst-dropdown-menu> |
| 154 | <button type="button" data-slide-command="add-after">{{$.L "slide.add"}}</button> |
| 155 | <button type="button" data-slide-command="duplicate">{{$.DuplicateLabel}}</button> |
| 156 | <button type="button" data-slide-command="move-up">{{$.L "slide.move.up"}}</button> |
| 157 | <button type="button" data-slide-command="move-down">{{$.L "slide.move.down"}}</button> |
| 158 | <button type="button" data-slide-command="delete" class="rst-danger">{{$.L "slide.delete"}}</button> |
| 159 | </div> |
| 160 | </details> |
| 161 | </li> |
| 162 | {{end}} |
| 163 | </ol> |
| 164 | </nav> |
| 165 | |
| 166 | <section slides-stage-pane aria-label="{{.StageLabel}}" data-view="desktop"> |
| 167 | {{/* The switcher (spec §3.2): three real radios with visible words. |
| 168 | Which one is on decides where a layout edit lands — the desktop |
| 169 | block, or its phone override map — so it is never an icon. */}} |
| 170 | <div slides-stage-bar> |
| 171 | <fieldset slides-switcher> |
| 172 | <legend class="rst-sr-only">{{.L "view"}}</legend> |
| 173 | <label><input type="radio" name="slides-view" value="desktop" checked> {{.L "view.desktop"}}</label> |
| 174 | <label><input type="radio" name="slides-view" value="phone"> {{.L "view.phone"}}</label> |
| 175 | <label><input type="radio" name="slides-view" value="both"> {{.L "view.both"}}</label> |
| 176 | </fieldset> |
| 177 | <label rst-switch slides-notes-toggle><input type="checkbox" name="slides-notes" checked><span rst-switch-track aria-hidden="true"></span> {{.L "slide.notes.show"}}</label> |
| 178 | </div> |
| 179 | {{/* The stage and the notes are always here, an empty deck's |
| 180 | included: the bundle's New slide loads the first slide INTO |
| 181 | [slides-stage], so the element has to exist before there is a |
| 182 | slide to put in it. The empty state sits inside the stage and is |
| 183 | replaced by the first slide's section. The phone stage beside it |
| 184 | is the "both" view's read-only preview, empty until then. */}} |
| 185 | <div slides-stages> |
| 186 | <div slides-stage data-vp="desktop">{{if .HasSlides}}{{.Stage}}{{else}}<div rst-box slides-stage-empty> |
| 187 | {{template "empty-state" dict "Title" .EmptyTitle "Body" .EmptyBody}} |
| 188 | </div>{{end}}</div> |
| 189 | <div slides-stage-phone role="region" aria-label="{{.L "view.phone.readonly"}}" hidden></div> |
| 190 | </div> |
| 191 | <section slides-notes-pane aria-labelledby="slides-notes-heading"{{if not .HasSlides}} hidden{{end}}> |
| 192 | <h2 id="slides-notes-heading" slides-notes-heading>{{.NotesLabel}}</h2> |
| 193 | <div slides-notes>{{.Notes}}</div> |
| 194 | </section> |
| 195 | </section> |
| 196 | |
| 197 | <details slides-inspector open> |
| 198 | <summary>{{.InspectorLabel}}</summary> |
| 199 | |
| 200 | {{/* Deck: title, theme, accent, fonts — each control one set_meta. */}} |
| 201 | <div rst-box-head><h2>{{.L "inspector.deck"}}</h2></div> |
| 202 | <form rst-form rst-box slides-inspector-form data-section="deck"> |
| 203 | {{template "field-text" dict "Name" "rst-deck-title" "Label" (.L "deck.title") "Value" .Title}} |
| 204 | {{template "field-select" dict "ID" "rst-deck-theme" "Name" "rst-deck-theme" "Label" (.L "deck.theme") "Options" .Themes "Plain" true}} |
| 205 | <div rst-field> |
| 206 | <label rst-field-label for="rst-deck-accent">{{.L "deck.accent"}}</label> |
| 207 | <input rst-input="short" id="rst-deck-accent" name="rst-deck-accent" type="color"> |
| 208 | </div> |
| 209 | <p><button rst-btn="sm" type="button" data-command="accent-reset">{{.L "deck.accent.reset"}}</button></p> |
| 210 | <div rst-field-row> |
| 211 | <div rst-field> |
| 212 | <label rst-field-label for="rst-deck-font-heading">{{.L "deck.font.heading"}}</label> |
| 213 | <select rst-input id="rst-deck-font-heading" name="rst-deck-font-heading"> |
| 214 | <option value="">{{.L "font.theme"}}</option> |
| 215 | <option value="system-ui, sans-serif">{{.L "font.system"}}</option> |
| 216 | <option value="Georgia, serif">{{.L "font.serif"}}</option> |
| 217 | <option value="ui-monospace, monospace">{{.L "font.mono"}}</option> |
| 218 | </select> |
| 219 | </div> |
| 220 | <div rst-field> |
| 221 | <label rst-field-label for="rst-deck-font-body">{{.L "deck.font.body"}}</label> |
| 222 | <select rst-input id="rst-deck-font-body" name="rst-deck-font-body"> |
| 223 | <option value="">{{.L "font.theme"}}</option> |
| 224 | <option value="system-ui, sans-serif">{{.L "font.system"}}</option> |
| 225 | <option value="Georgia, serif">{{.L "font.serif"}}</option> |
| 226 | <option value="ui-monospace, monospace">{{.L "font.mono"}}</option> |
| 227 | </select> |
| 228 | </div> |
| 229 | </div> |
| 230 | {{/* Unused images (spec §2.7): the assets no image block on any |
| 231 | slide refers to any more. Deleting the last block that used one |
| 232 | leaves the registration behind on purpose — an undo has to be |
| 233 | able to put the block back — so this is where a person clears |
| 234 | them out. The bundle fills the list from the document on every |
| 235 | change; each Remove is one remove_asset. */}} |
| 236 | <fieldset slides-unused-assets> |
| 237 | <legend>{{.L "assets.unused"}}</legend> |
| 238 | <p rst-text-muted slides-unused-none>{{.L "assets.unused.none"}}</p> |
| 239 | <ul slides-unused-list></ul> |
| 240 | </fieldset> |
| 241 | </form> |
| 242 | |
| 243 | {{/* Slide: layout, template, background, transition, phone |
| 244 | behaviour, the structure commands, and the Blocks list a |
| 245 | keyboard selects from. Hidden until there is a slide. */}} |
| 246 | <div rst-box-head slides-when-slide{{if not .HasSlides}} hidden{{end}}><h2>{{.L "inspector.slide"}}</h2></div> |
| 247 | <form rst-form rst-box slides-inspector-form data-section="slide" slides-when-slide{{if not .HasSlides}} hidden{{end}}> |
| 248 | {{template "choice-field" dict "Legend" (.L "slide.layout") "Name" "rst-slide-layout" "Options" (list |
| 249 | (dict "Value" "flow" "Title" (.L "layout.flow") "Desc" (.L "layout.flow.desc") "Checked" true) |
| 250 | (dict "Value" "canvas" "Title" (.L "layout.canvas") "Desc" (.L "layout.canvas.desc")))}} |
| 251 | <div rst-field data-layout="flow"> |
| 252 | <label rst-field-label for="rst-slide-template">{{.L "slide.template"}}</label> |
| 253 | <select rst-input id="rst-slide-template" name="rst-slide-template"> |
| 254 | <option value="title">{{.L "template.title"}}</option> |
| 255 | <option value="title-body">{{.L "template.title-body"}}</option> |
| 256 | <option value="two-column">{{.L "template.two-column"}}</option> |
| 257 | <option value="section">{{.L "template.section"}}</option> |
| 258 | <option value="image-full">{{.L "template.image-full"}}</option> |
| 259 | <option value="quote">{{.L "template.quote"}}</option> |
| 260 | <option value="blank">{{.L "template.blank"}}</option> |
| 261 | </select> |
| 262 | </div> |
| 263 | <div rst-field-row> |
| 264 | <div rst-field> |
| 265 | <label rst-field-label for="rst-slide-background">{{.L "slide.background"}}</label> |
| 266 | <select rst-input id="rst-slide-background" name="rst-slide-background"> |
| 267 | <option value="theme">{{.L "background.theme"}}</option> |
| 268 | <option value="color">{{.L "background.color"}}</option> |
| 269 | </select> |
| 270 | </div> |
| 271 | <div rst-field data-background="color" hidden> |
| 272 | <label rst-field-label for="rst-slide-background-color">{{.L "slide.background.color"}}</label> |
| 273 | <input rst-input="short" id="rst-slide-background-color" name="rst-slide-background-color" type="color"> |
| 274 | </div> |
| 275 | </div> |
| 276 | <div rst-field> |
| 277 | <label rst-field-label for="rst-slide-transition">{{.L "slide.transition"}}</label> |
| 278 | <select rst-input id="rst-slide-transition" name="rst-slide-transition"> |
| 279 | <option value="none">{{.L "transition.none"}}</option> |
| 280 | <option value="fade">{{.L "transition.fade"}}</option> |
| 281 | </select> |
| 282 | </div> |
| 283 | <div rst-field data-layout="canvas" hidden> |
| 284 | <label rst-field-label for="rst-slide-phone">{{.L "slide.phone"}}</label> |
| 285 | <select rst-input id="rst-slide-phone" name="rst-slide-phone"> |
| 286 | <option value="inherit">{{.L "slide.phone.inherit"}}</option> |
| 287 | <option value="reflow">{{.L "slide.phone.reflow"}}</option> |
| 288 | </select> |
| 289 | </div> |
| 290 | <div slides-inspector-actions> |
| 291 | <button rst-btn="sm" type="button" data-command="slide-move-up">{{.L "slide.move.up"}}</button> |
| 292 | <button rst-btn="sm" type="button" data-command="slide-move-down">{{.L "slide.move.down"}}</button> |
| 293 | <button rst-btn="sm" type="button" data-command="slide-add-after">{{.L "slide.add"}}</button> |
| 294 | <button rst-btn="sm" type="button" data-command="duplicate-slide">{{.DuplicateLabel}}</button> |
| 295 | <button rst-btn="sm" type="button" data-command="slide-delete-ask">{{.L "slide.delete"}}</button> |
| 296 | </div> |
| 297 | {{/* The delete confirm lives here, in the section, rather than in a |
| 298 | window.confirm the catalog cannot word and a keyboard cannot |
| 299 | always reach. */}} |
| 300 | <div rst-callout rst-tone="warning" slides-confirm hidden> |
| 301 | <div rst-callout-body> |
| 302 | <p>{{.L "slide.delete.confirm"}}</p> |
| 303 | <p slides-inspector-actions> |
| 304 | <button rst-btn="danger sm" type="button" data-command="slide-delete">{{.L "slide.delete"}}</button> |
| 305 | <button rst-btn="sm" type="button" data-command="slide-delete-cancel">{{.L "slide.delete.cancel"}}</button> |
| 306 | </p> |
| 307 | </div> |
| 308 | </div> |
| 309 | <fieldset slides-blocks-list> |
| 310 | <legend>{{.L "inspector.blocks"}}</legend> |
| 311 | <p rst-text-muted slides-blocks-none>{{.L "inspector.blocks.none"}}</p> |
| 312 | <div slides-blocks-buttons role="group"></div> |
| 313 | </fieldset> |
| 314 | <details rst-dropdown slides-add-block name="rst-menus"> |
| 315 | <summary rst-btn="sm">{{.L "block.add"}} <span rst-caret aria-hidden="true">{{icon "chevron-down"}}</span></summary> |
| 316 | <div rst-dropdown-menu> |
| 317 | {{range .BlockTypes}}<button type="button" data-add-block="{{.}}">{{$.L (printf "type.%s" .)}}</button> |
| 318 | {{end}} |
| 319 | </div> |
| 320 | </details> |
| 321 | </form> |
| 322 | |
| 323 | {{/* Block: the selected block's props (each field marked with the |
| 324 | types that have it), its placement (region on a flow slide, a |
| 325 | box and layer on a canvas one), its order, and its phone layer. |
| 326 | Hidden until a block is selected. */}} |
| 327 | {{/* The heading is the slide's, not the selection's: with a slide on |
| 328 | stage and nothing selected there is a Block section saying how to |
| 329 | get one, rather than a section that has silently vanished. The |
| 330 | bundle shows the line when a slide is on and no block is. */}} |
| 331 | <div rst-box-head slides-when-slide{{if not .HasSlides}} hidden{{end}}><h2>{{.L "inspector.block"}} <span slides-block-type></span></h2></div> |
| 332 | <p rst-text-muted slides-inspector-empty{{if .HasSlides}}{{else}} hidden{{end}}>{{.InspectorEmpty}}</p> |
| 333 | <form rst-form rst-box slides-inspector-form data-section="block" slides-when-block hidden> |
| 334 | <p slides-phone-badge hidden><span rst-status rst-tone="warning">{{.L "block.phone.override"}}</span> |
| 335 | <button rst-btn="sm" type="button" data-command="phone-reset">{{.L "block.phone.reset"}}</button></p> |
| 336 | <div rst-field data-types="heading"> |
| 337 | <label rst-field-label for="rst-block-level">{{.L "block.level"}}</label> |
| 338 | <select rst-input="short" id="rst-block-level" name="rst-block-level" data-prop="level" data-kind="int"> |
| 339 | <option value="1">1</option><option value="2">2</option><option value="3">3</option> |
| 340 | </select> |
| 341 | </div> |
| 342 | <div rst-field data-types="text list code"> |
| 343 | <label rst-field-label for="rst-block-size">{{.L "block.size"}}</label> |
| 344 | <select rst-input="short" id="rst-block-size" name="rst-block-size" data-prop="size"> |
| 345 | <option value="s">{{.L "size.s"}}</option><option value="m">{{.L "size.m"}}</option><option value="l">{{.L "size.l"}}</option> |
| 346 | </select> |
| 347 | </div> |
| 348 | <div rst-field data-types="text"> |
| 349 | <label rst-field-label for="rst-block-align">{{.L "block.align"}}</label> |
| 350 | <select rst-input="short" id="rst-block-align" name="rst-block-align" data-prop="align"> |
| 351 | <option value="start">{{.L "align.start"}}</option><option value="center">{{.L "align.center"}}</option><option value="end">{{.L "align.end"}}</option> |
| 352 | </select> |
| 353 | </div> |
| 354 | <div rst-field data-types="list"> |
| 355 | <label rst-field-label for="rst-block-style">{{.L "block.style"}}</label> |
| 356 | <select rst-input="short" id="rst-block-style" name="rst-block-style" data-prop="style"> |
| 357 | <option value="bullet">{{.L "style.bullet"}}</option><option value="number">{{.L "style.number"}}</option> |
| 358 | </select> |
| 359 | </div> |
| 360 | <div rst-field data-types="quote"> |
| 361 | <label rst-field-label for="rst-block-cite">{{.L "block.cite"}}</label> |
| 362 | <input rst-input id="rst-block-cite" name="rst-block-cite" type="text" data-prop="cite"> |
| 363 | </div> |
| 364 | {{/* The image's own upload (assets.go, spec §2.7). A real labelled |
| 365 | file input, with the accept list the endpoint validates |
| 366 | against; the hint is its description, so a screen reader hears |
| 367 | the limits before the picker opens. The status line is text |
| 368 | with role="status" — progress and every failure are read out |
| 369 | and are never colour alone. |
| 370 | |
| 371 | With no object store bound the input is disabled and the |
| 372 | no-store line replaces the hint. The page says so, rather than |
| 373 | the bundle finding out from a 503 after somebody has chosen a |
| 374 | file. */}} |
| 375 | <div rst-field data-types="image"> |
| 376 | <label rst-field-label for="rst-block-image">{{.L "block.image.file"}}</label> |
| 377 | <input rst-input id="rst-block-image" name="rst-block-image" type="file" slides-asset-input |
| 378 | accept="{{.AcceptTypes}}" aria-describedby="slides-asset-hint"{{if not .HasStore}} disabled{{end}}> |
| 379 | <p rst-field-hint id="slides-asset-hint">{{if .HasStore}}{{.L "block.image.hint"}}{{else}}{{.L "block.image.nostore"}}{{end}}</p> |
| 380 | <p rst-text-muted slides-asset-status role="status" aria-live="polite"></p> |
| 381 | </div> |
| 382 | <div rst-field data-types="image"> |
| 383 | <label rst-field-label for="rst-block-fit">{{.L "block.fit"}}</label> |
| 384 | <select rst-input="short" id="rst-block-fit" name="rst-block-fit" data-prop="fit"> |
| 385 | <option value="cover">{{.L "fit.cover"}}</option><option value="contain">{{.L "fit.contain"}}</option> |
| 386 | </select> |
| 387 | </div> |
| 388 | <div rst-field data-types="image"> |
| 389 | <label rst-field-label for="rst-block-alt">{{.L "block.alt"}}</label> |
| 390 | <input rst-input id="rst-block-alt" name="rst-block-alt" type="text" data-prop="alt"> |
| 391 | </div> |
| 392 | <div rst-field data-types="image"> |
| 393 | <label rst-switch><input type="checkbox" name="rst-block-decorative" data-prop="decorative" data-kind="bool"><span rst-switch-track aria-hidden="true"></span> {{.L "block.decorative"}}</label> |
| 394 | </div> |
| 395 | <div rst-field data-types="image shape"> |
| 396 | <label rst-field-label for="rst-block-radius">{{.L "block.radius"}}</label> |
| 397 | <input rst-input="short" id="rst-block-radius" name="rst-block-radius" type="number" min="0" max="50" step="1" data-prop="radius" data-kind="int"> |
| 398 | </div> |
| 399 | <div rst-field data-types="columns"> |
| 400 | <label rst-field-label for="rst-block-count">{{.L "block.count"}}</label> |
| 401 | <select rst-input="short" id="rst-block-count" name="rst-block-count" data-prop="count" data-kind="int"> |
| 402 | <option value="2">2</option><option value="3">3</option> |
| 403 | </select> |
| 404 | </div> |
| 405 | <div rst-field data-types="code"> |
| 406 | <label rst-field-label for="rst-block-language">{{.L "block.language"}}</label> |
| 407 | <input rst-input="short" id="rst-block-language" name="rst-block-language" type="text" data-prop="language"> |
| 408 | </div> |
| 409 | <div rst-field data-types="shape"> |
| 410 | <label rst-field-label for="rst-block-kind">{{.L "block.kind"}}</label> |
| 411 | <select rst-input="short" id="rst-block-kind" name="rst-block-kind" data-prop="kind"> |
| 412 | <option value="rect">{{.L "kind.rect"}}</option><option value="ellipse">{{.L "kind.ellipse"}}</option> |
| 413 | <option value="line">{{.L "kind.line"}}</option><option value="arrow">{{.L "kind.arrow"}}</option> |
| 414 | </select> |
| 415 | </div> |
| 416 | <div rst-field-row data-types="shape"> |
| 417 | <div rst-field> |
| 418 | <label rst-field-label for="rst-block-fill">{{.L "block.fill"}}</label> |
| 419 | <input rst-input="short" id="rst-block-fill" name="rst-block-fill" type="color" data-prop="fill"> |
| 420 | </div> |
| 421 | <div rst-field> |
| 422 | <label rst-field-label for="rst-block-stroke">{{.L "block.stroke"}}</label> |
| 423 | <input rst-input="short" id="rst-block-stroke" name="rst-block-stroke" type="color" data-prop="stroke"> |
| 424 | </div> |
| 425 | <div rst-field> |
| 426 | <label rst-field-label for="rst-block-strokeWidth">{{.L "block.strokeWidth"}}</label> |
| 427 | <input rst-input="short" id="rst-block-strokeWidth" name="rst-block-strokeWidth" type="number" min="0" max="50" step="1" data-prop="strokeWidth" data-kind="int"> |
| 428 | </div> |
| 429 | </div> |
| 430 | <div rst-field data-types="shape"> |
| 431 | <label rst-field-label for="rst-block-label">{{.L "block.label"}}</label> |
| 432 | <input rst-input id="rst-block-label" name="rst-block-label" type="text" data-text="label"> |
| 433 | </div> |
| 434 | <div rst-field-row data-types="table"> |
| 435 | <div rst-field> |
| 436 | <label rst-field-label for="rst-block-rows">{{.L "block.rows"}}</label> |
| 437 | <input rst-input="short" id="rst-block-rows" name="rst-block-rows" type="number" min="1" max="100" step="1" data-prop="rows" data-kind="int"> |
| 438 | </div> |
| 439 | <div rst-field> |
| 440 | <label rst-field-label for="rst-block-cols">{{.L "block.cols"}}</label> |
| 441 | <input rst-input="short" id="rst-block-cols" name="rst-block-cols" type="number" min="1" max="100" step="1" data-prop="cols" data-kind="int"> |
| 442 | </div> |
| 443 | </div> |
| 444 | <div rst-field data-types="table"> |
| 445 | <label rst-switch><input type="checkbox" name="rst-block-header" data-prop="header" data-kind="bool"><span rst-switch-track aria-hidden="true"></span> {{.L "block.header"}}</label> |
| 446 | </div> |
| 447 | <div rst-field data-types="spacer"> |
| 448 | <label rst-field-label for="rst-block-spacer-size">{{.L "block.size"}}</label> |
| 449 | <select rst-input="short" id="rst-block-spacer-size" name="rst-block-spacer-size" data-prop="size"> |
| 450 | <option value="s">{{.L "size.s"}}</option><option value="m">{{.L "size.m"}}</option><option value="l">{{.L "size.l"}}</option> |
| 451 | </select> |
| 452 | </div> |
| 453 | <div rst-field data-layout="flow"> |
| 454 | <label rst-field-label for="rst-block-region">{{.L "block.region"}}</label> |
| 455 | <select rst-input="short" id="rst-block-region" name="rst-block-region" data-place="region"> |
| 456 | <option value="heading">{{.L "region.heading"}}</option><option value="body">{{.L "region.body"}}</option> |
| 457 | <option value="aside">{{.L "region.aside"}}</option><option value="media">{{.L "region.media"}}</option> |
| 458 | </select> |
| 459 | </div> |
| 460 | <fieldset rst-field-range data-layout="canvas"> |
| 461 | <legend>{{.L "block.box"}}</legend> |
| 462 | <div rst-field-row> |
| 463 | <div rst-field> |
| 464 | <label rst-field-label for="rst-block-x">{{.L "box.x"}}</label> |
| 465 | <input rst-input="short" id="rst-block-x" name="rst-block-x" type="number" min="0" max="100" step="0.1" data-box="x"> |
| 466 | </div> |
| 467 | <div rst-field> |
| 468 | <label rst-field-label for="rst-block-y">{{.L "box.y"}}</label> |
| 469 | <input rst-input="short" id="rst-block-y" name="rst-block-y" type="number" min="0" max="100" step="0.1" data-box="y"> |
| 470 | </div> |
| 471 | <div rst-field> |
| 472 | <label rst-field-label for="rst-block-w">{{.L "box.w"}}</label> |
| 473 | <input rst-input="short" id="rst-block-w" name="rst-block-w" type="number" min="1" max="100" step="0.1" data-box="w"> |
| 474 | </div> |
| 475 | <div rst-field> |
| 476 | <label rst-field-label for="rst-block-h">{{.L "box.h"}}</label> |
| 477 | <input rst-input="short" id="rst-block-h" name="rst-block-h" type="number" min="1" max="100" step="0.1" data-box="h"> |
| 478 | </div> |
| 479 | </div> |
| 480 | </fieldset> |
| 481 | <div slides-inspector-actions data-layout="canvas"> |
| 482 | <button rst-btn="sm" type="button" data-command="block-forward">{{.L "block.forward"}}</button> |
| 483 | <button rst-btn="sm" type="button" data-command="block-back">{{.L "block.back"}}</button> |
| 484 | </div> |
| 485 | <div slides-inspector-actions> |
| 486 | <button rst-btn="sm" type="button" data-command="block-move-up">{{.L "block.move.up"}}</button> |
| 487 | <button rst-btn="sm" type="button" data-command="block-move-down">{{.L "block.move.down"}}</button> |
| 488 | <button rst-btn="sm" type="button" data-command="block-delete">{{.L "block.delete"}}</button> |
| 489 | </div> |
| 490 | <fieldset rst-field-range slides-phone-fields> |
| 491 | <legend>{{.L "block.phone"}}</legend> |
| 492 | <div rst-field> |
| 493 | <label rst-switch><input type="checkbox" name="rst-block-phone-hidden" data-phone="hidden"><span rst-switch-track aria-hidden="true"></span> {{.L "block.phone.hidden"}}</label> |
| 494 | </div> |
| 495 | <div rst-field-row> |
| 496 | <div rst-field> |
| 497 | <label rst-field-label for="rst-block-phone-order">{{.L "block.phone.order"}}</label> |
| 498 | <input rst-input="short" id="rst-block-phone-order" name="rst-block-phone-order" type="number" step="1" data-phone="order"> |
| 499 | </div> |
| 500 | <div rst-field> |
| 501 | <label rst-field-label for="rst-block-phone-size">{{.L "block.phone.size"}}</label> |
| 502 | <select rst-input="short" id="rst-block-phone-size" name="rst-block-phone-size" data-phone="size"> |
| 503 | <option value="">{{.L "block.phone.same"}}</option> |
| 504 | <option value="s">{{.L "size.s"}}</option><option value="m">{{.L "size.m"}}</option><option value="l">{{.L "size.l"}}</option> |
| 505 | </select> |
| 506 | </div> |
| 507 | </div> |
| 508 | </fieldset> |
| 509 | </form> |
| 510 | </details> |
| 511 | |
| 512 | {{/* The link dialog (Ctrl+K): a labelled field, three named buttons. |
| 513 | A <dialog> rather than window.prompt because a prompt has no label |
| 514 | the catalog controls and no way to remove a link. Closed until the |
| 515 | bundle opens it. */}} |
| 516 | <dialog slides-link-dialog aria-labelledby="slides-link-title"> |
| 517 | <form method="dialog" rst-form> |
| 518 | <h2 id="slides-link-title">{{.LinkTitle}}</h2> |
| 519 | <div rst-field> |
| 520 | <label rst-field-label for="slides-link-url">{{.LinkURLLabel}}</label> |
| 521 | <input rst-input id="slides-link-url" name="href" type="url" inputmode="url" autocomplete="off" spellcheck="false"> |
| 522 | </div> |
| 523 | <div rst-form-actions> |
| 524 | <button rst-btn="primary" type="submit" value="insert">{{.LinkInsert}}</button> |
| 525 | <button rst-btn type="submit" value="remove" formnovalidate>{{.LinkRemove}}</button> |
| 526 | <button rst-btn type="submit" value="cancel" formnovalidate>{{.LinkCancel}}</button> |
| 527 | </div> |
| 528 | </form> |
| 529 | </dialog> |
| 530 | </div> |
| 531 | {{end}} |
| 532 | |