{{define "content"}}
{{/* The page's name, as in its ("Table view · {sheet} · Sheets"); the sheet's own title is in the bar beside the brand. */}} <h1>Table view</h1> {{/* Was "rows {{.Data.Page}} of {{.Data.Pages}} pages", which spliced a row count and a page count into one phrase that counted neither. The trailing "with no JavaScript at all" went with it: this view is the product rather than the fallback, and a subtitle that leads with what the page lacks argues the opposite. */}} <p rst-page-header-sub>Page {{.Data.Page}} of {{.Data.Pages}}.{{if .Data.MayWrite}} Every cell here is editable.{{end}}</p> </div> {{/* One name per destination: "Version history" as in the File menu, "Back to sheet" for the grid everywhere, "All sheets" for the list. */}} <a rst-btn="ghost" href="/sheets/{{.Data.Sheet.ID}}/history">Version history</a> <a rst-btn="ghost" href="/sheets/{{.Data.Sheet.ID}}/comments">Comments</a> <a rst-btn="primary" href="/sheets/{{.Data.Sheet.ID}}">Back to sheet</a> </header> {{/* Sort and filter reorder what is SHOWN and never what is stored: calc's dependency graph is keyed on position, so physically moving rows would change what every formula points at. See ViewSpec. */}} <form rst-form method="post" action="/sheets/{{.Data.Sheet.ID}}/view"> <div rst-field-row> <div rst-field> <label rst-field-label for="sortCol">Sort by</label> <select rst-input id="sortCol" name="sortCol"> <option value="-1">nothing</option> {{$view := .Data.View}} {{range $i, $name := .Data.Cols}}<option value="{{$i}}"{{if eq $i $view.SortCol}} selected{{end}}>{{$name}}</option>{{end}} </select> </div> <div rst-field> <label rst-field-label for="sortDesc"> <input rst-check id="sortDesc" name="sortDesc" type="checkbox" value="1"{{if .Data.View.SortDsc}} checked{{end}}> Descending</label> </div> <div rst-field> <label rst-field-label for="filterCol">Show rows where</label> <select rst-input id="filterCol" name="filterCol"> <option value="-1">everything</option> {{range $i, $name := .Data.Cols}}<option value="{{$i}}"{{if eq $i $view.FilterCol}} selected{{end}}>{{$name}}</option>{{end}} </select> </div> <div rst-field> <label rst-field-label for="filterOp">is</label> <select rst-input id="filterOp" name="filterOp"> {{range .Data.FilterOps}}<option value="{{.}}"{{if eq . $view.FilterOp}} selected{{end}}>{{.}}</option>{{end}} </select> </div> <div rst-field> <label rst-field-label for="filterVal">value</label> <input rst-input="short" id="filterVal" name="filterVal" type="text" value="{{.Data.View.FilterVal}}"> </div> <button rst-btn="primary" type="submit">Apply</button> </div> </form> {{if .Data.MayWrite}} {{/* Column width, scriptless. PRODUCT.md's second principle: an affordance in the grid and not here means this view is unfinished, and width was the last of the seven that had no form behind it — bold, italic, alignment, number format, fill and text colour all landed with the cells they format. One form with a column picker rather than a control in each of the 26 headers: it matches the sort and filter control directly above it, and a header carrying a number input in every column is a header nobody can read. The size route takes this form and the grid's JSON on one path — see sizeFromForm — so a width set here and a width dragged there are the same write, and both fan out to every open tab. */}} <form rst-form method="post" action="/api/sheets/{{.Data.Sheet.ID}}/tabs/{{.Data.Sheet.TabID}}/size"> <input type="hidden" name="page" value="{{.Data.Page}}"> <div rst-field-row> <div rst-field> <label rst-field-label for="widthCol">Column width</label> <select rst-input id="widthCol" name="col"> {{range $i, $name := .Data.Cols}}<option value="{{$i}}">{{$name}}</option>{{end}} </select> </div> <div rst-field> <label rst-field-label for="widthPx">pixels</label> <input rst-input="short" id="widthPx" name="size" type="number" min="20" max="600" value="120"> </div> <button rst-btn="primary" type="submit">Set width</button> </div> </form> {{end}} {{if .Data.View.Active}} <p rst-field-help>This view is sorted or filtered. It changes what everybody sees on this tab, and it changes nothing about the sheet itself — the rows are where they always were.</p> {{end}} {{template "contrast-notice" .Data.Warned}} <div style="overflow-x:auto"> <table data-cell-table> <caption class="rst-sr-only">{{.Data.Sheet.Title}}, page {{.Data.Page}} of {{.Data.Pages}}</caption> <thead> <tr> <th scope="col"><span class="rst-sr-only">Row</span></th> {{range .Data.Cols}}<th scope="col">{{.}}</th>{{end}} </tr> </thead> <tbody> {{$sheet := .Data.Sheet}} {{$page := .Data.Page}} {{$mayWrite := .Data.MayWrite}} {{$mayFormat := .Data.MayWrite}} {{range .Data.Rows}} <tr> <th scope="row">{{.Number}}</th> {{range .Cells}} {{/* The same attribute vocabulary paint.js writes on the grid's td, written here by the server because this view has no script. Same names, same custom properties, one set of rules in sheets.css — a second vocabulary would be a second answer to "what does bold look like". */}} <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}}> {{if .Editing}} {{/* One cell at a time carries a form. Rendering an input in all 650 cells of a page would make the scriptless view the heaviest page in the app, and most of those inputs would never be touched. */}} <form method="post" action="/sheets/{{$sheet.ID}}/table"> <input type="hidden" name="r" value="{{.R}}"> <input type="hidden" name="c" value="{{.C}}"> <input type="hidden" name="page" value="{{$page}}"> <label class="rst-sr-only" for="cell-{{.Ref}}">{{.Ref}}</label> <input rst-input id="cell-{{.Ref}}" name="raw" type="text" value="{{.Raw}}" autofocus> <button rst-btn="primary compact" type="submit">Save</button> </form> {{if $mayFormat}} {{/* Formatting, for the cell already being edited. PRODUCT.md's second principle: the scriptless path is the product, not the fallback, and the grid has had these controls since 2026-09-02 while this view had none. Spec §14 recorded it as debt with the trigger "the next slice that touches the table view". It posts to the SAME route the grid's toolbar posts to — one allowlist, one bounds check, one cell cap. Only the decoder differs (handlers_format.go's isFormPost). Every toggle is a SELECT and not a checkbox, and that is load-bearing rather than fussy. formatPatch's fields are pointers so "leave bold alone" and "turn bold off" are different requests, and an unchecked checkbox is absent from a submission — indistinguishable from a field that was never there. A checkbox could make a cell bold and could never make it not bold. */}} <form method="post" action="/api/sheets/{{$sheet.ID}}/tabs/{{$sheet.TabID}}/format" data-cell-format> <input type="hidden" name="r1" value="{{.R}}"> <input type="hidden" name="c1" value="{{.C}}"> <input type="hidden" name="r2" value="{{.R}}"> <input type="hidden" name="c2" value="{{.C}}"> <input type="hidden" name="page" value="{{$page}}"> <label class="rst-sr-only" for="b-{{.Ref}}">Bold</label> <select rst-input id="b-{{.Ref}}" name="bold"> <option value="keep">Bold…</option> <option value="set">Bold</option> <option value="clear">Not bold</option> </select> <label class="rst-sr-only" for="i-{{.Ref}}">Italic</label> <select rst-input id="i-{{.Ref}}" name="italic"> <option value="keep">Italic…</option> <option value="set">Italic</option> <option value="clear">Not italic</option> </select> <label class="rst-sr-only" for="a-{{.Ref}}">Alignment</label> <select rst-input id="a-{{.Ref}}" name="align"> <option value="keep">Align…</option> <option value="">Automatic</option> <option value="left">Left</option> <option value="center">Centre</option> <option value="right">Right</option> </select> <label class="rst-sr-only" for="n-{{.Ref}}">Number format</label> <select rst-input id="n-{{.Ref}}" name="numfmt"> <option value="keep">Number…</option> {{range $.Data.NumberFormats}}<option value="{{.Code}}">{{.Label}}</option>{{end}} </select> <label class="rst-sr-only" for="f-{{.Ref}}">Fill colour</label> <select rst-input id="f-{{.Ref}}" name="fill"> <option value="keep">Fill…</option> <option value="none">No fill</option> {{range $.Data.FillSwatches}}<option value="{{.Name}}">{{.Label}}</option>{{end}} </select> <label class="rst-sr-only" for="k-{{.Ref}}">Text colour</label> <select rst-input id="k-{{.Ref}}" name="ink"> <option value="keep">Text…</option> <option value="none">Default</option> {{range $.Data.InkSwatches}}<option value="{{.Name}}">{{.Label}}</option>{{end}} </select> <button rst-btn="compact" type="submit">Apply</button> </form> {{end}} {{else if $mayWrite}} {{/* The link is the affordance: it turns this cell into the form above. The cell shows its computed value, and the title carries the input, so a formula is readable without opening it. */}} <a href="/sheets/{{$sheet.ID}}/table?page={{$page}}&edit={{.R}},{{.C}}" aria-label="Edit {{.Ref}}"{{if .Raw}} title="{{.Raw}}"{{end}}>{{.Display}}</a> {{else}} {{/* A viewer or a commenter. No anchor, because the destination would answer their submit with a 404 — offering the affordance and then refusing it is worse than not offering it. The   comes back here for the same reason it stays on the link view: with no anchor there is nothing holding an empty cell open. */}} {{if .Display}}{{.Display}}{{else}} {{end}} {{end}} </td> {{end}} </tr> {{end}} </tbody> </table> </div> <nav rst-pagination aria-label="Rows"> {{if .Data.HasPrev}} <a href="/sheets/{{.Data.Sheet.ID}}/table?page={{.Data.PrevPage}}">Previous rows</a> {{else}}<span rst-pagination-disabled>Previous rows</span>{{end}} <span>Page {{.Data.Page}} of {{.Data.Pages}}</span> {{if .Data.HasNext}} <a href="/sheets/{{.Data.Sheet.ID}}/table?page={{.Data.NextPage}}">Next rows</a> {{else}}<span rst-pagination-disabled>Next rows</span>{{end}} </nav> {{end}}