oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

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}}
13Every 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}}>
38Descending</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
96sees on this tab, and it changes nothing about the sheet itself — the
97rows 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}}&amp;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 &nbsp; 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}}&nbsp;{{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