| 1 | {{define "content"}} |
| 2 | <header rst-page-header> |
| 3 | <div rst-page-header-titles> |
| 4 | {{/* The page's name, as in its <title> ("Table view · {sheet} · |
| 5 | Sheets"); the sheet's own title is in the bar beside the brand. */}} |
| 6 | <h1>Table view</h1> |
| 7 | {{/* Was "rows {{.Data.Page}} of {{.Data.Pages}} pages", which spliced a |
| 8 | row count and a page count into one phrase that counted neither. |
| 9 | The trailing "with no JavaScript at all" went with it: this view is |
| 10 | the product rather than the fallback, and a subtitle that leads with |
| 11 | what the page lacks argues the opposite. */}} |
| 12 | <p rst-page-header-sub>Page {{.Data.Page}} of {{.Data.Pages}}.{{if .Data.MayWrite}} |
| 13 | Every cell here is editable.{{end}}</p> |
| 14 | </div> |
| 15 | {{/* One name per destination: "Version history" as in the File menu, |
| 16 | "Back to sheet" for the grid everywhere, "All sheets" for the list. */}} |
| 17 | <a rst-btn="ghost" href="/sheets/{{.Data.Sheet.ID}}/history">Version history</a> |
| 18 | <a rst-btn="ghost" href="/sheets/{{.Data.Sheet.ID}}/comments">Comments</a> |
| 19 | <a rst-btn="primary" href="/sheets/{{.Data.Sheet.ID}}">Back to sheet</a> |
| 20 | </header> |
| 21 | |
| 22 | {{/* Sort and filter reorder what is SHOWN and never what is stored: |
| 23 | calc's dependency graph is keyed on position, so physically moving |
| 24 | rows would change what every formula points at. See ViewSpec. */}} |
| 25 | <form rst-form method="post" action="/sheets/{{.Data.Sheet.ID}}/view"> |
| 26 | <div rst-field-row> |
| 27 | <div rst-field> |
| 28 | <label rst-field-label for="sortCol">Sort by</label> |
| 29 | <select rst-input id="sortCol" name="sortCol"> |
| 30 | <option value="-1">nothing</option> |
| 31 | {{$view := .Data.View}} |
| 32 | {{range $i, $name := .Data.Cols}}<option value="{{$i}}"{{if eq $i $view.SortCol}} selected{{end}}>{{$name}}</option>{{end}} |
| 33 | </select> |
| 34 | </div> |
| 35 | <div rst-field> |
| 36 | <label rst-field-label for="sortDesc"> |
| 37 | <input rst-check id="sortDesc" name="sortDesc" type="checkbox" value="1"{{if .Data.View.SortDsc}} checked{{end}}> |
| 38 | Descending</label> |
| 39 | </div> |
| 40 | <div rst-field> |
| 41 | <label rst-field-label for="filterCol">Show rows where</label> |
| 42 | <select rst-input id="filterCol" name="filterCol"> |
| 43 | <option value="-1">everything</option> |
| 44 | {{range $i, $name := .Data.Cols}}<option value="{{$i}}"{{if eq $i $view.FilterCol}} selected{{end}}>{{$name}}</option>{{end}} |
| 45 | </select> |
| 46 | </div> |
| 47 | <div rst-field> |
| 48 | <label rst-field-label for="filterOp">is</label> |
| 49 | <select rst-input id="filterOp" name="filterOp"> |
| 50 | {{range .Data.FilterOps}}<option value="{{.}}"{{if eq . $view.FilterOp}} selected{{end}}>{{.}}</option>{{end}} |
| 51 | </select> |
| 52 | </div> |
| 53 | <div rst-field> |
| 54 | <label rst-field-label for="filterVal">value</label> |
| 55 | <input rst-input="short" id="filterVal" name="filterVal" type="text" value="{{.Data.View.FilterVal}}"> |
| 56 | </div> |
| 57 | <button rst-btn="primary" type="submit">Apply</button> |
| 58 | </div> |
| 59 | </form> |
| 60 | |
| 61 | {{if .Data.MayWrite}} |
| 62 | {{/* Column width, scriptless. PRODUCT.md's second principle: an |
| 63 | affordance in the grid and not here means this view is unfinished, |
| 64 | and width was the last of the seven that had no form behind it — |
| 65 | bold, italic, alignment, number format, fill and text colour all |
| 66 | landed with the cells they format. |
| 67 | |
| 68 | One form with a column picker rather than a control in each of the |
| 69 | 26 headers: it matches the sort and filter control directly above |
| 70 | it, and a header carrying a number input in every column is a |
| 71 | header nobody can read. |
| 72 | |
| 73 | The size route takes this form and the grid's JSON on one path — |
| 74 | see sizeFromForm — so a width set here and a width dragged there |
| 75 | are the same write, and both fan out to every open tab. */}} |
| 76 | <form rst-form method="post" action="/api/sheets/{{.Data.Sheet.ID}}/tabs/{{.Data.Sheet.TabID}}/size"> |
| 77 | <input type="hidden" name="page" value="{{.Data.Page}}"> |
| 78 | <div rst-field-row> |
| 79 | <div rst-field> |
| 80 | <label rst-field-label for="widthCol">Column width</label> |
| 81 | <select rst-input id="widthCol" name="col"> |
| 82 | {{range $i, $name := .Data.Cols}}<option value="{{$i}}">{{$name}}</option>{{end}} |
| 83 | </select> |
| 84 | </div> |
| 85 | <div rst-field> |
| 86 | <label rst-field-label for="widthPx">pixels</label> |
| 87 | <input rst-input="short" id="widthPx" name="size" type="number" min="20" max="600" value="120"> |
| 88 | </div> |
| 89 | <button rst-btn="primary" type="submit">Set width</button> |
| 90 | </div> |
| 91 | </form> |
| 92 | {{end}} |
| 93 | |
| 94 | {{if .Data.View.Active}} |
| 95 | <p rst-field-help>This view is sorted or filtered. It changes what everybody |
| 96 | sees on this tab, and it changes nothing about the sheet itself — the |
| 97 | rows are where they always were.</p> |
| 98 | {{end}} |
| 99 | |
| 100 | {{template "contrast-notice" .Data.Warned}} |
| 101 | <div style="overflow-x:auto"> |
| 102 | <table data-cell-table> |
| 103 | <caption class="rst-sr-only">{{.Data.Sheet.Title}}, page {{.Data.Page}} of {{.Data.Pages}}</caption> |
| 104 | <thead> |
| 105 | <tr> |
| 106 | <th scope="col"><span class="rst-sr-only">Row</span></th> |
| 107 | {{range .Data.Cols}}<th scope="col">{{.}}</th>{{end}} |
| 108 | </tr> |
| 109 | </thead> |
| 110 | <tbody> |
| 111 | {{$sheet := .Data.Sheet}} |
| 112 | {{$page := .Data.Page}} |
| 113 | {{$mayWrite := .Data.MayWrite}} |
| 114 | {{$mayFormat := .Data.MayWrite}} |
| 115 | {{range .Data.Rows}} |
| 116 | <tr> |
| 117 | <th scope="row">{{.Number}}</th> |
| 118 | {{range .Cells}} |
| 119 | {{/* The same attribute vocabulary paint.js writes on the grid's td, |
| 120 | written here by the server because this view has no script. Same |
| 121 | names, same custom properties, one set of rules in sheets.css — |
| 122 | a second vocabulary would be a second answer to "what does bold |
| 123 | look like". */}} |
| 124 | <td{{if .Number}} data-kind="number"{{end}}{{with .Paint}}{{if .Bold}} data-b{{end}}{{if .Italic}} data-i{{end}}{{if .Align}} data-a="{{.Align}}"{{end}}{{if .Fill}} data-fill{{end}}{{if .Ink}} data-ink{{end}}{{if .FillHex}} data-fx{{end}}{{if .InkHex}} data-k{{end}}{{if .LowContrast}} data-contrast="low"{{end}}{{if .Any}} style="{{.Vars}}"{{end}}{{end}}> |
| 125 | {{if .Editing}} |
| 126 | {{/* One cell at a time carries a form. Rendering an input in all 650 |
| 127 | cells of a page would make the scriptless view the heaviest page |
| 128 | in the app, and most of those inputs would never be touched. */}} |
| 129 | <form method="post" action="/sheets/{{$sheet.ID}}/table"> |
| 130 | <input type="hidden" name="r" value="{{.R}}"> |
| 131 | <input type="hidden" name="c" value="{{.C}}"> |
| 132 | <input type="hidden" name="page" value="{{$page}}"> |
| 133 | <label class="rst-sr-only" for="cell-{{.Ref}}">{{.Ref}}</label> |
| 134 | <input rst-input id="cell-{{.Ref}}" name="raw" type="text" value="{{.Raw}}" autofocus> |
| 135 | <button rst-btn="primary compact" type="submit">Save</button> |
| 136 | </form> |
| 137 | {{if $mayFormat}} |
| 138 | {{/* Formatting, for the cell already being edited. |
| 139 | |
| 140 | PRODUCT.md's second principle: the scriptless path is the |
| 141 | product, not the fallback, and the grid has had these controls |
| 142 | since 2026-09-02 while this view had none. Spec §14 recorded it |
| 143 | as debt with the trigger "the next slice that touches the table |
| 144 | view". |
| 145 | |
| 146 | It posts to the SAME route the grid's toolbar posts to — one |
| 147 | allowlist, one bounds check, one cell cap. Only the decoder |
| 148 | differs (handlers_format.go's isFormPost). |
| 149 | |
| 150 | Every toggle is a SELECT and not a checkbox, and that is |
| 151 | load-bearing rather than fussy. formatPatch's fields are |
| 152 | pointers so "leave bold alone" and "turn bold off" are |
| 153 | different requests, and an unchecked checkbox is absent from a |
| 154 | submission — indistinguishable from a field that was never |
| 155 | there. A checkbox could make a cell bold and could never make |
| 156 | it not bold. */}} |
| 157 | <form method="post" action="/api/sheets/{{$sheet.ID}}/tabs/{{$sheet.TabID}}/format" data-cell-format> |
| 158 | <input type="hidden" name="r1" value="{{.R}}"> |
| 159 | <input type="hidden" name="c1" value="{{.C}}"> |
| 160 | <input type="hidden" name="r2" value="{{.R}}"> |
| 161 | <input type="hidden" name="c2" value="{{.C}}"> |
| 162 | <input type="hidden" name="page" value="{{$page}}"> |
| 163 | <label class="rst-sr-only" for="b-{{.Ref}}">Bold</label> |
| 164 | <select rst-input id="b-{{.Ref}}" name="bold"> |
| 165 | <option value="keep">Bold…</option> |
| 166 | <option value="set">Bold</option> |
| 167 | <option value="clear">Not bold</option> |
| 168 | </select> |
| 169 | <label class="rst-sr-only" for="i-{{.Ref}}">Italic</label> |
| 170 | <select rst-input id="i-{{.Ref}}" name="italic"> |
| 171 | <option value="keep">Italic…</option> |
| 172 | <option value="set">Italic</option> |
| 173 | <option value="clear">Not italic</option> |
| 174 | </select> |
| 175 | <label class="rst-sr-only" for="a-{{.Ref}}">Alignment</label> |
| 176 | <select rst-input id="a-{{.Ref}}" name="align"> |
| 177 | <option value="keep">Align…</option> |
| 178 | <option value="">Automatic</option> |
| 179 | <option value="left">Left</option> |
| 180 | <option value="center">Centre</option> |
| 181 | <option value="right">Right</option> |
| 182 | </select> |
| 183 | <label class="rst-sr-only" for="n-{{.Ref}}">Number format</label> |
| 184 | <select rst-input id="n-{{.Ref}}" name="numfmt"> |
| 185 | <option value="keep">Number…</option> |
| 186 | {{range $.Data.NumberFormats}}<option value="{{.Code}}">{{.Label}}</option>{{end}} |
| 187 | </select> |
| 188 | <label class="rst-sr-only" for="f-{{.Ref}}">Fill colour</label> |
| 189 | <select rst-input id="f-{{.Ref}}" name="fill"> |
| 190 | <option value="keep">Fill…</option> |
| 191 | <option value="none">No fill</option> |
| 192 | {{range $.Data.FillSwatches}}<option value="{{.Name}}">{{.Label}}</option>{{end}} |
| 193 | </select> |
| 194 | <label class="rst-sr-only" for="k-{{.Ref}}">Text colour</label> |
| 195 | <select rst-input id="k-{{.Ref}}" name="ink"> |
| 196 | <option value="keep">Text…</option> |
| 197 | <option value="none">Default</option> |
| 198 | {{range $.Data.InkSwatches}}<option value="{{.Name}}">{{.Label}}</option>{{end}} |
| 199 | </select> |
| 200 | <button rst-btn="compact" type="submit">Apply</button> |
| 201 | </form> |
| 202 | {{end}} |
| 203 | {{else if $mayWrite}} |
| 204 | {{/* The link is the affordance: it turns this cell into the form |
| 205 | above. The cell shows its computed value, and the title carries |
| 206 | the input, so a formula is readable without opening it. */}} |
| 207 | <a href="/sheets/{{$sheet.ID}}/table?page={{$page}}&edit={{.R}},{{.C}}" |
| 208 | aria-label="Edit {{.Ref}}"{{if .Raw}} title="{{.Raw}}"{{end}}>{{.Display}}</a> |
| 209 | {{else}} |
| 210 | {{/* A viewer or a commenter. No anchor, because the destination would |
| 211 | answer their submit with a 404 — offering the affordance and then |
| 212 | refusing it is worse than not offering it. The comes back |
| 213 | here for the same reason it stays on the link view: with no anchor |
| 214 | there is nothing holding an empty cell open. */}} |
| 215 | {{if .Display}}{{.Display}}{{else}} {{end}} |
| 216 | {{end}} |
| 217 | </td> |
| 218 | {{end}} |
| 219 | </tr> |
| 220 | {{end}} |
| 221 | </tbody> |
| 222 | </table> |
| 223 | </div> |
| 224 | |
| 225 | <nav rst-pagination aria-label="Rows"> |
| 226 | {{if .Data.HasPrev}} |
| 227 | <a href="/sheets/{{.Data.Sheet.ID}}/table?page={{.Data.PrevPage}}">Previous rows</a> |
| 228 | {{else}}<span rst-pagination-disabled>Previous rows</span>{{end}} |
| 229 | <span>Page {{.Data.Page}} of {{.Data.Pages}}</span> |
| 230 | {{if .Data.HasNext}} |
| 231 | <a href="/sheets/{{.Data.Sheet.ID}}/table?page={{.Data.NextPage}}">Next rows</a> |
| 232 | {{else}}<span rst-pagination-disabled>Next rows</span>{{end}} |
| 233 | </nav> |
| 234 | {{end}} |
| 235 | |