oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1/* Cell formatting: attributes on, colours in, nothing else.
2 *
3 * Every rule that paints a formatted cell lives in sheets.css, where
4 * three guard tests read it. This module only sets the attributes those
5 * rules select on, and two pairs of custom properties they read. It
6 * deliberately does not build a stylesheet: a stylesheet JavaScript
7 * generates is invisible to the colour-literal guard, the token guard
8 * and the vendored-paint restatement guard alike.
9 *
10 * A style is CLEARED by painting undefined. The server sends a cell's
11 * format key only when it has one, so a cell that just lost its
12 * formatting arrives with no key at all — and if clearing were the
13 * caller's job to remember, it would be forgotten exactly once and the
14 * fill would stay on screen until a reload.
15 */
16(function () {
17 'use strict';
18
19 /* Every attribute this module owns. Listed once, used by both the
20 * clear path and the sweep grid.js does before a full snapshot, so
21 * the two cannot disagree about what "painted" means. */
22 var ATTRS = ['data-b', 'data-i', 'data-a', 'data-n', 'data-fill',
23 'data-ink', 'data-fx', 'data-k', 'data-contrast'];
24
25 var PROPS = ['--sheet-h', '--sheet-c', '--sheet-kh', '--sheet-kc',
26 '--sheet-fx', '--sheet-k'];
27
28 /* PAINTED is the selector grid.js sweeps with. Built from ATTRS so
29 * adding an attribute above cannot leave a cell painted after a
30 * snapshot that no longer mentions it. */
31 var PAINTED = ATTRS.map(function (a) { return 'td[' + a + ']'; }).join(',');
32
33 /* hex accepts only what a document could legitimately have put in a
34 * fill: six or eight hex digits, the eight-digit form being XLSX's
35 * ARGB with the alpha dropped. Anything else returns '' and the cell
36 * is painted unstyled.
37 *
38 * This is a real check and not a formality. The value arrives from a
39 * spreadsheet somebody uploaded and is about to be written into a CSS
40 * custom property; without the pattern, a crafted "fill" could carry
41 * arbitrary CSS into the page's style attribute. A rejected colour
42 * costs one unstyled cell. */
43 function hex(v) {
44 if (typeof v !== 'string') return '';
45 if (!/^[0-9A-Fa-f]{6}$|^[0-9A-Fa-f]{8}$/.test(v)) return '';
46 return '#' + (v.length === 8 ? v.slice(2) : v);
47 }
48
49 /* num accepts a finite number only. Hue and chroma go into oklch()
50 * the same way, and for the same reason. */
51 function num(v) {
52 return (typeof v === 'number' && isFinite(v)) ? String(v) : '';
53 }
54
55 function clear(td) {
56 var i;
57 for (i = 0; i < ATTRS.length; i++) td.removeAttribute(ATTRS[i]);
58 for (i = 0; i < PROPS.length; i++) td.style.removeProperty(PROPS[i]);
59 }
60
61 /* number marks a cell whose computed value the SERVER says is a
62 * number, so the stylesheet can right-align it. Separate from cell()
63 * because it is a property of the value and not of the format, and
64 * the two arrive on different fields and change independently. */
65 function number(td, isNum) {
66 if (!td) return;
67 if (isNum) td.setAttribute('data-n', '');
68 else td.removeAttribute('data-n');
69 }
70
71 /* cell paints one td. Clearing first means a format change never has
72 * to be a diff: whatever the cell looked like, it now looks like s. */
73 function cell(td, s) {
74 if (!td) return;
75 var wasNum = td.hasAttribute('data-n');
76 clear(td);
77 /* clear() drops every attribute this module owns, including the
78 * numeric mark, which is not part of the style being applied — so
79 * it is put back. Losing it here would right-align a number until
80 * you formatted it and then never again. */
81 if (wasNum) td.setAttribute('data-n', '');
82 if (!s) return;
83
84 if (s.b) td.setAttribute('data-b', '');
85 if (s.i) td.setAttribute('data-i', '');
86 /* centerContinuous is Excel's "centre across the selection", which
87 * this app does not implement; centring the cell itself is the
88 * closest honest rendering. fill, justify and distributed are
89 * dropped rather than approximated. */
90 var a = s.a === 'centerContinuous' ? 'center' : s.a;
91 if (a === 'left' || a === 'center' || a === 'right') {
92 td.setAttribute('data-a', a);
93 }
94
95 /* The author's literal wins over our intent when both are present.
96 * They should never be — writing a fill here clears the hex — but
97 * a row written by an older build could carry both, and rendering
98 * the document as authored is the rule that settles the tie. */
99 var fx = hex(s.fx);
100 if (fx) {
101 td.style.setProperty('--sheet-fx', fx);
102 td.setAttribute('data-fx', '');
103 } else {
104 var h = num(s.fh), c = num(s.fc);
105 if (h && c) {
106 td.style.setProperty('--sheet-h', h);
107 td.style.setProperty('--sheet-c', c);
108 td.setAttribute('data-fill', '');
109 }
110 }
111
112 var k = hex(s.k);
113 if (k) {
114 td.style.setProperty('--sheet-k', k);
115 td.setAttribute('data-k', '');
116 } else {
117 var kh = num(s.kh), kc = num(s.kc);
118 if (kh && kc) {
119 td.style.setProperty('--sheet-kh', kh);
120 td.style.setProperty('--sheet-kc', kc);
121 td.setAttribute('data-ink', '');
122 }
123 }
124
125 /* The corner flag for an imported pair below WCAG AA. The rule for
126 * it has been in sheets.css since import landed and nothing set the
127 * attribute, so the page warned in prose about cells it did not
128 * mark. The server decides — contrast.go owns the arithmetic. */
129 if (s.lc) td.setAttribute('data-contrast', 'low');
130 }
131
132 /* bind ties this module to a live worksheet and to a way of looking a
133 * cell's paint up, so nothing outside it has to reach into the
134 * library's records array. Painting lives here; grid.js says WHAT a
135 * cell is, not how to draw it.
136 *
137 * Verified against the vendored build: setValueFromCoords rewrites a
138 * td's innerHTML and leaves the element, its attributes and its style
139 * alone, so a paint applied here survives every later value update.
140 * The only code that resets a className is the delete-column path,
141 * which the grid disables. */
142 var sheet = null;
143 var lookup = null;
144
145 function bind(ws, fn) { sheet = ws; lookup = fn; }
146
147 function tdAt(r, c) {
148 var rec = sheet && sheet.records && sheet.records[r];
149 return (rec && rec[c] && rec[c].element) || null;
150 }
151
152 function repaint(r, c) {
153 if (!lookup) return;
154 var td = tdAt(r, c), m = lookup(r, c);
155 cell(td, m.style);
156 number(td, !!m.n);
157 }
158
159 /* sweep unpaints everything, for the one case a per-cell repaint
160 * cannot cover: a full snapshot supersedes the whole window, and a
161 * cell whose format was removed while this tab was disconnected is
162 * simply absent from it. */
163 function sweep(root) {
164 if (!root) return;
165 var painted = root.querySelectorAll(PAINTED);
166 for (var i = 0; i < painted.length; i++) clear(painted[i]);
167 }
168
169 window.SheetsPaint = { bind: bind, repaint: repaint, sweep: sweep };
170}());
171