@toclocoinc/lattice-grid 1.4.3 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -6
- package/README.md +38 -6
- package/docs/AI-SKILL.md +471 -0
- package/docs/API.html +2168 -0
- package/docs/api-detail.html +3886 -0
- package/lattice-grid.d.ts +9 -1
- package/lattice-grid.esm.min.js +519 -22
- package/lattice-grid.min.cjs +47322 -0
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +518 -22
- package/modules/devtools.esm.min.js +66 -7
- package/modules/react.esm.min.js +2 -2
- package/modules/svelte.esm.min.js +2 -2
- package/modules/vue.esm.min.js +2 -2
- package/modules/webcomponent.esm.min.js +518 -22
- package/package.json +7 -9
package/docs/API.html
ADDED
|
@@ -0,0 +1,2168 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en-GB">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<!--
|
|
7
|
+
Lattice Grid — API reference.
|
|
8
|
+
Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
9
|
+
|
|
10
|
+
Self-contained: no stylesheet, script or font is fetched, so this opens from
|
|
11
|
+
disk, from a file share or from behind a firewall with nothing else present.
|
|
12
|
+
-->
|
|
13
|
+
<title>Lattice Grid — API Reference</title>
|
|
14
|
+
<style>
|
|
15
|
+
/* ---------------------------------------------------------------
|
|
16
|
+
Palette lifted from the grid's own theme, so the reference reads as an
|
|
17
|
+
extension of the product it documents rather than a generic doc template.
|
|
18
|
+
--------------------------------------------------------------- */
|
|
19
|
+
:root {
|
|
20
|
+
--ground: #ffffff;
|
|
21
|
+
--surface: #f7f8f9;
|
|
22
|
+
--surface-2: #eef1f3;
|
|
23
|
+
--ink: #1c2126;
|
|
24
|
+
--muted: #5b6670;
|
|
25
|
+
--rule: #dfe3e6;
|
|
26
|
+
--rule-strong: #c4ccd2;
|
|
27
|
+
--accent: #1a6bc7;
|
|
28
|
+
--accent-soft: #e8f0fb;
|
|
29
|
+
--danger: #c22b2b;
|
|
30
|
+
--danger-soft: #fbeaea;
|
|
31
|
+
--warning: #a96a0b;
|
|
32
|
+
--success: #1b7f3b;
|
|
33
|
+
--code-bg: #f2f4f6;
|
|
34
|
+
|
|
35
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
36
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
37
|
+
|
|
38
|
+
--measure: 68ch;
|
|
39
|
+
--rail: 250px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (prefers-color-scheme: dark) {
|
|
43
|
+
:root:not([data-theme="light"]) {
|
|
44
|
+
--ground: #14181c;
|
|
45
|
+
--surface: #1b2026;
|
|
46
|
+
--surface-2: #212831;
|
|
47
|
+
--ink: #e4e9ee;
|
|
48
|
+
--muted: #99a4ae;
|
|
49
|
+
--rule: #2a3138;
|
|
50
|
+
--rule-strong: #3a434c;
|
|
51
|
+
--accent: #4e9bea;
|
|
52
|
+
--accent-soft: #17293c;
|
|
53
|
+
--danger: #f08a8a;
|
|
54
|
+
--danger-soft: #33201f;
|
|
55
|
+
--warning: #e0a64a;
|
|
56
|
+
--success: #5fc27e;
|
|
57
|
+
--code-bg: #1e242b;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:root[data-theme="dark"] {
|
|
62
|
+
--ground: #14181c;
|
|
63
|
+
--surface: #1b2026;
|
|
64
|
+
--surface-2: #212831;
|
|
65
|
+
--ink: #e4e9ee;
|
|
66
|
+
--muted: #99a4ae;
|
|
67
|
+
--rule: #2a3138;
|
|
68
|
+
--rule-strong: #3a434c;
|
|
69
|
+
--accent: #4e9bea;
|
|
70
|
+
--accent-soft: #17293c;
|
|
71
|
+
--danger: #f08a8a;
|
|
72
|
+
--danger-soft: #33201f;
|
|
73
|
+
--warning: #e0a64a;
|
|
74
|
+
--success: #5fc27e;
|
|
75
|
+
--code-bg: #1e242b;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
* { box-sizing: border-box; }
|
|
79
|
+
|
|
80
|
+
body {
|
|
81
|
+
margin: 0;
|
|
82
|
+
background: var(--ground);
|
|
83
|
+
color: var(--ink);
|
|
84
|
+
font-family: var(--sans);
|
|
85
|
+
font-size: 15px;
|
|
86
|
+
line-height: 1.6;
|
|
87
|
+
-webkit-font-smoothing: antialiased;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ---------------------------------------------------------------
|
|
91
|
+
Shell: an index rail that stays put, mirroring the grid's own
|
|
92
|
+
tool-panel dock, and a reading column beside it.
|
|
93
|
+
--------------------------------------------------------------- */
|
|
94
|
+
.shell {
|
|
95
|
+
display: grid;
|
|
96
|
+
grid-template-columns: var(--rail) minmax(0, 1fr);
|
|
97
|
+
gap: 0;
|
|
98
|
+
max-width: 1360px;
|
|
99
|
+
margin: 0 auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.rail {
|
|
103
|
+
position: sticky;
|
|
104
|
+
top: 0;
|
|
105
|
+
align-self: start;
|
|
106
|
+
max-height: 100vh;
|
|
107
|
+
overflow-y: auto;
|
|
108
|
+
padding: 40px 24px 40px 32px;
|
|
109
|
+
border-right: 1px solid var(--rule);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.rail__brand {
|
|
113
|
+
font-family: var(--mono);
|
|
114
|
+
font-size: 15px;
|
|
115
|
+
font-weight: 700;
|
|
116
|
+
letter-spacing: -0.02em;
|
|
117
|
+
margin: 0 0 2px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.rail__sub {
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
color: var(--muted);
|
|
123
|
+
margin: 0 0 28px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.rail nav { display: flex; flex-direction: column; gap: 26px; }
|
|
127
|
+
.rail__group { display: flex; flex-direction: column; gap: 5px; }
|
|
128
|
+
|
|
129
|
+
.rail__label {
|
|
130
|
+
font-size: 10.5px;
|
|
131
|
+
font-weight: 700;
|
|
132
|
+
letter-spacing: 0.1em;
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
color: var(--muted);
|
|
135
|
+
margin-bottom: 3px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.rail a {
|
|
139
|
+
font-family: var(--mono);
|
|
140
|
+
font-size: 12.5px;
|
|
141
|
+
color: var(--ink);
|
|
142
|
+
text-decoration: none;
|
|
143
|
+
padding: 2px 0;
|
|
144
|
+
border-left: 2px solid transparent;
|
|
145
|
+
padding-left: 10px;
|
|
146
|
+
margin-left: -12px;
|
|
147
|
+
transition: color 120ms, border-color 120ms;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.rail a:hover, .rail a:focus-visible {
|
|
151
|
+
color: var(--accent);
|
|
152
|
+
border-left-color: var(--accent);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.main { padding: 40px 40px 120px; min-width: 0; }
|
|
156
|
+
|
|
157
|
+
/* --------------------------------------------------------------- */
|
|
158
|
+
|
|
159
|
+
header.masthead {
|
|
160
|
+
padding-bottom: 28px;
|
|
161
|
+
margin-bottom: 40px;
|
|
162
|
+
border-bottom: 2px solid var(--ink);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.eyebrow {
|
|
166
|
+
font-size: 10.5px;
|
|
167
|
+
font-weight: 700;
|
|
168
|
+
letter-spacing: 0.14em;
|
|
169
|
+
text-transform: uppercase;
|
|
170
|
+
color: var(--accent);
|
|
171
|
+
margin: 0 0 12px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
h1 {
|
|
175
|
+
font-family: var(--mono);
|
|
176
|
+
font-size: clamp(28px, 4vw, 40px);
|
|
177
|
+
font-weight: 700;
|
|
178
|
+
letter-spacing: -0.03em;
|
|
179
|
+
line-height: 1.1;
|
|
180
|
+
margin: 0 0 14px;
|
|
181
|
+
text-wrap: balance;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.lede { max-width: var(--measure); color: var(--muted); margin: 0; font-size: 16px; }
|
|
185
|
+
|
|
186
|
+
h2 {
|
|
187
|
+
font-family: var(--mono);
|
|
188
|
+
font-size: 21px;
|
|
189
|
+
font-weight: 700;
|
|
190
|
+
letter-spacing: -0.02em;
|
|
191
|
+
margin: 56px 0 6px;
|
|
192
|
+
padding-left: 14px;
|
|
193
|
+
border-left: 3px solid var(--accent);
|
|
194
|
+
scroll-margin-top: 24px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
h3 {
|
|
198
|
+
font-family: var(--mono);
|
|
199
|
+
font-size: 15.5px;
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
margin: 34px 0 10px;
|
|
202
|
+
scroll-margin-top: 24px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
h2 + .section-note { margin: 0 0 22px 17px; max-width: var(--measure); color: var(--muted); font-size: 14px; }
|
|
206
|
+
|
|
207
|
+
p { max-width: var(--measure); }
|
|
208
|
+
|
|
209
|
+
a { color: var(--accent); }
|
|
210
|
+
|
|
211
|
+
/* Inline identifiers — the primary content of the whole document. */
|
|
212
|
+
code {
|
|
213
|
+
font-family: var(--mono);
|
|
214
|
+
font-size: 0.885em;
|
|
215
|
+
background: var(--code-bg);
|
|
216
|
+
padding: 1px 5px;
|
|
217
|
+
border-radius: 3px;
|
|
218
|
+
white-space: nowrap;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
pre {
|
|
222
|
+
font-family: var(--mono);
|
|
223
|
+
font-size: 13px;
|
|
224
|
+
line-height: 1.65;
|
|
225
|
+
background: var(--surface);
|
|
226
|
+
border: 1px solid var(--rule);
|
|
227
|
+
border-radius: 5px;
|
|
228
|
+
padding: 16px 18px;
|
|
229
|
+
overflow-x: auto;
|
|
230
|
+
margin: 18px 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
pre code { background: none; padding: 0; font-size: inherit; white-space: pre; }
|
|
234
|
+
|
|
235
|
+
.cmt { color: var(--muted); }
|
|
236
|
+
.kw { color: var(--accent); }
|
|
237
|
+
|
|
238
|
+
/* ---------------------------------------------------------------
|
|
239
|
+
Reference tables. Dense, hairline, sticky-headed — the grid's own
|
|
240
|
+
visual language, and the right density for scanning an API.
|
|
241
|
+
--------------------------------------------------------------- */
|
|
242
|
+
.table-wrap {
|
|
243
|
+
overflow-x: auto;
|
|
244
|
+
border: 1px solid var(--rule);
|
|
245
|
+
border-radius: 5px;
|
|
246
|
+
margin: 18px 0 26px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
table { border-collapse: collapse; width: 100%; font-size: 13.5px; }
|
|
250
|
+
|
|
251
|
+
thead th {
|
|
252
|
+
position: sticky;
|
|
253
|
+
top: 0;
|
|
254
|
+
background: var(--surface);
|
|
255
|
+
text-align: left;
|
|
256
|
+
font-size: 10.5px;
|
|
257
|
+
font-weight: 700;
|
|
258
|
+
letter-spacing: 0.09em;
|
|
259
|
+
text-transform: uppercase;
|
|
260
|
+
color: var(--muted);
|
|
261
|
+
padding: 9px 14px;
|
|
262
|
+
border-bottom: 1px solid var(--rule-strong);
|
|
263
|
+
white-space: nowrap;
|
|
264
|
+
z-index: 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
tbody td { padding: 9px 14px; border-top: 1px solid var(--rule); vertical-align: top; }
|
|
268
|
+
tbody tr:first-child td { border-top: 0; }
|
|
269
|
+
tbody tr:hover { background: var(--surface); }
|
|
270
|
+
|
|
271
|
+
td.name, td.sig {
|
|
272
|
+
font-family: var(--mono);
|
|
273
|
+
font-size: 12.5px;
|
|
274
|
+
white-space: nowrap;
|
|
275
|
+
font-variant-numeric: tabular-nums;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
td.name { font-weight: 600; }
|
|
279
|
+
td.type { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
|
|
280
|
+
td.desc { min-width: 20em; }
|
|
281
|
+
td.dflt { font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: nowrap; }
|
|
282
|
+
|
|
283
|
+
/* --------------------------------------------------------------- */
|
|
284
|
+
|
|
285
|
+
.badge {
|
|
286
|
+
display: inline-block;
|
|
287
|
+
font-family: var(--mono);
|
|
288
|
+
font-size: 10px;
|
|
289
|
+
font-weight: 700;
|
|
290
|
+
letter-spacing: 0.05em;
|
|
291
|
+
text-transform: uppercase;
|
|
292
|
+
padding: 2px 6px;
|
|
293
|
+
border-radius: 3px;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
vertical-align: 1px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.badge--dom { background: var(--accent-soft); color: var(--accent); }
|
|
299
|
+
.badge--gap { background: var(--danger-soft); color: var(--danger); }
|
|
300
|
+
|
|
301
|
+
.note {
|
|
302
|
+
max-width: var(--measure);
|
|
303
|
+
background: var(--surface);
|
|
304
|
+
border-left: 3px solid var(--rule-strong);
|
|
305
|
+
padding: 14px 18px;
|
|
306
|
+
margin: 22px 0;
|
|
307
|
+
font-size: 14px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.note--warn { border-left-color: var(--warning); }
|
|
311
|
+
.note strong { font-weight: 700; }
|
|
312
|
+
.note p { margin: 0; max-width: none; }
|
|
313
|
+
.note p + p { margin-top: 8px; }
|
|
314
|
+
|
|
315
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 26px; }
|
|
316
|
+
|
|
317
|
+
.chip {
|
|
318
|
+
font-family: var(--mono);
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
background: var(--surface);
|
|
321
|
+
border: 1px solid var(--rule);
|
|
322
|
+
border-radius: 3px;
|
|
323
|
+
padding: 3px 8px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.chip--new { border-color: var(--accent); color: var(--accent); }
|
|
327
|
+
|
|
328
|
+
footer {
|
|
329
|
+
margin-top: 72px;
|
|
330
|
+
padding-top: 22px;
|
|
331
|
+
border-top: 1px solid var(--rule);
|
|
332
|
+
font-size: 13px;
|
|
333
|
+
color: var(--muted);
|
|
334
|
+
max-width: var(--measure);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@media (max-width: 900px) {
|
|
338
|
+
.shell { grid-template-columns: 1fr; }
|
|
339
|
+
.rail {
|
|
340
|
+
position: static;
|
|
341
|
+
max-height: none;
|
|
342
|
+
border-right: 0;
|
|
343
|
+
border-bottom: 1px solid var(--rule);
|
|
344
|
+
padding: 28px 24px;
|
|
345
|
+
}
|
|
346
|
+
.rail nav { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
|
347
|
+
.main { padding: 28px 24px 80px; }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@media (prefers-reduced-motion: reduce) {
|
|
351
|
+
* { transition: none !important; animation: none !important; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
|
|
355
|
+
</style>
|
|
356
|
+
</head>
|
|
357
|
+
<body>
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
<div class="shell">
|
|
361
|
+
<aside class="rail">
|
|
362
|
+
<p class="rail__brand">Lattice Grid</p>
|
|
363
|
+
<p class="rail__sub">API reference · v1.5.4</p>
|
|
364
|
+
<nav>
|
|
365
|
+
<div class="rail__group">
|
|
366
|
+
<span class="rail__label">Start</span>
|
|
367
|
+
<a href="api-detail.html">Developer guide →</a>
|
|
368
|
+
<a href="#construction">Construction</a>
|
|
369
|
+
<a href="#config">Configuration</a>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="rail__group">
|
|
372
|
+
<span class="rail__label">Columns</span>
|
|
373
|
+
<a href="#column">Column definition</a>
|
|
374
|
+
<a href="#column-specs">Column sub-specs</a>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="rail__group">
|
|
377
|
+
<span class="rail__label">Methods</span>
|
|
378
|
+
<a href="#grid-methods">grid.*</a>
|
|
379
|
+
<a href="#rows">grid.rows</a>
|
|
380
|
+
<a href="#columns">grid.columns</a>
|
|
381
|
+
<a href="#selection">grid.selection</a>
|
|
382
|
+
<a href="#filters">grid.filters</a>
|
|
383
|
+
<a href="#sort">grid.sort</a>
|
|
384
|
+
<a href="#edit">grid.edit</a>
|
|
385
|
+
<a href="#pagination">grid.pagination</a>
|
|
386
|
+
<a href="#scroll">grid.scroll</a>
|
|
387
|
+
<a href="#export">grid.export</a>
|
|
388
|
+
<a href="#state">grid.state</a>
|
|
389
|
+
<a href="#history">grid.history</a>
|
|
390
|
+
<a href="#diagnostics">grid.diagnostics</a>
|
|
391
|
+
<a href="#presence">grid.presence</a>
|
|
392
|
+
<a href="#comments">grid.comments</a>
|
|
393
|
+
<a href="#facets">grid.facets</a>
|
|
394
|
+
<a href="#updates">grid.updates</a>
|
|
395
|
+
<a href="#timeline">grid.timeline</a>
|
|
396
|
+
<a href="#presentation">grid.presentation</a>
|
|
397
|
+
<a href="#redaction">grid.redaction</a>
|
|
398
|
+
<a href="#formatting">grid.formatting</a>
|
|
399
|
+
<a href="#highlight">grid.highlight</a>
|
|
400
|
+
<a href="#views">grid.views</a>
|
|
401
|
+
<a href="#diff">grid.diff</a>
|
|
402
|
+
<a href="#permissions">grid.permissions</a>
|
|
403
|
+
<a href="#ai">grid.ai</a>
|
|
404
|
+
<a href="#overlay">grid.overlay</a>
|
|
405
|
+
<a href="#maximise">grid.maximise</a>
|
|
406
|
+
<a href="#licence">grid.licence</a>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="rail__group">
|
|
409
|
+
<span class="rail__label">Events</span>
|
|
410
|
+
<a href="#sources">Sources</a>
|
|
411
|
+
<a href="#events">Event reference</a>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="rail__group">
|
|
414
|
+
<span class="rail__label">Registries</span>
|
|
415
|
+
<a href="#names">Built-in names</a>
|
|
416
|
+
<a href="#rules">Conditional formatting</a>
|
|
417
|
+
<a href="#quickfilter">Quick filter</a>
|
|
418
|
+
<a href="#charts">In-cell charts</a>
|
|
419
|
+
<a href="#formulas">Formulas</a>
|
|
420
|
+
<a href="#custom-menu">Custom items</a>
|
|
421
|
+
<a href="#styling">Styling</a>
|
|
422
|
+
<a href="#operators">Filter grammar</a>
|
|
423
|
+
</div>
|
|
424
|
+
</nav>
|
|
425
|
+
</aside>
|
|
426
|
+
|
|
427
|
+
<main class="main">
|
|
428
|
+
<header class="masthead">
|
|
429
|
+
<p class="eyebrow">TOCLOCO Inc</p>
|
|
430
|
+
<h1>Lattice Grid API</h1>
|
|
431
|
+
<p class="lede">
|
|
432
|
+
Every configuration property, method and event on the public surface. Extracted from
|
|
433
|
+
the shipped source, not from the specification — where the two disagree, this document
|
|
434
|
+
follows the code and says so.
|
|
435
|
+
</p>
|
|
436
|
+
</header>
|
|
437
|
+
|
|
438
|
+
<p class="chips">
|
|
439
|
+
<span class="chip">Version 1.5.4</span>
|
|
440
|
+
<span class="chip">Zero dependencies</span>
|
|
441
|
+
<span class="chip"><a href="api-detail.html">Developer guide →</a></span>
|
|
442
|
+
</p>
|
|
443
|
+
|
|
444
|
+
<h2 id="construction">Construction</h2>
|
|
445
|
+
<p class="section-note">Two files are all you need: a stylesheet and a script. Nothing is fetched at runtime — no CDN, no font, no icon sprite.</p>
|
|
446
|
+
|
|
447
|
+
<pre><code><span class="cmt"><!-- Script tag. Everything is on one global. --></span>
|
|
448
|
+
<link rel="stylesheet" href="dist/lattice-grid.min.css">
|
|
449
|
+
<script src="dist/lattice-grid.min.js"></script>
|
|
450
|
+
|
|
451
|
+
<script>
|
|
452
|
+
<span class="kw">const</span> grid = LatticeGrid.createGrid(document.getElementById('grid'), config);
|
|
453
|
+
</script></code></pre>
|
|
454
|
+
|
|
455
|
+
<p>Or as modules, importing the bundle by path:</p>
|
|
456
|
+
|
|
457
|
+
<pre><code><span class="cmt">// With a renderer, in a browser.</span>
|
|
458
|
+
<span class="kw">import</span> { createGrid } <span class="kw">from</span> './dist/lattice-grid.esm.js';
|
|
459
|
+
<span class="kw">const</span> grid = createGrid(document.getElementById('grid'), config);
|
|
460
|
+
|
|
461
|
+
<span class="cmt">// Headless: the same API without a renderer. Data, filters, sort,</span>
|
|
462
|
+
<span class="cmt">// grouping, totals, formatting and export all work; grid.element is</span>
|
|
463
|
+
<span class="cmt">// null and the DOM-only chrome is simply absent.</span>
|
|
464
|
+
<span class="cmt">// Runs in Node, for tests and server-side export.</span>
|
|
465
|
+
<span class="kw">import</span> { createHeadlessGrid } <span class="kw">from</span> './dist/lattice-core.esm.js';
|
|
466
|
+
<span class="kw">const</span> grid = createHeadlessGrid(config);</code></pre>
|
|
467
|
+
|
|
468
|
+
<h2 id="adapters">Framework adapters</h2>
|
|
469
|
+
<p class="section-note">The web component carries the grid inside it, so use it <em>or</em> <code>createGrid</code> in a page, not both — two copies keep separate registries, and a renderer registered through one is invisible to the other. One optional bundle per framework. The framework and <code>createGrid</code> are passed in rather than imported, so the adapters add no dependency and carry no second copy of the grid.</p>
|
|
470
|
+
|
|
471
|
+
<pre><code><span class="cmt">// React</span>
|
|
472
|
+
<span class="kw">import</span> React <span class="kw">from</span> 'react';
|
|
473
|
+
<span class="kw">import</span> { createGrid } <span class="kw">from</span> './dist/lattice-grid.esm.js';
|
|
474
|
+
<span class="kw">import</span> { createLatticeGrid } <span class="kw">from</span> './dist/modules/react.esm.js';
|
|
475
|
+
|
|
476
|
+
<span class="kw">const</span> LatticeGrid = createLatticeGrid({ React, createGrid });
|
|
477
|
+
<LatticeGrid columns={columns} rows={rows} rowKey="id" onCellChanged={fn} />
|
|
478
|
+
|
|
479
|
+
<span class="cmt">// Vue 3</span>
|
|
480
|
+
<span class="kw">import</span> * <span class="kw">as</span> vue <span class="kw">from</span> 'vue';
|
|
481
|
+
<span class="kw">import</span> { createLatticeGrid } <span class="kw">from</span> './dist/modules/vue.esm.js';
|
|
482
|
+
|
|
483
|
+
<span class="kw">const</span> LatticeGrid = createLatticeGrid({ vue, createGrid });
|
|
484
|
+
<LatticeGrid :columns="columns" :rows="rows" row-key="id" @cell-changed="fn" />
|
|
485
|
+
|
|
486
|
+
<span class="cmt">// Svelte — an action, so no framework runtime is needed</span>
|
|
487
|
+
<span class="kw">import</span> { createLatticeAction } <span class="kw">from</span> './dist/modules/svelte.esm.js';
|
|
488
|
+
|
|
489
|
+
<span class="kw">const</span> lattice = createLatticeAction({ createGrid });
|
|
490
|
+
<div use:lattice={{ columns, rows, rowKey: 'id' }} on:cell-changed={fn}></div></code></pre>
|
|
491
|
+
|
|
492
|
+
<div class="table-wrap">
|
|
493
|
+
<table>
|
|
494
|
+
<thead><tr><th>Entry point</th><th>Factory</th><th>Needs</th></tr></thead>
|
|
495
|
+
<tbody>
|
|
496
|
+
<tr><td class="name">modules/react</td><td class="sig">createLatticeGrid({ React, createGrid })</td><td class="desc">Returns a component. Forwards a ref exposing <code>.grid</code>.</td></tr>
|
|
497
|
+
<tr><td class="name">modules/vue</td><td class="sig">createLatticeGrid({ vue, createGrid })</td><td class="desc">Returns a Vue 3 component definition.</td></tr>
|
|
498
|
+
<tr><td class="name">modules/svelte</td><td class="sig">createLatticeAction({ createGrid })</td><td class="desc">Returns a <code>use:</code> action.</td></tr>
|
|
499
|
+
</tbody>
|
|
500
|
+
</table>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div class="table-wrap">
|
|
504
|
+
<table>
|
|
505
|
+
<thead><tr><th>Prop</th><th>Type</th><th>Does</th></tr></thead>
|
|
506
|
+
<tbody>
|
|
507
|
+
<tr><td class="name">any config key</td><td class="type">as documented below</td><td class="desc">Applied through <code>grid.setAll()</code> when the reference changes. Never rebuilds the grid.</td></tr>
|
|
508
|
+
<tr><td class="name">sort</td><td class="type">SortEntry[]</td><td class="desc"><code>grid.sort.set()</code></td></tr>
|
|
509
|
+
<tr><td class="name">filters</td><td class="type">FilterSet</td><td class="desc"><code>grid.filters.set()</code></td></tr>
|
|
510
|
+
<tr><td class="name">quickFilter</td><td class="type">string | { text, mode }</td><td class="desc"><code>grid.filters.quick()</code></td></tr>
|
|
511
|
+
<tr><td class="name">selectedKeys</td><td class="type">string[]</td><td class="desc"><code>grid.selection.set()</code></td></tr>
|
|
512
|
+
<tr><td class="name">on<Event></td><td class="type">(e: GridEvent) => void</td><td class="desc">One per event. <code>cell:changed</code> → <code>onCellChanged</code> in React; <code>@cell-changed</code> in Vue; <code>on:cell-changed</code> in Svelte.</td></tr>
|
|
513
|
+
<tr><td class="name">className, style, id</td><td class="type">string | object</td><td class="desc">React only. Applied to the host element, not the grid.</td></tr>
|
|
514
|
+
</tbody>
|
|
515
|
+
</table>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<div class="note">
|
|
519
|
+
<p><strong>There is no npm package.</strong> Lattice Grid is <code>private: true</code> and is not
|
|
520
|
+
published to any registry, so <code>import … from '@latticegrid/dom'</code> will not resolve.
|
|
521
|
+
Import the built file by path, or use the script tag. The same is true of the type
|
|
522
|
+
declarations: point your <code>tsconfig</code> at <code>dist/lattice-grid.d.ts</code> rather
|
|
523
|
+
than expecting <code>@types</code> to find them.</p>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<div class="table-wrap">
|
|
527
|
+
<table>
|
|
528
|
+
<thead><tr><th>File</th><th>Needed?</th><th>What it is</th></tr></thead>
|
|
529
|
+
<tbody>
|
|
530
|
+
<tr><td class="sig">lattice-grid.min.js</td><td>yes</td><td class="desc">The whole product as a UMD build — core, renderer, editors, exports. Defines <code>window.LatticeGrid</code>, and also works with AMD or CommonJS loaders.</td></tr>
|
|
531
|
+
<tr><td class="sig">lattice-grid.min.css</td><td>yes</td><td class="desc">The single stylesheet. Without it the grid is in the DOM and unreadable — no column widths, no scrolling, no theme.</td></tr>
|
|
532
|
+
<tr><td class="sig">lattice-grid.esm.min.js</td><td>alternative</td><td class="desc">The same thing as an ES module, if you are importing rather than script-tagging.</td></tr>
|
|
533
|
+
<tr><td class="sig">lattice-core.esm.js</td><td>optional</td><td class="desc">Headless core only, for Node. No renderer.</td></tr>
|
|
534
|
+
<tr><td class="sig">lattice-grid.d.ts</td><td>optional</td><td class="desc">Type declarations, for editor tooling.</td></tr>
|
|
535
|
+
<tr><td class="sig">the unminified builds</td><td>optional</td><td class="desc"><code>lattice-grid.js</code>, <code>.esm.js</code>, <code>.css</code> — readable source for debugging. Ship the minified ones.</td></tr>
|
|
536
|
+
</tbody>
|
|
537
|
+
</table>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div class="table-wrap">
|
|
541
|
+
<table>
|
|
542
|
+
<thead><tr><th>Signature</th><th>Returns</th><th>Notes</th></tr></thead>
|
|
543
|
+
<tbody>
|
|
544
|
+
<tr>
|
|
545
|
+
<td class="sig">createGrid(element, config?)</td>
|
|
546
|
+
<td class="type">Grid</td>
|
|
547
|
+
<td class="desc">Resolves the document from <code>element.ownerDocument</code>, so a grid inside an iframe uses that frame's document. Throws with a clear message if there is no DOM.</td>
|
|
548
|
+
</tr>
|
|
549
|
+
<tr>
|
|
550
|
+
<td class="sig">createHeadlessGrid(config?)</td>
|
|
551
|
+
<td class="type">Grid</td>
|
|
552
|
+
<td class="desc">Core only. Everything below except <code>grid.element</code> and the DOM-only config keys works unchanged.</td>
|
|
553
|
+
</tr>
|
|
554
|
+
</tbody>
|
|
555
|
+
</table>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<h2 id="webcomponent"><lattice-grid> web component</h2>
|
|
559
|
+
<p class="section-note">A self-contained module bundle that registers a custom element on import. One script, one tag, no build step — for Rails, Django, Laravel or any page without a bundler. Load this <em>or</em> <code>lattice-grid.esm.js</code>, not both: the module carries the grid with it.</p>
|
|
560
|
+
|
|
561
|
+
<pre><code><link rel="stylesheet" href="dist/lattice-grid.min.css">
|
|
562
|
+
<script type="module" src="dist/modules/webcomponent.esm.min.js"></script>
|
|
563
|
+
|
|
564
|
+
<lattice-grid row-key="id"
|
|
565
|
+
columns='[{"field":"id"},{"field":"city"}]'
|
|
566
|
+
rows='[{"id":"A","city":"Leeds"}]'></lattice-grid></code></pre>
|
|
567
|
+
|
|
568
|
+
<div class="table-wrap">
|
|
569
|
+
<table>
|
|
570
|
+
<thead><tr><th>Attribute</th><th>Config key</th><th>Notes</th></tr></thead>
|
|
571
|
+
<tbody>
|
|
572
|
+
<tr><td class="sig">theme</td><td class="type">theme</td><td class="desc"></td></tr>
|
|
573
|
+
<tr><td class="sig">density</td><td class="type">density</td><td class="desc"></td></tr>
|
|
574
|
+
<tr><td class="sig">locale</td><td class="type">locale</td><td class="desc"></td></tr>
|
|
575
|
+
<tr><td class="sig">row-key</td><td class="type">rowKey</td><td class="desc"></td></tr>
|
|
576
|
+
<tr><td class="sig">row-height</td><td class="type">rowHeight</td><td class="desc">Number.</td></tr>
|
|
577
|
+
<tr><td class="sig">header-height</td><td class="type">headerHeight</td><td class="desc">Number.</td></tr>
|
|
578
|
+
<tr><td class="sig">auto-height</td><td class="type">autoHeight</td><td class="desc">Boolean attribute.</td></tr>
|
|
579
|
+
<tr><td class="sig">selection</td><td class="type">selection</td><td class="desc"><code>none</code>, <code>single</code> or <code>multiple</code>.</td></tr>
|
|
580
|
+
<tr><td class="sig">rows</td><td class="type">rows</td><td class="desc">JSON. Prefer the property.</td></tr>
|
|
581
|
+
<tr><td class="sig">columns</td><td class="type">columns</td><td class="desc">JSON. Prefer the property.</td></tr>
|
|
582
|
+
</tbody>
|
|
583
|
+
</table>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div class="table-wrap">
|
|
587
|
+
<table>
|
|
588
|
+
<thead><tr><th>Property</th><th>Type</th><th>Notes</th></tr></thead>
|
|
589
|
+
<tbody>
|
|
590
|
+
<tr><td class="sig">rows</td><td class="type">unknown[]</td><td class="desc">Row data. A structure, so a property rather than an attribute.</td></tr>
|
|
591
|
+
<tr><td class="sig">columns</td><td class="type">Column[]</td><td class="desc">Column definitions.</td></tr>
|
|
592
|
+
<tr><td class="sig">config</td><td class="type">GridConfig</td><td class="desc">Merges any configuration without an attribute of its own.</td></tr>
|
|
593
|
+
<tr><td class="sig">grid</td><td class="type">Grid | null</td><td class="desc">Read-only. The underlying grid, for the full imperative API.</td></tr>
|
|
594
|
+
</tbody>
|
|
595
|
+
</table>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<p class="section-note">Events are re-dispatched as <code>CustomEvent</code>s named <code>lattice-</code> plus the grid name with colons hyphenated: <code>cell:changed</code> becomes <code>lattice-cell-changed</code>. The payload is on <code>event.detail</code>. The prefix avoids colliding with platform events — the grid emits one called <code>scroll</code>.</p>
|
|
599
|
+
|
|
600
|
+
<h2 id="config">Configuration properties</h2>
|
|
601
|
+
<p class="section-note">
|
|
602
|
+
Every key is settable at runtime through <code>grid.set(key, value)</code>. Keys marked
|
|
603
|
+
<span class="badge badge--dom">dom</span> are read by <code>createGrid</code> and ignored by a headless grid.
|
|
604
|
+
</p>
|
|
605
|
+
|
|
606
|
+
<h3>Data and structure</h3>
|
|
607
|
+
<div class="table-wrap">
|
|
608
|
+
<table>
|
|
609
|
+
<thead><tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
610
|
+
<tbody>
|
|
611
|
+
<tr><td class="name">columns</td><td class="type">(Column | ColumnGroup)[]</td><td class="dflt">—</td><td class="desc">Column definitions. Groups may nest.</td></tr>
|
|
612
|
+
<tr><td class="name">columnGroups</td><td class="type">ColumnGroup[]</td><td class="dflt">—</td><td class="desc">Header grouping declared separately from the columns.</td></tr>
|
|
613
|
+
<tr><td class="name">rows</td><td class="type">unknown[]</td><td class="dflt">—</td><td class="desc">Row objects. Held by reference; not copied.</td></tr>
|
|
614
|
+
<tr><td class="name">rowKey</td><td class="type">string | (row) => string</td><td class="dflt">—</td><td class="desc">Stable row identity. Without it the grid assigns a key per row object and warns: enough for sorting, filtering, selection and copying within a session, but change tracking, streaming dedupe, selection persistence and remote reload all switch off, because new objects are new rows.</td></tr>
|
|
615
|
+
<tr><td class="name">source</td><td class="type">SourceConfig</td><td class="dflt">memory</td><td class="desc">Where rows come from: <code>memory</code>, <code>paged</code>, <code>remote</code> or <code>stream</code>. See <a href="#sources">Sources</a>.</td></tr>
|
|
616
|
+
<tr><td class="name">tree</td><td class="type">TreeConfig</td><td class="dflt">—</td><td class="desc"><code>{ path }</code> or <code>{ parentKey }</code>, plus <code>label</code>, <code>orphans</code>. Rows form a hierarchy. See <a href="api-detail.html#tree-data">Tree data</a>.</td></tr>
|
|
617
|
+
<tr><td class="name">detail</td><td class="type">DetailConfig</td><td class="dflt">—</td><td class="desc"><code>{ rows, config, render, isMaster, height, cacheLimit, target }</code>. A master row expands into a nested grid, inline or into an element you supply. See <a href="api-detail.html#master-detail">Master-detail</a>.</td></tr>
|
|
618
|
+
<tr><td class="name">context</td><td class="type">unknown</td><td class="dflt">—</td><td class="desc">Arbitrary value passed to every callback, so formatters and renderers need no closures over app state.</td></tr>
|
|
619
|
+
</tbody>
|
|
620
|
+
</table>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<h3>Defaults and registries</h3>
|
|
624
|
+
<div class="table-wrap">
|
|
625
|
+
<table>
|
|
626
|
+
<thead><tr><th>Property</th><th>Type</th><th>Description</th></tr></thead>
|
|
627
|
+
<tbody>
|
|
628
|
+
<tr><td class="name">columnDefaults</td><td class="type">Column</td><td class="desc">Merged under every column before its own definition.</td></tr>
|
|
629
|
+
<tr><td class="name">columnPresets</td><td class="type">Record<string, Column></td><td class="desc">Named bundles applied with <code>preset: 'money'</code>.</td></tr>
|
|
630
|
+
<tr><td class="name">dataTypes</td><td class="type">Record<string, DataType></td><td class="desc">Custom types. Registered ahead of the built-ins, so a name here overrides one of ours.</td></tr>
|
|
631
|
+
<tr><td class="name">sampleSize</td><td class="type">number</td><td class="desc">Values read per undeclared column when inferring its type. Default 100.</td></tr>
|
|
632
|
+
<tr><td class="name">targetSize</td><td class="type">'default' | 'large'</td><td class="desc">Raises every interactive target to a comfortable size for touch, leaving the type alone. Applied automatically on a coarse pointer; <code>'default'</code> opts out of that.</td></tr>
|
|
633
|
+
<tr><td class="name">components</td><td class="type">Record<string, Ctor></td><td class="desc">Renderers, editors and filters addressable by name.</td></tr>
|
|
634
|
+
<tr><td class="name">pipes</td><td class="type">Record<string, fn></td><td class="desc">Template pipes for <code>cell.template</code>.</td></tr>
|
|
635
|
+
<tr><td class="name">totalFns</td><td class="type">Record<string, TotalFn></td><td class="desc">Custom aggregations, addressable from <code>column.total</code>.</td></tr>
|
|
636
|
+
<tr><td class="name">variants</td><td class="type">Record<string, VariantDefinition></td><td class="desc">Semantic colour tokens for decorations.</td></tr>
|
|
637
|
+
</tbody>
|
|
638
|
+
</table>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
641
|
+
<h3>Behaviour</h3>
|
|
642
|
+
<div class="table-wrap">
|
|
643
|
+
<table>
|
|
644
|
+
<thead><tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
645
|
+
<tbody>
|
|
646
|
+
<tr><td class="name">selection</td><td class="type">SelectionConfig | 'single' | 'multiple' | 'none'</td><td class="dflt">—</td><td class="desc">Object form adds <code>checkbox</code> (a pinned column of row checkboxes), <code>headerCheckbox</code> (tri-state select-all in its heading), <code>groupSelectsChildren</code>, <code>ranges</code>, <code>fillHandle</code>, <code>fill</code>. See <a href="api-detail.html#selection-checkbox">Selection and ranges</a>.</td></tr>
|
|
647
|
+
<tr><td class="name">edit</td><td class="type">EditConfig | boolean</td><td class="dflt">—</td><td class="desc"><code>{ enabled, mode: 'cell' | 'row', start: 'single' | 'double' | 'key', enterMovesDown, undoDepth, commit, confirm, pendingTimeout }</code>. The last three turn on optimistic writes.</td></tr>
|
|
648
|
+
<tr><td class="name">pagination</td><td class="type">PaginationConfig | boolean</td><td class="dflt">—</td><td class="desc">Local or remote paging.</td></tr>
|
|
649
|
+
<tr><td class="name">quickFilterText</td><td class="type">string</td><td class="dflt">—</td><td class="desc">Initial quick-filter term. Equivalent to <code>grid.filters.quick(text)</code>.</td></tr>
|
|
650
|
+
<tr><td class="name">hostFilter</td><td class="type">{ active(), passes(row) }</td><td class="dflt">—</td><td class="desc">An application-level predicate composed with the grid's own filters.</td></tr>
|
|
651
|
+
<tr><td class="name">pivot</td><td class="type">object</td><td class="dflt">—</td><td class="desc"><code>{ enabled, groupTotals, totalsLabel, maxColumns, separator }</code>. <code>groupTotals: 'before' | 'after'</code> adds a column group totalling every value column across all pivot values, at the near or far edge; omitted, it adds none. <code>totalsLabel</code> heads it, defaulting to <code>Total</code>. <code>maxColumns</code> defaults to 500, counts the totals group, and fails with a message rather than locking the browser.</td></tr>
|
|
652
|
+
<tr><td class="name">grandTotalRow</td><td class="type">boolean | 'bottom'</td><td class="dflt">false</td><td class="desc"><code>true</code> puts it inline at the end of the rows; <code>'bottom'</code> pins it above the status bar. Maintained incrementally on a memory source — see <a href="api-detail.html#grouping">Grouping, totals and pivot</a>.</td></tr>
|
|
653
|
+
<tr><td class="name">groupFooter</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">A closing total row per group.</td></tr>
|
|
654
|
+
<tr><td class="name">totalFilteredOnly</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">Totals reduce the filtered set. <code>false</code> totals the whole dataset, group totals included. See <a href="api-detail.html#total-filtered-only">Grouping, totals and pivot</a>.</td></tr>
|
|
655
|
+
<tr><td class="name">totalOnlyChangedColumns</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">Reduce only the totalled columns an edit actually changed. Off by default — it asserts that each total depends on nothing but its own column. See <a href="api-detail.html#grouping">Grouping, totals and pivot</a>.</td></tr>
|
|
656
|
+
<tr><td class="name">showTotalInHeader</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">Under grouping or pivot, a totalled column's heading names its reduction on a line above the column name. See <a href="api-detail.html#show-total-in-header">Grouping, totals and pivot</a>.</td></tr>
|
|
657
|
+
<tr><td class="name">allowUnsafeTemplates</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">Off by default. Templates are escaped unless this is explicitly set. When set, an interpolated value may contain presentational markup — but script is still removed from it: <code><script></code>, <code><iframe></code> and the other executable tags, <code>on*</code> handler attributes, and <code>javascript:</code> URLs. The flag permits markup, not code.</td></tr>
|
|
658
|
+
<tr><td class="name">licence</td><td class="type">string</td><td class="dflt">—</td><td class="desc">Signed licence key. Removes the trial watermark; unlocks nothing, because nothing is locked.</td></tr>
|
|
659
|
+
</tbody>
|
|
660
|
+
</table>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<h3>Presentation</h3>
|
|
664
|
+
<div class="table-wrap">
|
|
665
|
+
<table>
|
|
666
|
+
<thead><tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
667
|
+
<tbody>
|
|
668
|
+
<tr><td class="name">locale</td><td class="type">string</td><td class="dflt">runtime</td><td class="desc">BCP-47. Drives every formatter and one shared <code>Intl.Collator</code>.</td></tr>
|
|
669
|
+
<tr><td class="name">theme</td><td class="type">'light' | 'dark' | 'high-contrast' | 'terminal'</td><td class="dflt">—</td><td class="desc">Stamped as <code>data-theme</code> on the grid's root. Unset follows the viewer's <code>prefers-color-scheme</code>. See <a href="api-detail.html#theming">Theming</a>.</td></tr>
|
|
670
|
+
<tr><td class="name">density</td><td class="type">'compact' | 'standard' | 'comfortable' | 'spacious' | number</td><td class="dflt">'compact'</td><td class="desc">One scale that every geometry token derives from — row height, spacing, decoration sizes, and type at a damped rate. Row heights are 23.8 / 28 / 42 / 56px. A number scales 28px, so <code>1.4</code> gives 39.2px for anything between the presets. Virtualisation follows it; an explicit <code>rowHeight</code> overrides it.</td></tr>
|
|
671
|
+
<tr><td class="name">rowHeight</td><td class="type">number | (row) => number</td><td class="dflt">28</td><td class="desc">A function enables variable-height rows.</td></tr>
|
|
672
|
+
<tr><td class="name">headerHeight</td><td class="type">number</td><td class="dflt">32</td><td class="desc">Per header row.</td></tr>
|
|
673
|
+
<tr><td class="name">overscan</td><td class="type">number</td><td class="dflt">4</td><td class="desc">Rows rendered beyond the viewport.</td></tr>
|
|
674
|
+
<tr><td class="name">autoHeight</td><td class="type">boolean | 'visible'</td><td class="dflt">—</td><td class="desc">Size rows to their content: cells wrap instead of ellipsising, and each row takes the height its tallest cell needs. Only rendered rows are measured either way — the difference is that <code>true</code> gives up above 10,000 rows and returns to fixed heights, while <code>'visible'</code> keeps measuring at any size and accepts a scrollbar that shifts as rows are measured on the way past.</td></tr>
|
|
675
|
+
<tr><td class="name">columnVirtualisationAbove</td><td class="type">number</td><td class="dflt">30</td><td class="desc">Column count above which columns virtualise too.</td></tr>
|
|
676
|
+
<tr><td class="name">state</td><td class="type">GridState</td><td class="dflt">—</td><td class="desc">Restore a saved view at construction.</td></tr>
|
|
677
|
+
</tbody>
|
|
678
|
+
</table>
|
|
679
|
+
</div>
|
|
680
|
+
|
|
681
|
+
<h3>Performance</h3>
|
|
682
|
+
<div class="table-wrap">
|
|
683
|
+
<table>
|
|
684
|
+
<thead><tr><th>Property</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
685
|
+
<tbody>
|
|
686
|
+
<tr><td class="name">useWorker</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">Compute column distributions off the main thread. Sorting, filtering and grouping run on the main thread.</td></tr>
|
|
687
|
+
<tr><td class="name">workerThreshold</td><td class="type">number</td><td class="dflt">50000</td><td class="desc">Row count above which a distribution is sent to the Worker.</td></tr>
|
|
688
|
+
<tr><td class="name">workerUrl</td><td class="type">string</td><td class="dflt">—</td><td class="desc">External worker file, for a CSP that forbids <code>blob:</code>. Settled when the Worker is built; changing it rebuilds one.</td></tr>
|
|
689
|
+
<tr><td class="name">sharedMemory</td><td class="type">boolean</td><td class="dflt">false</td><td class="desc">Pass columns to the worker in a <code>SharedArrayBuffer</code> instead of copying them, where the page is cross-origin isolated. Retains a shared copy of each column that crosses.</td></tr>
|
|
690
|
+
</tbody>
|
|
691
|
+
</table>
|
|
692
|
+
</div>
|
|
693
|
+
|
|
694
|
+
<h3>Chrome <span class="badge badge--dom">dom</span></h3>
|
|
695
|
+
<div class="table-wrap">
|
|
696
|
+
<table>
|
|
697
|
+
<thead><tr><th>Property</th><th>Type</th><th>Description</th></tr></thead>
|
|
698
|
+
<tbody>
|
|
699
|
+
<tr><td class="name">statusBar</td><td class="type">boolean | { panels }</td><td class="desc">Composable panels along the bottom. Default set: <code>rowCount</code>, <code>selectedCount</code>, <code>aggregation</code>, <code>comments</code>, <code>updates</code>, <code>progress</code>. Each is silent when it has nothing to report.</td></tr>
|
|
700
|
+
<tr><td class="name">maximise</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc"><code>false</code> removes the rail button and <code>grid.maximise</code>, for an application with its own full-screen mode.</td></tr>
|
|
701
|
+
<tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code> — nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV.</td></tr>
|
|
702
|
+
<tr><td class="name">contextMenu</td><td class="type">boolean | (p) => MenuItem[]</td><td class="desc">Right-click menu. The function form is <code>(params, defaults) => items</code> — see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it — what a read-only grid wants, since the default menu offers Paste, Clear and Fill down.</td></tr>
|
|
703
|
+
<tr><td class="name">columnMenu</td><td class="type">boolean</td><td class="desc">The header's 3-dot menu. <code>false</code> suppresses it.</td></tr>
|
|
704
|
+
<tr><td class="name">highlightOnChange</td><td class="type">boolean | string | object</td><td class="desc">Flash a cell when its value changes. <code>{ colour, duration }</code>; <code>duration: 0</code> stays until cleared.</td></tr>
|
|
705
|
+
<tr><td class="name">rowClass</td><td class="type">string | string[] | (p) => …</td><td class="desc">A class, or classes, for every row. Re-evaluated on each repaint.</td></tr>
|
|
706
|
+
<tr><td class="name">rowStyle</td><td class="type">CellStyle | (p) => CellStyle</td><td class="desc">Inline styles for every row. Camel-case or hyphenated property names.</td></tr>
|
|
707
|
+
<tr><td class="name">views</td><td class="type">object</td><td class="desc"><code>saved</code>, <code>allowSave</code>, <code>storage</code>. See <a href="#views">grid.views</a>.</td></tr>
|
|
708
|
+
<tr><td class="name">permissions</td><td class="type">string | object | fn</td><td class="desc">Per-column access. See <a href="#permissions">grid.permissions</a>.</td></tr>
|
|
709
|
+
<tr><td class="name">diff</td><td class="type">object</td><td class="desc"><code>{ snapshot }</code> turns on audit mode.</td></tr>
|
|
710
|
+
<tr><td class="name">historyBar</td><td class="type">boolean | object</td><td class="desc">A standalone undo/redo toolbar with a timeline.</td></tr>
|
|
711
|
+
<tr><td class="name">ai</td><td class="type">object</td><td class="desc"><code>{ ask }</code> mounts the prompt bar. Your <code>ask</code> receives <code>{ prompt, schema, schemaText, message, context }</code> and returns the model's reply.</td></tr>
|
|
712
|
+
<tr><td class="name">dataTypes</td><td class="type">object</td><td class="desc">Custom types by name. <code>createRadixType</code> and <code>createUnitType</code> are exported for building them.</td></tr>
|
|
713
|
+
<tr><td class="name">editBar</td><td class="type">boolean</td><td class="desc">A spreadsheet-style input above the header. When on, it hosts the column's real editor and inline editing is suppressed.</td></tr>
|
|
714
|
+
<tr><td class="name">pagination</td><td class="type">boolean | object</td><td class="desc">Renders the pager control: page size, a summary, first/previous/next/last, and a page number you can type into and press Enter to jump.</td></tr>
|
|
715
|
+
</tbody>
|
|
716
|
+
</table>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<h2 id="column">Column definition</h2>
|
|
720
|
+
<p class="section-note">Everything is optional. A column with only <code>field</code> infers its type from sampled data and takes every default from there.</p>
|
|
721
|
+
|
|
722
|
+
<div class="table-wrap">
|
|
723
|
+
<table>
|
|
724
|
+
<thead><tr><th>Property</th><th>Type</th><th>Description</th></tr></thead>
|
|
725
|
+
<tbody>
|
|
726
|
+
<tr><td class="name">id</td><td class="type">string</td><td class="desc">Defaults to <code>field</code>. Required when there is no field.</td></tr>
|
|
727
|
+
<tr><td class="name">field</td><td class="type">string</td><td class="desc">Dotted paths supported: <code>'site.address.postcode'</code>.</td></tr>
|
|
728
|
+
<tr><td class="name">title</td><td class="type">string</td><td class="desc">Header text. Defaults to a humanised <code>field</code>.</td></tr>
|
|
729
|
+
<tr><td class="name">type</td><td class="type">TypeName | false</td><td class="desc">A data type bundles format, parse, compare, storage, editor, filter, renderer and Excel behaviour. <code>false</code> disables inference. <code>'image'</code> treats the value as a URL and draws it — see <a href="#image-guide">image columns</a>.</td></tr>
|
|
730
|
+
<tr><td class="name">preset</td><td class="type">string | string[]</td><td class="desc">Named bundles from <code>columnPresets</code>.</td></tr>
|
|
731
|
+
<tr><td class="name">format</td><td class="type">FormatSpec | string</td><td class="desc">Shorthand strings like <code>'percent:1'</code> or <code>'date:dd MMM yyyy'</code>.</td></tr>
|
|
732
|
+
<tr><td class="name">lookup</td><td class="type">LookupSpec</td><td class="desc">Id-to-label mapping. Nested <code>children</code> are flattened, so a tree-shaped list resolves labels everywhere.</td></tr>
|
|
733
|
+
<tr><td class="name">value</td><td class="type">ColumnValueSpec</td><td class="desc">Computed values and the value lifecycle.</td></tr>
|
|
734
|
+
<tr><td class="name">cell</td><td class="type">ColumnCellSpec | string</td><td class="desc">A bare string is a renderer name.</td></tr>
|
|
735
|
+
<tr><td class="name">edit</td><td class="type">ColumnEditSpec | boolean | string</td><td class="desc">A bare string is an editor name.</td></tr>
|
|
736
|
+
<tr><td class="name">sort</td><td class="type">ColumnSortSpec | boolean</td><td class="desc"></td></tr>
|
|
737
|
+
<tr><td class="name">filter</td><td class="type">ColumnFilterSpec | boolean | FilterName</td><td class="desc"></td></tr>
|
|
738
|
+
<tr><td class="name">group</td><td class="type">object | boolean</td><td class="desc"><code>{ enabled, index, explode }</code>.</td></tr>
|
|
739
|
+
<tr><td class="name">pivot</td><td class="type">object | boolean</td><td class="desc"><code>{ enabled, index }</code>.</td></tr>
|
|
740
|
+
<tr><td class="name">total</td><td class="type">TotalName | TotalFn</td><td class="desc">One property drives the group row, the tree node, the pivot cell and the grand total.</td></tr>
|
|
741
|
+
<tr><td class="name">layout</td><td class="type">ColumnLayoutSpec | number</td><td class="desc">A bare number is the width.</td></tr>
|
|
742
|
+
<tr><td class="name">header</td><td class="type">ColumnHeaderSpec | string</td><td class="desc"></td></tr>
|
|
743
|
+
<tr><td class="name">export</td><td class="type">ColumnExportSpec</td><td class="desc"><code>{ lookup: 'label' | 'value' | 'columns', csv, excel }</code>.</td></tr>
|
|
744
|
+
<tr><td class="name">allowGroup / allowPivot / allowTotal</td><td class="type">boolean</td><td class="desc">Whether the tool panel offers the column for that zone.</td></tr>
|
|
745
|
+
<tr><td class="name">nullable</td><td class="type">boolean</td><td class="desc">Affects storage choice and null ordering.</td></tr>
|
|
746
|
+
</tbody>
|
|
747
|
+
</table>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
<h2 id="column-specs">Column sub-specs</h2>
|
|
751
|
+
|
|
752
|
+
<h3>value</h3>
|
|
753
|
+
<div class="table-wrap">
|
|
754
|
+
<table>
|
|
755
|
+
<thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
|
|
756
|
+
<tbody>
|
|
757
|
+
<tr><td class="name">compute</td><td class="type">(deps, ctx) => unknown</td><td class="desc">Derived value. Receives only its declared dependencies.</td></tr>
|
|
758
|
+
<tr><td class="name">deps</td><td class="type">string[] | '*'</td><td class="desc">Declared dependencies. Cycles are caught at compile time, not at render.</td></tr>
|
|
759
|
+
<tr><td class="name">pure</td><td class="type">boolean</td><td class="desc">Allows caching. A DEV-mode proxy flags impure computes that read outside their deps.</td></tr>
|
|
760
|
+
<tr><td class="name">format</td><td class="type">(p) => string</td><td class="desc">Overrides the type's formatter.</td></tr>
|
|
761
|
+
<tr><td class="name">parse</td><td class="type">(p) => unknown</td><td class="desc">Editor output to value. Always called, whatever the editor emitted.</td></tr>
|
|
762
|
+
<tr><td class="name">apply</td><td class="type">(p) => boolean</td><td class="desc">Writes the value back into the row object.</td></tr>
|
|
763
|
+
<tr><td class="name">key</td><td class="type">(p) => string</td><td class="desc">Group key override.</td></tr>
|
|
764
|
+
<tr><td class="name">compare</td><td class="type">Comparator</td><td class="desc">Overrides the type's comparator.</td></tr>
|
|
765
|
+
<tr><td class="name">quickFilterText</td><td class="type">(p) => string</td><td class="desc">What the quick filter matches against.</td></tr>
|
|
766
|
+
</tbody>
|
|
767
|
+
</table>
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<h3>cell</h3>
|
|
771
|
+
<div class="table-wrap">
|
|
772
|
+
<table>
|
|
773
|
+
<thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
|
|
774
|
+
<tbody>
|
|
775
|
+
<tr><td class="name">render</td><td class="type">RendererName | RenderFn | Ctor</td><td class="desc">Renderer name or component. The built-in names are listed under <a href="#builtin-renderers">built-in renderers</a>.</td></tr>
|
|
776
|
+
<tr><td class="name">props</td><td class="type">object</td><td class="desc">Passed to the renderer.</td></tr>
|
|
777
|
+
<tr><td class="name">decoration</td><td class="type">DecorationName | spec</td><td class="desc"><code>pill</code>, <code>bar</code>, <code>fill</code>, <code>dot</code>, <code>edge</code>.</td></tr>
|
|
778
|
+
<tr><td class="name">variant</td><td class="type">VariantSpec</td><td class="desc">Maps a value to a semantic token: <code>{ map }</code>, or <code>{ when: [...], default }</code>.</td></tr>
|
|
779
|
+
<tr><td class="name">template</td><td class="type">string</td><td class="desc">Escaped unless <code>allowUnsafeTemplates</code> is set.</td></tr>
|
|
780
|
+
<tr><td class="name">class</td><td class="type">string | string[] | (p) => …</td><td class="desc">Classes for this column's cells.</td></tr>
|
|
781
|
+
<tr><td class="name">classWhen</td><td class="type">{ [class]: (p) => boolean }</td><td class="desc">A class per predicate, re-evaluated as values change.</td></tr>
|
|
782
|
+
<tr><td class="name">style / css</td><td class="type">CellStyle | (p) => CellStyle</td><td class="desc">Inline styles, static or computed.</td></tr>
|
|
783
|
+
<tr><td class="name">tooltip</td><td class="type">string | (p) => string</td><td class="desc"></td></tr>
|
|
784
|
+
<tr><td class="name">align / wrap / autoHeight</td><td class="type">—</td><td class="desc">Presentation flags.</td></tr>
|
|
785
|
+
<tr><td class="name">spanColumns / spanRows</td><td class="type">(p) => number</td><td class="desc">Spanned cells render in their own layer so row recycling cannot clip them.</td></tr>
|
|
786
|
+
</tbody>
|
|
787
|
+
</table>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<div class="note">
|
|
791
|
+
<p><strong>Custom CSS, by scope.</strong> <em>Cells</em>: <code>cell.class</code>,
|
|
792
|
+
<code>cell.classWhen</code>, <code>cell.style</code> and <code>cell.css</code>, all of which
|
|
793
|
+
may be functions of the cell. <em>Columns</em>: the same four, declared on the column, so they
|
|
794
|
+
apply to every cell in it; the header takes <code>header.class</code>. <em>Rows</em>:
|
|
795
|
+
<code>rowClass</code> and <code>rowStyle</code> on the grid.</p>
|
|
796
|
+
<p>All of them are re-evaluated on every repaint and remove what they added last time first.
|
|
797
|
+
That is not caution: rows and cells come from pools, so an element that carried a class for
|
|
798
|
+
one row will later carry a different row, and a class written once and left alone smears down
|
|
799
|
+
the grid as the user scrolls.</p>
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
<h3>edit, sort, filter, layout, header</h3>
|
|
803
|
+
<div class="table-wrap">
|
|
804
|
+
<table>
|
|
805
|
+
<thead><tr><th>Spec</th><th>Keys</th></tr></thead>
|
|
806
|
+
<tbody>
|
|
807
|
+
<tr><td class="name">edit</td><td class="desc"><code>enabled</code> (boolean or predicate), <code>editor</code>, <code>props</code>, <code>popup</code>, <code>validate</code></td></tr>
|
|
808
|
+
<tr><td class="name">sort</td><td class="desc"><code>enabled</code>, <code>direction</code>, <code>order</code>, <code>nullsFirst</code></td></tr>
|
|
809
|
+
<tr><td class="name">filter</td><td class="desc"><code>enabled</code>, <code>type</code>, <code>props</code></td></tr>
|
|
810
|
+
<tr><td class="name">layout</td><td class="desc"><code>width</code>, <code>min</code>, <code>max</code>, <code>flex</code>, <code>pin</code>, <code>hidden</code>, <code>resizable</code>, <code>movable</code>, <code>lockVisible</code>, <code>lockPosition</code>. A <code>pin</code> of <code>'start'</code> or <code>'end'</code> holds the viewport edge while there is something to scroll; where the columns do not fill the grid, spare width falls beyond the last column rather than in front of it.</td></tr>
|
|
811
|
+
<tr><td class="name">header</td><td class="desc"><code>template</code>, <code>render</code>, <code>props</code>, <code>class</code>, <code>tooltip</code>, <code>align</code></td></tr>
|
|
812
|
+
</tbody>
|
|
813
|
+
</table>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<h2 id="grid-methods">Grid methods</h2>
|
|
817
|
+
<p class="section-note">Top-level members. Everything else hangs off a namespace.</p>
|
|
818
|
+
|
|
819
|
+
<div class="table-wrap">
|
|
820
|
+
<table>
|
|
821
|
+
<thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
|
|
822
|
+
<tbody>
|
|
823
|
+
<tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.5.4'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
|
|
824
|
+
<tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
|
|
825
|
+
<tr><td class="sig">set(key, value)</td><td class="type">void</td><td class="desc">Write one key. Every key is live; nothing needs a rebuild.</td></tr>
|
|
826
|
+
<tr><td class="sig">setAll(values)</td><td class="type">void</td><td class="desc">Write several in one pass. Emits one <code>config:changed</code> for the batch, not one per key.</td></tr>
|
|
827
|
+
<tr><td class="sig">config()</td><td class="type">GridConfig</td><td class="desc">The whole live configuration as a shallow copy. Pairs with <code>setAll</code> for a read–modify–write round trip. Nested objects are shared by reference, so treat it as read-only.</td></tr>
|
|
828
|
+
<tr><td class="sig">on(event, handler)</td><td class="type">() => void</td><td class="desc">Returns its own unsubscribe. <code>'*'</code> subscribes to everything.</td></tr>
|
|
829
|
+
<tr><td class="sig">once(event, handler)</td><td class="type">() => void</td><td class="desc"></td></tr>
|
|
830
|
+
<tr><td class="sig">off(event, handler)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
831
|
+
<tr><td class="sig">emit(event, payload)</td><td class="type">void</td><td class="desc">Emit on the grid's bus, for custom components.</td></tr>
|
|
832
|
+
<tr><td class="sig">attachRenderer(renderer)</td><td class="type">void</td><td class="desc">Bind a renderer to a headless grid.</td></tr>
|
|
833
|
+
<tr><td class="sig">destroy()</td><td class="type">void</td><td class="desc">Release listeners, workers and pooled buffers.</td></tr>
|
|
834
|
+
<tr><td class="name">element</td><td class="type">HTMLElement | null</td><td class="desc">The rendered root; null when headless.</td></tr>
|
|
835
|
+
<tr><td class="name">ready</td><td class="type">boolean</td><td class="desc"></td></tr>
|
|
836
|
+
<tr><td class="name">destroyed</td><td class="type">boolean</td><td class="desc"></td></tr>
|
|
837
|
+
</tbody>
|
|
838
|
+
</table>
|
|
839
|
+
</div>
|
|
840
|
+
|
|
841
|
+
<h2 id="rows">grid.rows</h2>
|
|
842
|
+
<p>Data usually arrives after the grid does. Build it with <code>rows: []</code>, then load
|
|
843
|
+
when your fetch resolves — the sort, filters, grouping and column layout you set up in the
|
|
844
|
+
meantime all survive, and apply to the new data.</p>
|
|
845
|
+
<pre><code><span class="kw">const</span> grid = createGrid(el, { columns, rowKey: 'id', rows: [] });
|
|
846
|
+
grid.overlay.show('loading');
|
|
847
|
+
|
|
848
|
+
<span class="kw">const</span> data = <span class="kw">await</span> fetch('/api/circuits').then(r => r.json());
|
|
849
|
+
grid.rows.load(data); <span class="cmt">// replaces whatever was there</span>
|
|
850
|
+
grid.overlay.hide();</code></pre>
|
|
851
|
+
<div class="table-wrap">
|
|
852
|
+
<table>
|
|
853
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
854
|
+
<tbody>
|
|
855
|
+
<tr><td class="sig">load(rows)</td><td class="type">void</td><td class="desc"><strong>Replaces</strong> the data. The view — sort, filters, grouping, column layout — is kept. Same as <code>grid.set('rows', data)</code>.</td></tr>
|
|
856
|
+
<tr><td class="sig">get(index)</td><td class="type">Row</td><td class="desc">By display index, after filtering, grouping and flattening.</td></tr>
|
|
857
|
+
<tr><td class="sig">byKey(key)</td><td class="type">Row</td><td class="desc">By row key, whether or not it is on screen.</td></tr>
|
|
858
|
+
<tr><td class="sig">matchCount()</td><td class="type">number</td><td class="desc">Data rows passing the filters, across every page. Excludes group headers, footers and totals — the numerator of "1,204 of 100,000".</td></tr>
|
|
859
|
+
<tr><td class="sig">count()</td><td class="type">number</td><td class="desc">Display rows — group rows included, collapsed children excluded.</td></tr>
|
|
860
|
+
<tr><td class="sig">totalCount()</td><td class="type">number</td><td class="desc">Source rows before filtering. The denominator of "1,204 of 100,000".</td></tr>
|
|
861
|
+
<tr><td class="sig">value(key, colId)</td><td class="type">unknown</td><td class="desc">The stored value.</td></tr>
|
|
862
|
+
<tr><td class="sig">text(key, colId)</td><td class="type">string</td><td class="desc">The formatted display text.</td></tr>
|
|
863
|
+
<tr><td class="sig">values(key)</td><td class="type">object</td><td class="desc">Every column's value for one row.</td></tr>
|
|
864
|
+
<tr><td class="sig">data()</td><td class="type">unknown[]</td><td class="desc">The caller's original row objects, in source order.</td></tr>
|
|
865
|
+
<tr><td class="sig">forEach(fn)</td><td class="type">void</td><td class="desc">Walks display rows without materialising them all.</td></tr>
|
|
866
|
+
<tr><td class="sig">forEachAll(fn)</td><td class="type">void</td><td class="desc">Walks every row in the data, before any filter — leaf rows only, in the order they arrived. What you want for a total, an export or a reconciliation, where <code>forEach</code> would give you the view instead. A remote or paged source holds only what it has fetched and says so.</td></tr>
|
|
867
|
+
<tr><td class="sig">apply(change)</td><td class="type">object</td><td class="desc">Transactional add / update / remove. Needs <code>rowKey</code>.</td></tr>
|
|
868
|
+
<tr><td class="sig">queue(change)</td><td class="type">void</td><td class="desc">Batches a change into the next frame — the high-frequency path.</td></tr>
|
|
869
|
+
<tr><td class="sig">refresh(opts)</td><td class="type">void</td><td class="desc">Force re-evaluation of computed values and cells.</td></tr>
|
|
870
|
+
<tr><td class="sig">expand(key, deep?)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
871
|
+
<tr><td class="sig">collapse(key)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
872
|
+
<tr><td class="sig">expandAll() / collapseAll()</td><td class="type">void</td><td class="desc"></td></tr>
|
|
873
|
+
</tbody>
|
|
874
|
+
</table>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<h2 id="columns">grid.columns</h2>
|
|
878
|
+
<div class="table-wrap">
|
|
879
|
+
<table>
|
|
880
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
881
|
+
<tbody>
|
|
882
|
+
<tr><td class="sig">all()</td><td class="type">ResolvedColumn[]</td><td class="desc">Every column, hidden included.</td></tr>
|
|
883
|
+
<tr><td class="sig">visible()</td><td class="type">ResolvedColumn[]</td><td class="desc">In render order, including generated group and pivot columns.</td></tr>
|
|
884
|
+
<tr><td class="sig">get(id)</td><td class="type">ResolvedColumn</td><td class="desc"></td></tr>
|
|
885
|
+
<tr><td class="sig">show(ids) / hide(ids)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
886
|
+
<tr><td class="sig">move(id, to)</td><td class="type">void</td><td class="desc">Index into the full column order.</td></tr>
|
|
887
|
+
<tr><td class="sig">pin(id, side)</td><td class="type">void</td><td class="desc"><code>'start'</code>, <code>'end'</code> or <code>null</code>.</td></tr>
|
|
888
|
+
<tr><td class="sig">resize(id, px)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
889
|
+
<tr><td class="sig">autoSize(ids)</td><td class="type">void</td><td class="desc">Fit each column to its rendered content.</td></tr>
|
|
890
|
+
<tr><td class="sig">fit()</td><td class="type">void</td><td class="desc">Distribute all columns across the viewport.</td></tr>
|
|
891
|
+
<tr><td class="sig">group(ids)</td><td class="type">void</td><td class="desc">Set the row-group columns, in order.</td></tr>
|
|
892
|
+
<tr><td class="sig">pivot(ids)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
893
|
+
<tr><td class="sig">totals(ids)</td><td class="type">void</td><td class="desc">Which columns carry an aggregation.</td></tr>
|
|
894
|
+
<tr><td class="sig">setTotal(id, fn)</td><td class="type">void</td><td class="desc">Change one column's aggregation. <code>null</code> stops totalling it.</td></tr>
|
|
895
|
+
<tr><td class="sig">distinct(id)</td><td class="type">unknown[]</td><td class="desc">Distinct values, read from the dictionary rather than by scanning rows.</td></tr>
|
|
896
|
+
<tr><td class="sig">state()</td><td class="type">ColumnState[]</td><td class="desc">Serialisable column state.</td></tr>
|
|
897
|
+
<tr><td class="sig">apply(state)</td><td class="type">void</td><td class="desc">Restore it.</td></tr>
|
|
898
|
+
</tbody>
|
|
899
|
+
</table>
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<h2 id="selection">grid.selection</h2>
|
|
903
|
+
<div class="table-wrap">
|
|
904
|
+
<table>
|
|
905
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
906
|
+
<tbody>
|
|
907
|
+
<tr><td class="sig">keys()</td><td class="type">string[]</td><td class="desc">Selected row keys.</td></tr>
|
|
908
|
+
<tr><td class="sig">rows()</td><td class="type">Row[]</td><td class="desc"></td></tr>
|
|
909
|
+
<tr><td class="sig">all()</td><td class="type">Row[]</td><td class="desc">Including rows selected but currently filtered out.</td></tr>
|
|
910
|
+
<tr><td class="sig">set(keys)</td><td class="type">void</td><td class="desc">Replace the selection.</td></tr>
|
|
911
|
+
<tr><td class="sig">clear()</td><td class="type">void</td><td class="desc"></td></tr>
|
|
912
|
+
<tr><td class="sig">ranges()</td><td class="type">Range[]</td><td class="desc">Cell ranges, for spreadsheet-style selection.</td></tr>
|
|
913
|
+
<tr><td class="sig">setRange(range)</td><td class="type">void</td><td class="desc">Replace every range with one.</td></tr>
|
|
914
|
+
<tr><td class="sig">addRange(range)</td><td class="type">void</td><td class="desc">Add a range without discarding the others — the API form of ctrl-click. Becomes the anchor <code>extendRange</code> grows.</td></tr>
|
|
915
|
+
<tr><td class="sig">startRange(rowIndex, colId, opts)</td><td class="type">void</td><td class="desc">Begin a range at a cell. <code>opts.additive</code> keeps the existing ranges.</td></tr>
|
|
916
|
+
<tr><td class="sig">extendRange(rowIndex, colId)</td><td class="type">void</td><td class="desc">Extend the newest range, keeping its anchor.</td></tr>
|
|
917
|
+
<tr><td class="sig">corner()</td><td class="type">{ row, colId } | null</td><td class="desc">Bottom-right cell of the newest range, where the fill handle sits.</td></tr>
|
|
918
|
+
<tr><td class="sig">inRange(rowIndex, colId)</td><td class="type">boolean</td><td class="desc">Is a cell inside any selected range?</td></tr>
|
|
919
|
+
<tr><td class="sig">cells()</td><td class="type">{ key, colId }[]</td><td class="desc">Every cell in the selected ranges.</td></tr>
|
|
920
|
+
<tr><td class="sig">summary()</td><td class="type">object</td><td class="desc">count, sum, min, max, avg over the range.</td></tr>
|
|
921
|
+
</tbody>
|
|
922
|
+
</table>
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
<h2 id="filters">grid.filters</h2>
|
|
926
|
+
<div class="table-wrap">
|
|
927
|
+
<table>
|
|
928
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
929
|
+
<tbody>
|
|
930
|
+
<tr><td class="sig">get()</td><td class="type">FilterSet</td><td class="desc">The whole condition tree.</td></tr>
|
|
931
|
+
<tr><td class="sig">set(filters)</td><td class="type">void</td><td class="desc">Replace it. <code>null</code> clears everything.</td></tr>
|
|
932
|
+
<tr><td class="sig">quick(text, opts?)</td><td class="type">void</td><td class="desc">The quick filter, applied across every column.</td></tr>
|
|
933
|
+
<tr><td class="sig">clear()</td><td class="type">void</td><td class="desc"></td></tr>
|
|
934
|
+
</tbody>
|
|
935
|
+
</table>
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
<h2 id="sort">grid.sort</h2>
|
|
939
|
+
<div class="table-wrap">
|
|
940
|
+
<table>
|
|
941
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
942
|
+
<tbody>
|
|
943
|
+
<tr><td class="sig">get()</td><td class="type">SortEntry[]</td><td class="desc"><code>{ col, dir, nullsFirst? }</code>, in priority order.</td></tr>
|
|
944
|
+
<tr><td class="sig">set(entries)</td><td class="type">void</td><td class="desc">Multi-sort by passing several entries.</td></tr>
|
|
945
|
+
<tr><td class="sig">clear()</td><td class="type">void</td><td class="desc"></td></tr>
|
|
946
|
+
</tbody>
|
|
947
|
+
</table>
|
|
948
|
+
</div>
|
|
949
|
+
|
|
950
|
+
<h2 id="edit">grid.edit</h2>
|
|
951
|
+
<div class="table-wrap">
|
|
952
|
+
<table>
|
|
953
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
954
|
+
<tbody>
|
|
955
|
+
<tr><td class="sig">start(key, colId)</td><td class="type">void</td><td class="desc">Open an edit session. The row must be rendered.</td></tr>
|
|
956
|
+
<tr><td class="sig">stop(cancel?, opts?)</td><td class="type">object</td><td class="desc">Commit or discard. Pass <code>{ value, key, colId }</code> to write a value.</td></tr>
|
|
957
|
+
<tr><td class="sig">undo() / redo()</td><td class="type">void</td><td class="desc">Depth from <code>edit.undoDepth</code>.</td></tr>
|
|
958
|
+
<tr><td class="sig">setCells(writes, type?)</td><td class="type">number</td><td class="desc">Write many cells as one undoable step. Returns how many landed.</td></tr>
|
|
959
|
+
<tr><td class="sig">pasteInto(anchor, text, extent?)</td><td class="type">number</td><td class="desc">Paste tab-separated text, using Excel's tiling rules.</td></tr>
|
|
960
|
+
<tr><td class="sig">settle(id, ok, reason?)</td><td class="type">boolean</td><td class="desc">Report the outcome of an optimistic write. Only needed with <code>edit.confirm: 'manual'</code>; the id arrives on <code>cell:pending</code>.</td></tr>
|
|
961
|
+
<tr><td class="sig">pending()</td><td class="type">OpenWrite[]</td><td class="desc">Writes still awaiting an outcome. Empty unless <code>edit.commit</code> is set.</td></tr>
|
|
962
|
+
<tr><td class="sig">status(key, colId)</td><td class="type">'pending' | null</td><td class="desc">Whether a cell has a write in flight.</td></tr>
|
|
963
|
+
</tbody>
|
|
964
|
+
</table>
|
|
965
|
+
</div>
|
|
966
|
+
|
|
967
|
+
<h2 id="pagination">grid.pagination</h2>
|
|
968
|
+
<p class="section-note">
|
|
969
|
+
A window over the rows the query already produced — not another query. A page change
|
|
970
|
+
re-slices; it does not re-filter, re-sort or re-group, so paging a million rows costs
|
|
971
|
+
nothing beyond the repaint.
|
|
972
|
+
</p>
|
|
973
|
+
<div class="table-wrap">
|
|
974
|
+
<table>
|
|
975
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
976
|
+
<tbody>
|
|
977
|
+
<tr><td class="sig">get()</td><td class="type">{ page, pageSize, total, pageCount }</td><td class="desc"><code>total</code> is the filtered row count, so it moves when a filter does.</td></tr>
|
|
978
|
+
<tr><td class="sig">set({ page?, pageSize? })</td><td class="type">void</td><td class="desc">Move, resize, or both. <code>pageSize: 0</code> turns paging off and shows everything. Emits <code>page:changed</code> once the rows have moved.</td></tr>
|
|
979
|
+
</tbody>
|
|
980
|
+
</table>
|
|
981
|
+
</div>
|
|
982
|
+
|
|
983
|
+
<h2 id="scroll">grid.scroll</h2>
|
|
984
|
+
<div class="table-wrap">
|
|
985
|
+
<table>
|
|
986
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
987
|
+
<tbody>
|
|
988
|
+
<tr><td class="sig">position()</td><td class="type">{ top, left }</td><td class="desc"></td></tr>
|
|
989
|
+
<tr><td class="sig">toRow(index, align?)</td><td class="type">void</td><td class="desc"><code>align</code>: <code>'start'</code>, <code>'centre'</code>, <code>'end'</code>.</td></tr>
|
|
990
|
+
<tr><td class="sig">toColumn(id)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
991
|
+
</tbody>
|
|
992
|
+
</table>
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<h2 id="export">grid.export</h2>
|
|
996
|
+
<div class="table-wrap">
|
|
997
|
+
<table>
|
|
998
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
999
|
+
<tbody>
|
|
1000
|
+
<tr><td class="sig">csv(opts)</td><td class="type">string | Blob</td><td class="desc">Fields sanitised against formula injection.</td></tr>
|
|
1001
|
+
<tr><td class="sig">excel(opts)</td><td class="type">Promise<Blob></td><td class="desc">Real <code>.xlsx</code>, written without a ZIP dependency. Large exports stream.</td></tr>
|
|
1002
|
+
<tr><td class="sig">clipboard(opts)</td><td class="type">Promise</td><td class="desc">TSV, with the grid's own paste parser as its counterpart.</td></tr>
|
|
1003
|
+
<tr><td class="sig">print(opts)</td><td class="type">void</td><td class="desc">Switches virtualisation and pinning off for the printed document.</td></tr>
|
|
1004
|
+
<tr><td class="sig">rows(mode)</td><td class="type">Row[]</td><td class="desc"><code>'all'</code>, <code>'visible'</code> or <code>'selected'</code>.</td></tr>
|
|
1005
|
+
</tbody>
|
|
1006
|
+
</table>
|
|
1007
|
+
</div>
|
|
1008
|
+
|
|
1009
|
+
<div class="note">
|
|
1010
|
+
<p><strong>Excel value conversion.</strong> A data type may declare <code>toExcelValue</code> and <code>excelKind</code>, because a spreadsheet's number formats are not free-form. A time of day is written as a fraction of a day under <code>hh:mm:ss</code>; a duration as days under <code>[h]:mm:ss</code>, where the brackets are what stop Excel wrapping at 24 hours. Both stay numeric, so they still sort and subtract in the sheet.</p>
|
|
1011
|
+
<p>IP columns are stored as packed integers so they sort as addresses, and declare <code>excelKind: 'string'</code> so the dotted form is what reaches the file. Radix columns export decimal — OOXML cannot express base 16, and staying numeric was judged worth more than display fidelity.</p>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<h2 id="state">grid.state</h2>
|
|
1015
|
+
<div class="table-wrap">
|
|
1016
|
+
<table>
|
|
1017
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1018
|
+
<tbody>
|
|
1019
|
+
<tr><td class="sig">get()</td><td class="type">GridState</td><td class="desc">Versioned and serialisable: <code>columns</code>, <code>columnOrder</code>, <code>filters</code>, <code>quick</code>, <code>sort</code>, <code>group</code>, <code>pivot</code>, <code>expanded</code>, <code>selection</code>, <code>scroll</code>, <code>pagination</code>.</td></tr>
|
|
1020
|
+
<tr><td class="sig">apply(state, opts?)</td><td class="type">object</td><td class="desc">Restore a view. Returns a report of anything it could not apply — a column that no longer exists, for instance — rather than failing silently.</td></tr>
|
|
1021
|
+
<tr><td class="sig">baseline()</td><td class="type">GridState | null</td><td class="desc">The state the grid started in, captured once <em>after</em> <code>config.state</code> and any default view — so the baseline is the grid you shipped, not the one before your own configuration ran.</td></tr>
|
|
1022
|
+
<tr><td class="sig">reset()</td><td class="type">object | null</td><td class="desc">Put the grid back to that baseline, as <strong>one</strong> undo entry. Clears anything the baseline does not mention, including the quick filter.</td></tr>
|
|
1023
|
+
<tr><td class="sig">modified()</td><td class="type">boolean</td><td class="desc">Whether anything has changed since construction. Lets a "restore" control disable itself rather than offering an action that would do nothing.</td></tr>
|
|
1024
|
+
</tbody>
|
|
1025
|
+
</table>
|
|
1026
|
+
</div>
|
|
1027
|
+
|
|
1028
|
+
<h2 id="history">grid.history</h2>
|
|
1029
|
+
<p>Undo across the whole grid, not only edits. Sorts, filters, column moves, grouping, an applied view and a restore all record an entry, and each carries a label written for a button — <code>"sort by Region"</code>, not <code>"sort"</code>.</p>
|
|
1030
|
+
<div class="table-wrap">
|
|
1031
|
+
<table>
|
|
1032
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1033
|
+
<tbody>
|
|
1034
|
+
<tr><td class="sig">undo()</td><td class="type">object | null</td><td class="desc">The entry that was undone.</td></tr>
|
|
1035
|
+
<tr><td class="sig">redo()</td><td class="type">object | null</td><td class="desc"></td></tr>
|
|
1036
|
+
<tr><td class="sig">canUndo() / canRedo()</td><td class="type">boolean</td><td class="desc"></td></tr>
|
|
1037
|
+
<tr><td class="sig">peek(direction?)</td><td class="type">object | null</td><td class="desc">What the next undo or redo <em>would</em> do, so a control can name it before it is pressed.</td></tr>
|
|
1038
|
+
<tr><td class="sig">list()</td><td class="type">object[]</td><td class="desc">The timeline, newest first.</td></tr>
|
|
1039
|
+
<tr><td class="sig">transaction(label, fn)</td><td class="type">object | null</td><td class="desc">Group several changes into one entry. Nested transactions join the outer one.</td></tr>
|
|
1040
|
+
<tr><td class="sig">clear()</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1041
|
+
</tbody>
|
|
1042
|
+
</table>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div class="note"><p>A multi-cell paste is one entry, not one per cell. An AI plan is one entry however many actions it contains, labelled with what it did.</p></div>
|
|
1045
|
+
|
|
1046
|
+
<h2 id="diagnostics">grid.diagnostics</h2>
|
|
1047
|
+
<p>What the grid is doing, as data: render counts and their causes, memory layout, operation and provider timing, event listener counts, effective configuration, and a list of things that look like mistakes.</p>
|
|
1048
|
+
<p><strong>This is the API; the devtools panel is a consumer of it.</strong> Built in that order deliberately — instrumentation shaped by a UI tends to report what is convenient to display rather than what is true, and an API that only exists behind a panel cannot be asserted against in a test.</p>
|
|
1049
|
+
<pre><code><span class="cmt">// the assertion this exists to make possible</span>
|
|
1050
|
+
const before = grid.diagnostics.renders().dom.cellWrites;
|
|
1051
|
+
grid.filters.set({ col: 'status', op: 'eq', value: 'active' });
|
|
1052
|
+
await nextFrame();
|
|
1053
|
+
const written = grid.diagnostics.renders().dom.cellWrites - before;
|
|
1054
|
+
expect(written).toBeLessThan(200);</code></pre>
|
|
1055
|
+
<div class="table-wrap">
|
|
1056
|
+
<table>
|
|
1057
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1058
|
+
<tbody>
|
|
1059
|
+
<tr><td class="sig">snapshot()</td><td class="type">object</td><td class="desc">Everything, in one structure.</td></tr>
|
|
1060
|
+
<tr><td class="sig">renders()</td><td class="type">object</td><td class="desc">Counts by cause, the last render's phase timings, DOM write counters and viewport state.</td></tr>
|
|
1061
|
+
<tr><td class="sig">store()</td><td class="type">object</td><td class="desc">Per-column backing kind and byte footprint, total bytes, rows against physical slots, tombstoned rows.</td></tr>
|
|
1062
|
+
<tr><td class="sig">operations()</td><td class="type">object</td><td class="desc">Count, mean and worst per operation kind, with a bounded sample of recent calls.</td></tr>
|
|
1063
|
+
<tr><td class="sig">providers()</td><td class="type">object</td><td class="desc">Calls, errors, in-flight count and latency per provider, with failures retained.</td></tr>
|
|
1064
|
+
<tr><td class="sig">events()</td><td class="type">object</td><td class="desc">Listener count per event type.</td></tr>
|
|
1065
|
+
<tr><td class="sig">config()</td><td class="type">object</td><td class="desc"><code>{ effective, supplied, defaulted }</code> — which values you chose and which the grid chose.</td></tr>
|
|
1066
|
+
<tr><td class="sig">warnings()</td><td class="type">object[]</td><td class="desc">Everything flagged, newest first, each with a stable <code>id</code>.</td></tr>
|
|
1067
|
+
<tr><td class="sig">dismiss(id)</td><td class="type">void</td><td class="desc">Hide a warning for this session. Not permanently.</td></tr>
|
|
1068
|
+
<tr><td class="sig">bundle()</td><td class="type">object</td><td class="desc">A support bundle. <strong>Contains no row data.</strong></td></tr>
|
|
1069
|
+
<tr><td class="sig">checkOptions(options)</td><td class="type">boolean</td><td class="desc">True when an options object changed identity without its contents changing.</td></tr>
|
|
1070
|
+
<tr><td class="sig">record(kind, detail)</td><td class="type">void</td><td class="desc">Record your own operation, so custom work appears alongside the grid's.</td></tr>
|
|
1071
|
+
<tr><td class="sig">reset()</td><td class="type">void</td><td class="desc">Zero the counters. Warnings and configuration are left alone.</td></tr>
|
|
1072
|
+
</tbody>
|
|
1073
|
+
</table>
|
|
1074
|
+
</div>
|
|
1075
|
+
<h3>The support bundle</h3>
|
|
1076
|
+
<p><code>bundle()</code> returns configuration, query state, timing history, warnings, provider statistics, version and environment. It <strong>never contains row data, cell values or column values</strong>, and says so in its own <code>contains</code> field. The guarantee is the point: a bundle that had to be read for confidential content before sending is a bundle nobody sends.</p>
|
|
1077
|
+
<h3>Warnings</h3>
|
|
1078
|
+
<p>Each carries a stable <code>id</code> a support conversation can name, a plain description, and the specific values involved. Two sources are merged: checks run against the grid, and everything the grid has reported through its own one-per-cause warnings.</p>
|
|
1079
|
+
<div class="table-wrap">
|
|
1080
|
+
<table>
|
|
1081
|
+
<thead><tr><th>Id</th><th>What it means</th></tr></thead>
|
|
1082
|
+
<tbody>
|
|
1083
|
+
<tr><td class="sig">options-identity-churn</td><td class="desc">An options object rebuilt on every parent render. A wrapper comparing by identity will tear the grid down each time.</td></tr>
|
|
1084
|
+
<tr><td class="sig">duplicate-row-keys</td><td class="desc">Two rows share a key. Presents as "the wrong row updated", never as an error.</td></tr>
|
|
1085
|
+
<tr><td class="sig">query-references-unknown-column</td><td class="desc">A filter or sort names a column that does not exist. Silently matches nothing.</td></tr>
|
|
1086
|
+
<tr><td class="sig">listener-count-growing</td><td class="desc">Probable subscription leak in the host. Presents as gradual slowdown.</td></tr>
|
|
1087
|
+
<tr><td class="sig">main-thread-eligible-for-worker</td><td class="desc">A large operation ran on the main thread despite the worker threshold.</td></tr>
|
|
1088
|
+
<tr><td class="sig">slow-provider</td><td class="desc">A provider took more than a second to answer.</td></tr>
|
|
1089
|
+
</tbody>
|
|
1090
|
+
</table>
|
|
1091
|
+
</div>
|
|
1092
|
+
<h3>The devtools panel</h3>
|
|
1093
|
+
<p>An optional module. It imports nothing — the grid is handed to it — so deployments that never load it pay nothing.</p>
|
|
1094
|
+
<pre><code>import { createGrid } from '@toclocoinc/lattice-grid';
|
|
1095
|
+
import { createDevtools } from '@toclocoinc/lattice-grid/modules/devtools';
|
|
1096
|
+
|
|
1097
|
+
const grid = createGrid(el, config);
|
|
1098
|
+
createDevtools({ grid }); <span class="cmt">// Ctrl+Shift+D collapses it</span></code></pre>
|
|
1099
|
+
<p>Nine tabs over the API above, a compact vitals strip to leave open while working, and a render heat overlay that tints cells as they are written — blue for a new value, red for a cell rewritten with the value it already held. The second colour is the one worth chasing: it is work the grid did not need to do, and no counter alone will tell you where it is.</p>
|
|
1100
|
+
<div class="note"><p>The panel observes and never mutates. A configuration editor would create a second path into state that has to be kept correct forever, so there is not one. Nothing leaves the browser: there is no telemetry, and the bundle is produced only when you ask for it.</p></div>
|
|
1101
|
+
|
|
1102
|
+
<h2 id="presence">grid.presence</h2>
|
|
1103
|
+
<p>Who else is on this grid and what they are doing: cursor, selection, active edit, and an optional advisory lock. It prevents the two failure modes of multi-user data work — two people editing the same cell unaware of each other, and one person unable to tell whether anyone else is there at all.</p>
|
|
1104
|
+
<p><strong>The grid never opens a connection.</strong> You supply the transport and the identity; the grid renders what arrives and publishes what changes. A WebSocket, MQTT, a CRDT library or a polling endpoint all satisfy the interface. Without a provider the feature is inert.</p>
|
|
1105
|
+
<pre><code>presence: {
|
|
1106
|
+
provider, <span class="cmt">// subscribe + publish</span>
|
|
1107
|
+
me: { id: 'u_17', name: 'Tony' },
|
|
1108
|
+
throttleMs: 60,
|
|
1109
|
+
lock: true <span class="cmt">// advisory; see below</span>
|
|
1110
|
+
}</code></pre>
|
|
1111
|
+
<div class="table-wrap">
|
|
1112
|
+
<table>
|
|
1113
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1114
|
+
<tbody>
|
|
1115
|
+
<tr><td class="sig">enabled</td><td class="type">boolean</td><td class="desc">False without a provider.</td></tr>
|
|
1116
|
+
<tr><td class="sig">peers()</td><td class="type">Peer[]</td><td class="desc">Everyone else, most recently active first, each with <code>idle</code>, <code>hidden</code> and <code>cursorFresh</code>.</td></tr>
|
|
1117
|
+
<tr><td class="sig">hiddenCount()</td><td class="type">number</td><td class="desc">Peers none of whose positions are in this view.</td></tr>
|
|
1118
|
+
<tr><td class="sig">editorOf(rowId, colId)</td><td class="type">Peer | null</td><td class="desc">Who is editing a cell, if the claim is fresh.</td></tr>
|
|
1119
|
+
<tr><td class="sig">lockedBy(rowId, colId)</td><td class="type">Peer | null</td><td class="desc">Null unless <code>lock</code> is on. <strong>Advisory.</strong></td></tr>
|
|
1120
|
+
<tr><td class="sig">jumpTo(peerId)</td><td class="type">boolean</td><td class="desc">Scroll to a peer's cursor. False when their row is not in this view.</td></tr>
|
|
1121
|
+
<tr><td class="sig">publish()</td><td class="type">void</td><td class="desc">Publish now. The grid already does this on cursor, selection and edit changes.</td></tr>
|
|
1122
|
+
<tr><td class="sig">setPublishing(on)</td><td class="type">void</td><td class="desc">Receive without appearing — for observer and supervisor roles.</td></tr>
|
|
1123
|
+
<tr><td class="sig">setPaused(paused)</td><td class="type">void</td><td class="desc">Suspend publishing. Done for you while the tab is hidden.</td></tr>
|
|
1124
|
+
<tr><td class="sig">connect(provider)</td><td class="type">void</td><td class="desc">Attach or detach after construction.</td></tr>
|
|
1125
|
+
<tr><td class="sig">stats()</td><td class="type">object</td><td class="desc">Published, received, throttle drops, provider errors, peer count.</td></tr>
|
|
1126
|
+
</tbody>
|
|
1127
|
+
</table>
|
|
1128
|
+
</div>
|
|
1129
|
+
<h3>The provider</h3>
|
|
1130
|
+
<pre><code>{
|
|
1131
|
+
subscribe(onMessage) { <span class="cmt">/* call onMessage(peer) or onMessage(peer[]) */</span> return unsubscribe; },
|
|
1132
|
+
publish(state) { <span class="cmt">/* send it however you like */</span> }
|
|
1133
|
+
}</code></pre>
|
|
1134
|
+
<p>A message is one peer state or an array of them, so a transport that sends a full roster on connect and deltas afterwards needs no unwrapping. <code>{ id, left: true }</code> removes a peer.</p>
|
|
1135
|
+
<div class="note"><p><strong>Presence carries intent, never values.</strong> A peer's committed edit must reach the grid as <em>data</em>, through whatever channel you already use — as a transaction, so it gets the flash-on-change treatment. Presence is throttled, lossy and ephemeral by design, so a value carried on it is a value that can be dropped. That is the kind of bug that surfaces once a month in production and cannot be reproduced.</p></div>
|
|
1136
|
+
<h3>Positions travel as row keys</h3>
|
|
1137
|
+
<p>Peers sort and filter independently, so a row index addresses a different record on every screen. Presence is positional in <em>data</em> terms: a peer's cursor renders wherever that row currently sits in your view, and is held but not drawn when the row is filtered out, on another page, or evicted from a bounded window. Those peers are counted by <code>hiddenCount()</code> and shown in the roster as “not in view”, so their absence does not read as a disconnection.</p>
|
|
1138
|
+
<h3>Idle and removal</h3>
|
|
1139
|
+
<p>Derived from <em>local receipt time</em>, never the timestamp in the payload. Clocks between clients disagree by seconds routinely, so a peer with a fast clock would look permanently fresh and one with a slow clock permanently idle. Silence past <code>idleMs</code> desaturates them; past <code>removeMs</code> they go. An explicit <code>left</code> signal is used when your transport provides one.</p>
|
|
1140
|
+
<h3>Locking is advisory</h3>
|
|
1141
|
+
<div class="note"><p><strong>Locking reduces collisions. It does not eliminate them.</strong> Presence is throttled and can arrive out of order, so two clients can enter an edit at the same moment. The authoritative resolution is the <strong>conditional write in <code>edit.commit</code></strong>, which returns a conflict and rolls the optimistic edit back. If you treat locking as a guarantee and skip that write, you will lose data.</p></div>
|
|
1142
|
+
<p>With <code>lock: true</code>, starting an edit on a cell a peer holds returns <code>false</code> from <code>edit.start</code>, emits <code>presence:lockRefused</code>, and announces the holder through a live region — a cell that silently refuses to enter edit mode is indistinguishable from a broken grid.</p>
|
|
1143
|
+
<h3>What is drawn</h3>
|
|
1144
|
+
<p>A peer's cursor is a <strong>dashed</strong> border in their colour; your own focus ring is solid, and the difference is in the kind of line rather than only the hue so the two can never be confused. Their name shows for a moment after their cursor moves and on hover, then fades to the bare border. A selection is a low-opacity tint, with the most recent peer winning a contested cell outright rather than blending. An active edit is solid and tinted — the loudest treatment, because it is the state that matters most.</p>
|
|
1145
|
+
<p>Nothing is inserted into the grid: every treatment is written onto cells that already exist, so presence cannot shift layout, cover an in-cell chart, or intercept a click. The roster is the exception, because it is a control.</p>
|
|
1146
|
+
<p>Colours are assigned by hashing the peer id against <code>--lattice-peer-1</code> … <code>--lattice-peer-8</code>, so one person is the same colour on every screen and across reloads.</p>
|
|
1147
|
+
|
|
1148
|
+
<h2 id="comments">grid.comments</h2>
|
|
1149
|
+
<p>Threaded comments attached to individual cells, for collaborative data review — flagging an anomaly, asking why a figure changed, recording the reason behind a manual correction. A commented cell carries a small triangle in its upper-right corner; clicking the corner opens the thread.</p>
|
|
1150
|
+
<p><strong>The grid owns presentation and interaction only.</strong> Storage, identity and permissions are yours. Comment data lives wherever you put it and is reached through a provider.</p>
|
|
1151
|
+
<div class="note"><p><strong>A stable <code>rowKey</code> is required.</strong> Comments are keyed on row identity plus field, never row index, and they outlive the values they annotate. Configure the grid without a <code>rowKey</code> and comments are disabled — named in the same console warning as the other identity-dependent features — rather than silently filing threads against positions that move on the next sort.</p>
|
|
1152
|
+
<p>Identity must be stable <em>across sessions and across data reloads</em>, not merely within one session. A key derived from load order is not enough: reload the data in a different order and every comment reattaches to the wrong row.</p></div>
|
|
1153
|
+
<pre><code>comments: {
|
|
1154
|
+
provider, <span class="cmt">// required; without it the feature is inert</span>
|
|
1155
|
+
mode: 'anchored', <span class="cmt">// or 'docked' for a side panel</span>
|
|
1156
|
+
markdown: false, <span class="cmt">// restricted: emphasis, code, links</span>
|
|
1157
|
+
rowLabel: (row) => row.data.name <span class="cmt">// so the panel says what is being discussed</span>
|
|
1158
|
+
}</code></pre>
|
|
1159
|
+
<div class="table-wrap">
|
|
1160
|
+
<table>
|
|
1161
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1162
|
+
<tbody>
|
|
1163
|
+
<tr><td class="sig">enabled</td><td class="type">boolean</td><td class="desc">False without a provider or without stable row identity.</td></tr>
|
|
1164
|
+
<tr><td class="sig">unavailable()</td><td class="type">string | null</td><td class="desc"><code>'no-provider'</code>, <code>'no-row-identity'</code>, or null.</td></tr>
|
|
1165
|
+
<tr><td class="sig">at(rowId, colId)</td><td class="type">object | null</td><td class="desc"><code>{ count, unresolved, updated }</code> for one cell. Counts only — this is read on every repaint.</td></tr>
|
|
1166
|
+
<tr><td class="sig">open(rowId, colId)</td><td class="type">Promise</td><td class="desc">Open a thread and load its bodies.</td></tr>
|
|
1167
|
+
<tr><td class="sig">close(opts?)</td><td class="type">void</td><td class="desc">Close and discard the bodies.</td></tr>
|
|
1168
|
+
<tr><td class="sig">add(body, opts?)</td><td class="type">Promise</td><td class="desc">Add to the open thread. <code>opts.parentId</code> replies within it.</td></tr>
|
|
1169
|
+
<tr><td class="sig">edit(commentId, body)</td><td class="type">Promise</td><td class="desc"></td></tr>
|
|
1170
|
+
<tr><td class="sig">remove(commentId)</td><td class="type">Promise</td><td class="desc"></td></tr>
|
|
1171
|
+
<tr><td class="sig">resolve() / unresolve()</td><td class="type">Promise</td><td class="desc">Mark the open thread.</td></tr>
|
|
1172
|
+
<tr><td class="sig">request(rowIds, fields?)</td><td class="type">void</td><td class="desc">Ask for index entries. Debounced; the viewport does this for you.</td></tr>
|
|
1173
|
+
<tr><td class="sig">refresh()</td><td class="type">void</td><td class="desc">Reload the index for known rows, after your application learns of a change elsewhere.</td></tr>
|
|
1174
|
+
<tr><td class="sig">loadAll()</td><td class="type">Promise<boolean></td><td class="desc">Load the index for every row, which the comments-only filter needs first.</td></tr>
|
|
1175
|
+
<tr><td class="sig">complete</td><td class="type">boolean</td><td class="desc">Whether the index covers the whole row set.</td></tr>
|
|
1176
|
+
<tr><td class="sig">hiddenUnresolved()</td><td class="type">number</td><td class="desc">Unresolved threads on rows the current filter hides. Zero when the index is partial.</td></tr>
|
|
1177
|
+
<tr><td class="sig">filterToCommented(opts?)</td><td class="type">boolean</td><td class="desc">Restrict to rows carrying comments. <code>unresolvedOnly</code> narrows further. False when the index is incomplete.</td></tr>
|
|
1178
|
+
<tr><td class="sig">thread / openKey / loading</td><td class="type">—</td><td class="desc">The open thread, its cell key, and whether it is still loading.</td></tr>
|
|
1179
|
+
</tbody>
|
|
1180
|
+
</table>
|
|
1181
|
+
</div>
|
|
1182
|
+
<h3>The provider</h3>
|
|
1183
|
+
<p>Every method returns a promise. A rejection surfaces in the panel without disturbing grid state, and an optimistic write is rolled back.</p>
|
|
1184
|
+
<div class="table-wrap">
|
|
1185
|
+
<table>
|
|
1186
|
+
<thead><tr><th>Method</th><th>Description</th></tr></thead>
|
|
1187
|
+
<tbody>
|
|
1188
|
+
<tr><td class="sig">loadIndex(rowIds, fields)</td><td class="desc">Counts and timestamps for the requested cells. <strong>Never bodies.</strong> Called for the viewport and on scroll, debounced.</td></tr>
|
|
1189
|
+
<tr><td class="sig">loadThread(cellKey)</td><td class="desc">The ordered comments for one cell.</td></tr>
|
|
1190
|
+
<tr><td class="sig">addComment(cellKey, body, parentId, ctx)</td><td class="desc"><code>ctx.value</code> is the cell's value at the time of writing. Store it.</td></tr>
|
|
1191
|
+
<tr><td class="sig">editComment(id, body)</td><td class="desc"></td></tr>
|
|
1192
|
+
<tr><td class="sig">deleteComment(id)</td><td class="desc"></td></tr>
|
|
1193
|
+
<tr><td class="sig">resolveThread(cellKey) / unresolveThread(cellKey)</td><td class="desc"></td></tr>
|
|
1194
|
+
</tbody>
|
|
1195
|
+
</table>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div class="note"><p><strong>The grid performs no authorisation.</strong> A comment may carry <code>can: { edit, delete, resolve }</code> and the grid draws affordances accordingly, but that is a convenience for the user and <em>never</em> a security control. Absent flags mean every affordance is shown. Your provider must reject what it must reject.</p></div>
|
|
1198
|
+
<p>Author information is rendered exactly as the provider supplies it — <code>author: { name, avatarUrl, initials }</code>. The grid does not know who the user is and does not guess.</p>
|
|
1199
|
+
<p><strong>Bodies are text.</strong> The default path never produces markup. With <code>markdown: true</code> the panel handles emphasis, code and links only, builds elements rather than assigning HTML, and refuses any link scheme other than <code>http</code>, <code>https</code> and <code>mailto</code>.</p>
|
|
1200
|
+
<p>Comments follow their row through sorting and grouping. When a commented row is filtered out its comments are not lost and not shown; <code>hiddenUnresolved()</code> reports what is outstanding on hidden rows so their absence does not mislead, and the status bar’s <code>comments</code> panel puts that count on screen whenever it is not zero. Comments remain available while streaming, and a thread whose row is evicted by a bounded window closes with an explanation. Comments do not appear in exports and do not serialise into saved views — a view captures display configuration, not data.</p>
|
|
1201
|
+
<p>Keyboard: <kbd>Alt</kbd>+<kbd>M</kbd> opens the thread on the focused cell. The panel traps focus while open and returns it to the originating cell on close. Cells carrying comments announce the fact, and the unresolved count, through their accessible description.</p>
|
|
1202
|
+
|
|
1203
|
+
<h2 id="facets">grid.facets</h2>
|
|
1204
|
+
<p>A distribution chart in each column heading, which is also a filter control. Clicking a bar filters to that bucket; dragging across bars on an ordered column filters to the range. As filters are applied, the other columns' charts recount, so a dataset can be explored by clicking through headings rather than opening a dialog.</p>
|
|
1205
|
+
<p><strong>Off by default.</strong> The band roughly doubles the header's height, which is a cost no grid should pay without being asked.</p>
|
|
1206
|
+
<pre><code>facets: { enabled: true } <span class="cmt">// grid-wide</span>
|
|
1207
|
+
|
|
1208
|
+
<span class="cmt">// per column, layered over the grid's settings</span>
|
|
1209
|
+
{ field: 'price', type: 'number', facet: { strategy: 'quantile', buckets: 16 } }
|
|
1210
|
+
{ field: 'notes', facet: false } <span class="cmt">// opt one column out</span></code></pre>
|
|
1211
|
+
<div class="table-wrap">
|
|
1212
|
+
<table>
|
|
1213
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1214
|
+
<tbody>
|
|
1215
|
+
<tr><td class="sig">get(colId)</td><td class="type">object | null</td><td class="desc"><code>{ bounds, counts, unfiltered, stale, suppressed }</code>. Schedules the computation if it has not run; redraw on <code>facet:computed</code> rather than awaiting.</td></tr>
|
|
1216
|
+
<tr><td class="sig">suppression(colId)</td><td class="type">string | null</td><td class="desc">Why there is no chart: <code>type</code>, <code>cardinality</code>, <code>rows</code>, <code>streaming</code>, <code>no-provider</code>, <code>disabled</code>. Null when there is one.</td></tr>
|
|
1217
|
+
<tr><td class="sig">config(colId?)</td><td class="type">object</td><td class="desc">The resolved settings, column layered over grid.</td></tr>
|
|
1218
|
+
<tr><td class="sig">select(colId, from, to?, opts?)</td><td class="type">boolean</td><td class="desc">Filter to a bucket, or to the range <code>from</code>–<code>to</code>. <code>opts.additive</code> adds to a categorical set. Selecting what is already selected clears it.</td></tr>
|
|
1219
|
+
<tr><td class="sig">clear(colId)</td><td class="type">boolean</td><td class="desc">Remove only this column's filter, leaving every other filter in place.</td></tr>
|
|
1220
|
+
<tr><td class="sig">selected(colId)</td><td class="type">number[]</td><td class="desc">Which buckets the column's own filter currently covers.</td></tr>
|
|
1221
|
+
<tr><td class="sig">toggle(colId, open?)</td><td class="type">boolean</td><td class="desc">Expand or collapse the chart. Rides in a saved view.</td></tr>
|
|
1222
|
+
<tr><td class="sig">isExpanded(colId)</td><td class="type">boolean</td><td class="desc"></td></tr>
|
|
1223
|
+
<tr><td class="sig">refresh(opts?)</td><td class="type">void</td><td class="desc">Recount every chart. <code>immediate</code> skips the debounce.</td></tr>
|
|
1224
|
+
<tr><td class="sig">expanded()</td><td class="type">string[]</td><td class="desc">Every expanded column.</td></tr>
|
|
1225
|
+
</tbody>
|
|
1226
|
+
</table>
|
|
1227
|
+
</div>
|
|
1228
|
+
<p><strong>A column is never counted against its own filter.</strong> Every other active filter applies; that column's own conditions are pruned out. Without this, clicking a bucket would collapse the chart to that single bar, leaving no way to see what was excluded or to widen the selection.</p>
|
|
1229
|
+
<p><strong>The filters are ordinary filters.</strong> They go through <code>filters.set</code>, so they undo, ride in saved views, and appear in whatever filter UI you already have. A drag emits a <code>between</code> range rather than a set of bucket indices, so it still means something after the data is replaced and the edges move.</p>
|
|
1230
|
+
<div class="table-wrap">
|
|
1231
|
+
<table>
|
|
1232
|
+
<thead><tr><th>Option</th><th>Default</th><th>Description</th></tr></thead>
|
|
1233
|
+
<tbody>
|
|
1234
|
+
<tr><td class="name">enabled</td><td class="dflt">false</td><td class="desc">Grid-wide, or per column.</td></tr>
|
|
1235
|
+
<tr><td class="name">collapsed</td><td class="dflt">true</td><td class="desc">Start as a one-line density strip that opens on click.</td></tr>
|
|
1236
|
+
<tr><td class="name">height</td><td class="dflt">28</td><td class="desc">Band height in pixels.</td></tr>
|
|
1237
|
+
<tr><td class="name">buckets</td><td class="dflt">20</td><td class="desc">Numeric and date columns.</td></tr>
|
|
1238
|
+
<tr><td class="name">strategy</td><td class="dflt">'equal'</td><td class="desc"><code>equal</code>, <code>quantile</code> or <code>log</code>. Equal width looks wrong on skewed data.</td></tr>
|
|
1239
|
+
<tr><td class="name">granularity</td><td class="dflt">auto</td><td class="desc"><code>hour</code> … <code>year</code>. Chosen from the span when omitted.</td></tr>
|
|
1240
|
+
<tr><td class="name">order</td><td class="dflt">'count'</td><td class="desc"><code>count</code> or <code>alpha</code>, for categorical columns.</td></tr>
|
|
1241
|
+
<tr><td class="name">cardinalityLimit</td><td class="dflt">50</td><td class="desc">Distinct values above which a text column has no readable chart.</td></tr>
|
|
1242
|
+
<tr><td class="name">aboveLimit</td><td class="dflt">'suppress'</td><td class="desc"><code>suppress</code>, or <code>topN</code> for a top list with an aggregated remainder.</td></tr>
|
|
1243
|
+
<tr><td class="name">rowCeiling</td><td class="dflt">2000000</td><td class="desc">Rows above which charts are suppressed.</td></tr>
|
|
1244
|
+
<tr><td class="name">debounce</td><td class="dflt">120</td><td class="desc">Milliseconds a filter change waits before charts recount.</td></tr>
|
|
1245
|
+
<tr><td class="name">whilePaused</td><td class="dflt">true</td><td class="desc">Whether a paused stream re-enables charts.</td></tr>
|
|
1246
|
+
<tr><td class="name">provider</td><td class="dflt">—</td><td class="desc">Async bucket counts for a paged or remote source. Without one, charts are suppressed silently.</td></tr>
|
|
1247
|
+
<tr><td class="name">format</td><td class="dflt">—</td><td class="desc"><code>(bucket, count, unfiltered) => string</code> for tooltips and accessible names.</td></tr>
|
|
1248
|
+
</tbody>
|
|
1249
|
+
</table>
|
|
1250
|
+
</div>
|
|
1251
|
+
<p><strong>Live streams suppress charts.</strong> Buckets that move under the pointer are worse than no chart — the control lies about what clicking it will do. Filters already made stay applied, because they are ordinary filters. Pausing the stream brings the charts back; set <code>whilePaused: false</code> if you would rather it did not.</p>
|
|
1252
|
+
<p><strong>Server-side sources need a <code>provider</code>.</strong> It receives the column, the current filter state with that column's own conditions removed, and the bucketing settings, and returns counts. Results are cached against the filter state, but this is still <em>one query per column per filter change</em> — a grid with eight faceted columns will ask eight questions every time a filter moves, and the backend has to be able to absorb that.</p>
|
|
1253
|
+
<p>Charts are keyboard operable: focus enters from the header, arrows move between buckets, <kbd>Enter</kbd> toggles, <kbd>Shift</kbd> with arrows extends a range on ordered columns, <kbd>Escape</kbd> clears. Each bucket carries its range and count as an accessible name, and the chart as a whole carries a one-sentence description of the distribution's shape — which is the part bar-by-bar labels cannot convey.</p>
|
|
1254
|
+
|
|
1255
|
+
<h2 id="updates">grid.updates</h2>
|
|
1256
|
+
<p>Control over an incoming feed: hold it, let it through, and see what the batching is actually saving you. Pausing does not drop anything — held changes keep merging, so a long pause costs one entry per changed row rather than one per update.</p>
|
|
1257
|
+
<pre><code>grid.updates.pause(); <span class="cmt">// hold the feed; it keeps arriving and merging</span>
|
|
1258
|
+
grid.updates.stats(); <span class="cmt">// { pending, queued, coalesced, flushes, ... }</span>
|
|
1259
|
+
grid.updates.flush(); <span class="cmt">// apply what is waiting, stay paused</span>
|
|
1260
|
+
grid.updates.resume(); <span class="cmt">// apply everything and go live again</span></code></pre>
|
|
1261
|
+
<div class="table-wrap">
|
|
1262
|
+
<table>
|
|
1263
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1264
|
+
<tbody>
|
|
1265
|
+
<tr><td class="sig">paused</td><td class="type">boolean</td><td class="desc">True while updates are held.</td></tr>
|
|
1266
|
+
<tr><td class="sig">pause()</td><td class="type">boolean</td><td class="desc">Hold incoming updates. True when this call paused it.</td></tr>
|
|
1267
|
+
<tr><td class="sig">resume()</td><td class="type">object</td><td class="desc">Apply everything held and start applying again. Returns the rows added, updated and removed.</td></tr>
|
|
1268
|
+
<tr><td class="sig">flush()</td><td class="type">object</td><td class="desc">Apply what is waiting <em>without</em> leaving the paused state — a single step.</td></tr>
|
|
1269
|
+
<tr><td class="sig">stats()</td><td class="type">object</td><td class="desc">Counters for the feed and the buffer: what arrived, what will be applied, and the difference.</td></tr>
|
|
1270
|
+
<tr><td class="sig">log(opts?)</td><td class="type">object[]</td><td class="desc">The timestamped changes still held, oldest first. <code>since</code> narrows to a time window.</td></tr>
|
|
1271
|
+
</tbody>
|
|
1272
|
+
</table>
|
|
1273
|
+
</div>
|
|
1274
|
+
<p><code>coalesced</code> is the number a batching strategy is actually bought with: rows that arrived more than once in a window and were written once. A feed where it stays at zero is not being coalesced, whatever the interval says.</p>
|
|
1275
|
+
<p>The log is bounded two ways, because an entry is not a fixed size — one carrying a single changed cell and one carrying a fifty-thousand-row batch both count as one.</p>
|
|
1276
|
+
<div class="table-wrap">
|
|
1277
|
+
<table>
|
|
1278
|
+
<thead><tr><th>Option</th><th>Default</th><th>Description</th></tr></thead>
|
|
1279
|
+
<tbody>
|
|
1280
|
+
<tr><td class="name">updates.logLimit</td><td class="dflt">2000</td><td class="desc">How many changes are kept.</td></tr>
|
|
1281
|
+
<tr><td class="name">updates.logRows</td><td class="dflt">100000</td><td class="desc">How many rows those changes account for between them. A feed delivering large batches reaches this one first.</td></tr>
|
|
1282
|
+
<tr><td class="name">updates.flush</td><td class="dflt">'frame'</td><td class="desc"><code>frame</code> lands on a paint boundary, which is what makes one repaint per batch reliable. <code>microtask</code> at the end of the current task, <code>interval</code> on the coalescing window, <code>manual</code> only when you call <code>flush()</code>.</td></tr>
|
|
1283
|
+
<tr><td class="name">updates.maxQueued</td><td class="dflt">20000</td><td class="desc">Queued rows that force an early flush, whatever the strategy — including <code>manual</code>.</td></tr>
|
|
1284
|
+
<tr><td class="name">updates.budgetMs</td><td class="dflt">10</td><td class="desc">Milliseconds one flush may spend applying before deferring the rest to the next frame.</td></tr>
|
|
1285
|
+
</tbody>
|
|
1286
|
+
</table>
|
|
1287
|
+
</div>
|
|
1288
|
+
<h3>Applying changes</h3>
|
|
1289
|
+
<p><code>rows.apply(change)</code> applies immediately and returns what happened; <code>rows.queue(change)</code> batches into the next flush and returns a promise. Both take the same shape: <code>add</code>, <code>update</code>, <code>remove</code>, and an optional <code>at</code> insert position.</p>
|
|
1290
|
+
<p><strong>An update is a patch, not a replacement.</strong> Fields absent from the update are untouched, so a delta from a websocket or a save response can be applied as it arrives without reading the row back first. Coalescing merges fields too: <code>{price}</code> and <code>{volume}</code> arriving as separate messages inside one window both survive.</p>
|
|
1291
|
+
<pre><code>grid.rows.apply({ update: [{ id: 'R1', price: 42 }] });
|
|
1292
|
+
<span class="cmt">// every other field on R1 is left alone</span></code></pre>
|
|
1293
|
+
<p><strong>Rows that cannot be applied are reported, not thrown.</strong> A batch of a thousand containing three bad rows applies the other 997 and lists the three.</p>
|
|
1294
|
+
<div class="table-wrap">
|
|
1295
|
+
<table>
|
|
1296
|
+
<thead><tr><th>Reason</th><th>Meaning</th></tr></thead>
|
|
1297
|
+
<tbody>
|
|
1298
|
+
<tr><td class="sig">unknown-id</td><td class="desc">An update or remove naming a row that is not in the grid.</td></tr>
|
|
1299
|
+
<tr><td class="sig">duplicate-id</td><td class="desc">An add whose key already exists. Refused rather than admitted — selection, expansion, comments and the key index all resolve one key to one row.</td></tr>
|
|
1300
|
+
</tbody>
|
|
1301
|
+
</table>
|
|
1302
|
+
</div>
|
|
1303
|
+
<pre><code>const result = grid.rows.apply({ update: [ ... ] });
|
|
1304
|
+
result.rejected; <span class="cmt">// [{ operation, id, reason }]</span></code></pre>
|
|
1305
|
+
<div class="note"><p><strong>These are batches, not database transactions.</strong> There is no isolation and no all-or-nothing guarantee: partial application with per-row rejection is the defined behaviour, which is why the API is not called a transaction.</p></div>
|
|
1306
|
+
|
|
1307
|
+
<p><code>stats()</code> reports <code>held</code> against <code>heldLimit</code> — what the log is carrying now, against what it will carry. <code>rows</code> is a lifetime total of everything that ever arrived and says nothing about memory; these two do. Raise <code>logRows</code> for a deeper scrubber on a grid you have measured, and lower it on a feed of very wide rows.</p>
|
|
1308
|
+
|
|
1309
|
+
<h2 id="timeline">grid.timeline</h2>
|
|
1310
|
+
<p>Moves the grid back through recent <em>data</em> changes — what a row held a minute ago, before the number moved. It reads the change log rather than the undo history: history records what <em>you</em> did, and on a live grid the question is what the <em>data</em> did.</p>
|
|
1311
|
+
<p>Nothing is scrubbable until <code>attach()</code>. What a value used to be is not recoverable after the fact, and reading a row per key on every change is real cost on a busy feed, so recording is off until you ask for it and the window fills from that moment.</p>
|
|
1312
|
+
<pre><code>grid.timeline.attach(); <span class="cmt">// start recording; a scrubber appears</span>
|
|
1313
|
+
grid.timeline.seek(5); <span class="cmt">// stand five changes back</span>
|
|
1314
|
+
grid.timeline.step(-1); <span class="cmt">// one further back</span>
|
|
1315
|
+
grid.timeline.at(); <span class="cmt">// the moment being shown</span>
|
|
1316
|
+
grid.timeline.toLive(); <span class="cmt">// return, applying everything stepped over</span>
|
|
1317
|
+
grid.timeline.detach(); <span class="cmt">// stop recording; the scrubber goes</span></code></pre>
|
|
1318
|
+
<div class="table-wrap">
|
|
1319
|
+
<table>
|
|
1320
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1321
|
+
<tbody>
|
|
1322
|
+
<tr><td class="sig">attached</td><td class="type">boolean</td><td class="desc">Whether the scrubber is recording.</td></tr>
|
|
1323
|
+
<tr><td class="sig">live</td><td class="type">boolean</td><td class="desc">True when the grid is showing the present.</td></tr>
|
|
1324
|
+
<tr><td class="sig">position</td><td class="type">number</td><td class="desc">How many steps back the grid is standing. Zero is live.</td></tr>
|
|
1325
|
+
<tr><td class="sig">depth</td><td class="type">number</td><td class="desc">How many steps back it is possible to go.</td></tr>
|
|
1326
|
+
<tr><td class="sig">attach()</td><td class="type">void</td><td class="desc">Start recording what changes replace.</td></tr>
|
|
1327
|
+
<tr><td class="sig">detach()</td><td class="type">void</td><td class="desc">Stop recording and return to the present.</td></tr>
|
|
1328
|
+
<tr><td class="sig">seek(steps)</td><td class="type">number</td><td class="desc">Stand a number of steps back, 0 being live. Clamped, not refused, at both ends.</td></tr>
|
|
1329
|
+
<tr><td class="sig">step(by)</td><td class="type">number</td><td class="desc">Move relatively; negative goes back in time.</td></tr>
|
|
1330
|
+
<tr><td class="sig">toLive()</td><td class="type">number</td><td class="desc">Return to the present, applying everything stepped over.</td></tr>
|
|
1331
|
+
<tr><td class="sig">at()</td><td class="type">number | null</td><td class="desc">The timestamp being shown.</td></tr>
|
|
1332
|
+
<tr><td class="sig">span()</td><td class="type">object | null</td><td class="desc"><code>{ from, to }</code> — the range the scrubber can move over.</td></tr>
|
|
1333
|
+
</tbody>
|
|
1334
|
+
</table>
|
|
1335
|
+
</div>
|
|
1336
|
+
<p>Cells whose value moved during a seek are marked and stay marked until the next seek, in <code>--lattice-timeline-changed</code>. On a wide row the change you are hunting for is easy to scroll past, and a flash you can miss helps nobody. Marking compares rendered column values rather than raw fields, so a computed column that moved because its inputs moved is marked too. Chart columns redraw as you scrub, like any other cell.</p>
|
|
1337
|
+
<div class="note"><p>Value changes reverse; row additions and removals do not — a window containing them scrubs over the value changes and leaves the row set alone. While scrubbed back the grid is not live: changes keep being recorded but are not applied, and returning to the head applies everything missed. The <code>delta</code> renderer is the one cell type to keep off a scrubbed grid, because it samples on a wall-clock timer and reads a seek as a real movement.</p></div>
|
|
1338
|
+
|
|
1339
|
+
<h2 id="presentation">grid.presentation</h2>
|
|
1340
|
+
<p>Enlarges the grid, drops the chrome and steps through saved views — for a screen share or a room. State only at this level: full screen and the pixels are the DOM layer's, so a headless grid can still be put into presentation state and asked about it.</p>
|
|
1341
|
+
<pre><code>grid.presentation.start({ scale: 1.5, views: ['q3', 'q4'] });
|
|
1342
|
+
grid.presentation.step(1); <span class="cmt">// next view</span>
|
|
1343
|
+
grid.presentation.setSpotlight({ colIds: ['revenue'] });
|
|
1344
|
+
grid.presentation.nudge(1); <span class="cmt">// a little larger</span>
|
|
1345
|
+
grid.presentation.stop(); <span class="cmt">// or Esc</span></code></pre>
|
|
1346
|
+
<div class="note"><p><strong><kbd>Esc</kbd> ends the presentation</strong>, not just full screen — leaving one without the other would strand an enlarged, chrome-less grid in the page with no control left to turn it off. An open editor or menu still closes first.</p></div>
|
|
1347
|
+
<div class="table-wrap">
|
|
1348
|
+
<table>
|
|
1349
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1350
|
+
<tbody>
|
|
1351
|
+
<tr><td class="sig">active</td><td class="type">boolean</td><td class="desc">True while a presentation is running.</td></tr>
|
|
1352
|
+
<tr><td class="sig">scale</td><td class="type">number</td><td class="desc">The current enlargement.</td></tr>
|
|
1353
|
+
<tr><td class="sig">start(options?)</td><td class="type">boolean</td><td class="desc">Begin presenting. <code>scale</code>, <code>views</code>, <code>chrome</code> keep-list, <code>interval</code> for auto-advance.</td></tr>
|
|
1354
|
+
<tr><td class="sig">stop()</td><td class="type">boolean</td><td class="desc">Stop and put the grid back as it was.</td></tr>
|
|
1355
|
+
<tr><td class="sig">setScale(value)</td><td class="type">number</td><td class="desc">Set the enlargement, clamped to 0.5–4.</td></tr>
|
|
1356
|
+
<tr><td class="sig">nudge(steps?)</td><td class="type">number</td><td class="desc">Move the enlargement by steps, for the live keyboard adjustment.</td></tr>
|
|
1357
|
+
<tr><td class="sig">options</td><td class="type">object</td><td class="desc">The options the running presentation started with.</td></tr>
|
|
1358
|
+
<tr><td class="sig">views</td><td class="type">string[]</td><td class="desc">The view ids being stepped through.</td></tr>
|
|
1359
|
+
<tr><td class="sig">index</td><td class="type">number</td><td class="desc">Position in the sequence, <code>-1</code> when there is none.</td></tr>
|
|
1360
|
+
<tr><td class="sig">viewId</td><td class="type">string | null</td><td class="desc">The view id currently shown.</td></tr>
|
|
1361
|
+
<tr><td class="sig">step(by?)</td><td class="type">number</td><td class="desc">Step forward or back through the sequence.</td></tr>
|
|
1362
|
+
<tr><td class="sig">goTo(index)</td><td class="type">number</td><td class="desc">Show a numbered position.</td></tr>
|
|
1363
|
+
<tr><td class="sig">spotlight</td><td class="type">object | null</td><td class="desc">What is currently lit.</td></tr>
|
|
1364
|
+
<tr><td class="sig">setSpotlight(target?)</td><td class="type">boolean</td><td class="desc">Light rows, columns or their intersection and let the rest recede. Call with nothing to clear.</td></tr>
|
|
1365
|
+
<tr><td class="sig">reset()</td><td class="type">boolean</td><td class="desc">Put the current view back as it was saved, discarding what the presenter has sorted or filtered since.</td></tr>
|
|
1366
|
+
</tbody>
|
|
1367
|
+
</table>
|
|
1368
|
+
</div>
|
|
1369
|
+
<p>Enlargement is a CSS scale factor multiplied into the same tokens <a href="#config">density</a> uses, so text, rows, padding and controls grow together rather than the grid being zoomed as an image. Font size is damped against it — type that scaled linearly with a 2× row height reads as shouting.</p>
|
|
1370
|
+
|
|
1371
|
+
<h2 id="redaction">grid.redaction</h2>
|
|
1372
|
+
<p>Obscures a column's values on screen while leaving the shape of the data — row count, sort, filters, layout — perfectly readable. Built for presenting and screen sharing. Right-click a column heading for <strong>Redact column</strong>.</p>
|
|
1373
|
+
<p><strong>This is not a security control.</strong> The values stay in the model, the DOM, the clipboard and every export; anyone with the page can read them from devtools or by turning off one CSS rule. It defeats a camera, which is the whole claim. For a value that must not reach the browser at all, use <a href="#permissions">permissions</a> with <code>writeOnly</code>.</p>
|
|
1374
|
+
<pre><code>grid.redaction.toggle('salary'); <span class="cmt">// returns the state it is now in</span>
|
|
1375
|
+
grid.redaction.add('salary');
|
|
1376
|
+
grid.redaction.set(['salary', 'bonus']);
|
|
1377
|
+
grid.redaction.list(); <span class="cmt">// ['salary', 'bonus']</span>
|
|
1378
|
+
grid.redaction.clear(); <span class="cmt">// back to normal when the call ends</span></code></pre>
|
|
1379
|
+
<div class="table-wrap">
|
|
1380
|
+
<table>
|
|
1381
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1382
|
+
<tbody>
|
|
1383
|
+
<tr><td class="sig">has(colId)</td><td class="type">boolean</td><td class="desc">Is this column redacted?</td></tr>
|
|
1384
|
+
<tr><td class="sig">list()</td><td class="type">string[]</td><td class="desc">Every redacted column id.</td></tr>
|
|
1385
|
+
<tr><td class="sig">toggle(colId)</td><td class="type">boolean</td><td class="desc">Redact, or stop. Returns the state it is now in.</td></tr>
|
|
1386
|
+
<tr><td class="sig">add(colId)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1387
|
+
<tr><td class="sig">remove(colId)</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1388
|
+
<tr><td class="sig">set(ids)</td><td class="type">void</td><td class="desc">Replace the whole set.</td></tr>
|
|
1389
|
+
<tr><td class="sig">clear()</td><td class="type">void</td><td class="desc">Stop redacting everything.</td></tr>
|
|
1390
|
+
<tr><td class="sig">active</td><td class="type">boolean</td><td class="desc">True when at least one column is redacted.</td></tr>
|
|
1391
|
+
</tbody>
|
|
1392
|
+
</table>
|
|
1393
|
+
</div>
|
|
1394
|
+
<p>The treatment is a CSS token, so a host can swap it: <code>--lattice-redaction-filter</code> defaults to <code>blur(5px) contrast(0.85)</code> and accepts anything the <code>filter</code> property does, including <code>url(#your-svg-filter)</code> for a mosaic.</p>
|
|
1395
|
+
|
|
1396
|
+
<h2 id="formatting">grid.formatting</h2>
|
|
1397
|
+
<p>Conditional formatting rules the grid holds as <em>runtime state</em>, so an end user can change them. Rules travel in a saved view and undo like any other change. A scope is a column id, or <code>'*'</code> for every column; grid-wide rules are evaluated first, then the column's own, as one ordered list in which the first match wins.</p>
|
|
1398
|
+
<p>This is distinct from <code>compileRules()</code> feeding <code>cell.style</code>, which compiles at configuration time and is what you want for rules a user should not be able to change. Both work at once: a runtime rule layers over whatever <code>cell.style</code> produced, winning only for the properties it names.</p>
|
|
1399
|
+
<pre><code>createGrid(el, {
|
|
1400
|
+
formatting: { // optional seed
|
|
1401
|
+
margin: [{ when: { op: 'lt', value: 0 }, style: { background: '#fbeceb' } }],
|
|
1402
|
+
},
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
grid.formatting.add('margin', { when: { op: 'lt', value: 0 }, style: { background: 'red' } });
|
|
1406
|
+
grid.formatting.add('*', { when: { op: 'blank' }, style: { background: '#f1f3f5' } });
|
|
1407
|
+
grid.formatting.move('margin', ruleId, 0); // order is meaning
|
|
1408
|
+
grid.formatting.update('margin', ruleId, { enabled: false });
|
|
1409
|
+
grid.formatting.remove('margin', ruleId);
|
|
1410
|
+
grid.formatting.clear('margin'); // or clear() for everything</code></pre>
|
|
1411
|
+
<div class="table-wrap">
|
|
1412
|
+
<table>
|
|
1413
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1414
|
+
<tbody>
|
|
1415
|
+
<tr><td class="sig">list(scope?)</td><td class="type">Rule[]</td><td class="desc">The rules for one scope, in evaluation order.</td></tr>
|
|
1416
|
+
<tr><td class="sig">all()</td><td class="type">object</td><td class="desc">Every rule keyed by scope — the shape a saved view carries.</td></tr>
|
|
1417
|
+
<tr><td class="sig">scopes()</td><td class="type">string[]</td><td class="desc">Every scope holding at least one rule.</td></tr>
|
|
1418
|
+
<tr><td class="sig">add(scope, rule, opts?)</td><td class="type">Rule | null</td><td class="desc">Appends, or inserts at <code>opts.at</code>. Returns the rule with its generated <code>id</code>.</td></tr>
|
|
1419
|
+
<tr><td class="sig">remove(scope, idOrIndex)</td><td class="type">boolean</td><td class="desc">By id or position.</td></tr>
|
|
1420
|
+
<tr><td class="sig">update(scope, idOrIndex, patch)</td><td class="type">Rule | null</td><td class="desc">Merges fields. The <code>id</code> is identity and cannot be reassigned.</td></tr>
|
|
1421
|
+
<tr><td class="sig">move(scope, idOrIndex, to)</td><td class="type">boolean</td><td class="desc">Reorder, which can change which rule wins.</td></tr>
|
|
1422
|
+
<tr><td class="sig">set(scope, rules)</td><td class="type">Rule[]</td><td class="desc">Replace one scope.</td></tr>
|
|
1423
|
+
<tr><td class="sig">replaceAll(rules)</td><td class="type">void</td><td class="desc">Replace every scope at once.</td></tr>
|
|
1424
|
+
<tr><td class="sig">clear(scope?)</td><td class="type">void</td><td class="desc">One scope, or all of them.</td></tr>
|
|
1425
|
+
<tr><td class="sig">styleFor(colId, value)</td><td class="type">object | null</td><td class="desc">What the rules alone would paint, for an export or a preview.</td></tr>
|
|
1426
|
+
</tbody>
|
|
1427
|
+
</table>
|
|
1428
|
+
</div>
|
|
1429
|
+
<div class="note"><p>A rule held here must be JSON: <code>style</code> may not be a function, because the rules are serialised into views and undo slices. Config-time <code>cell.style</code> still accepts one. Group rows are not formatted, matching the way decoration is dropped for them.</p></div>
|
|
1430
|
+
|
|
1431
|
+
<h2 id="highlight">grid.highlight</h2>
|
|
1432
|
+
<p>One mechanism for two jobs: the flash a changed cell makes, and a marker you paint deliberately. A target is a cell (<code>{key, colId}</code>), a row (<code>{key}</code>, or a bare row key) or a column (<code>{colId}</code>). Cell beats row beats column, so a specific highlight is never hidden by a broad one laid over it.</p>
|
|
1433
|
+
<pre><code>createGrid(el, {
|
|
1434
|
+
highlightOnChange: { colour: '#ffe08a', duration: 1200 }, // or just true
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
grid.highlight({ key: 'r1', colId: 'cap' }, { colour: 'green', duration: 800 });
|
|
1438
|
+
grid.highlight({ key: 'r3' }, { colour: '#fdeaea', duration: 0 }); // 0 = until cleared
|
|
1439
|
+
grid.highlight({ colId: 'margin' }, { colour: '#e7f1fd', duration: 0 });
|
|
1440
|
+
grid.highlight.clear({ key: 'r3' });
|
|
1441
|
+
grid.highlight.clear(); // everything</code></pre>
|
|
1442
|
+
<div class="table-wrap">
|
|
1443
|
+
<table>
|
|
1444
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1445
|
+
<tbody>
|
|
1446
|
+
<tr><td class="sig">highlight(target, opts?)</td><td class="type">boolean</td><td class="desc"><code>colour</code> (or <code>color</code>) and <code>duration</code> in milliseconds. <code>duration: 0</code> stays until cleared.</td></tr>
|
|
1447
|
+
<tr><td class="sig">clear(target?)</td><td class="type">boolean</td><td class="desc">One target, or every highlight when called with nothing.</td></tr>
|
|
1448
|
+
<tr><td class="sig">list()</td><td class="type">object[]</td><td class="desc">Every active highlight and its remaining duration.</td></tr>
|
|
1449
|
+
<tr><td class="sig">colourFor(key, colId)</td><td class="type">string | null</td><td class="desc">What a given cell is painted, after precedence.</td></tr>
|
|
1450
|
+
</tbody>
|
|
1451
|
+
</table>
|
|
1452
|
+
</div>
|
|
1453
|
+
<div class="note"><p>A highlight belongs to the <em>row</em>, not the element. Rows are recycled as you scroll, so highlights are reapplied after every paint — they survive scrolling, sorting, filtering and paging without any of them knowing highlights exist.</p></div>
|
|
1454
|
+
|
|
1455
|
+
<h2 id="views">grid.views</h2>
|
|
1456
|
+
<p>Named states the user can return to. Views supplied in <code>config.views.saved</code> are <strong>defined views</strong>: listed apart in the picker, and neither renamable nor deletable — refused by the model as well as hidden in the interface. Views the user saves are their own and carry rename, share, default and delete.</p>
|
|
1457
|
+
<pre><code>createGrid(el, {
|
|
1458
|
+
views: {
|
|
1459
|
+
saved: [{ id: 'escalations', name: 'Escalations', description: 'Worst SLA first',
|
|
1460
|
+
state: { filters: { col: 'statusId', op: 'eq', value: 4 },
|
|
1461
|
+
sort: [{ col: 'utilisation', dir: 'desc' }] } }],
|
|
1462
|
+
allowSave: true, // false removes the save form entirely
|
|
1463
|
+
storage: { read, write }, // synchronous mirror; see the note below
|
|
1464
|
+
},
|
|
1465
|
+
});</code></pre>
|
|
1466
|
+
<div class="table-wrap">
|
|
1467
|
+
<table>
|
|
1468
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1469
|
+
<tbody>
|
|
1470
|
+
<tr><td class="sig">list() / get(id)</td><td class="type">object[] / object</td><td class="desc"></td></tr>
|
|
1471
|
+
<tr><td class="sig">save(name, opts?)</td><td class="type">object</td><td class="desc">Captures the current state. <code>opts</code>: <code>id</code>, <code>description</code>, <code>shared</code>, <code>isDefault</code>.</td></tr>
|
|
1472
|
+
<tr><td class="sig">apply(id)</td><td class="type">object | null</td><td class="desc">One undo entry. Resets to the baseline first, so a view is a <em>destination</em> rather than a patch — the same view gives the same grid whatever was applied before it.</td></tr>
|
|
1473
|
+
<tr><td class="sig">rename(id, name)</td><td class="type">object | null</td><td class="desc">Null for a defined view.</td></tr>
|
|
1474
|
+
<tr><td class="sig">remove(id)</td><td class="type">boolean</td><td class="desc">False for a defined view.</td></tr>
|
|
1475
|
+
<tr><td class="sig">setDefault(id)</td><td class="type">object | null</td><td class="desc"><code>null</code> clears it. A default view is applied on load, without recording an undo entry.</td></tr>
|
|
1476
|
+
<tr><td class="sig">export(id) / import(json)</td><td class="type">object</td><td class="desc">A JSON payload. What "sharing" means is yours to decide.</td></tr>
|
|
1477
|
+
<tr><td class="sig">diff(id)</td><td class="type">object | null</td><td class="desc">What applying a view would change.</td></tr>
|
|
1478
|
+
<tr><td class="sig">reload()</td><td class="type">void</td><td class="desc">Re-read from storage, discarding what is in memory.</td></tr>
|
|
1479
|
+
<tr><td class="sig">activeId</td><td class="type">string | null</td><td class="desc"></td></tr>
|
|
1480
|
+
</tbody>
|
|
1481
|
+
</table>
|
|
1482
|
+
</div>
|
|
1483
|
+
<div class="note"><p><strong>The grid makes no network calls.</strong> <code>storage.write</code> is for a synchronous mirror such as <code>localStorage</code>. To persist to a server, listen for <code>view:saved</code>, <code>view:renamed</code>, <code>view:removed</code> and <code>view:default</code> — each carries the one view that moved, so you can send a single record rather than diffing two lists. Because the grid does not track whether your write landed, a failed request leaves the view visible locally: catch it and call <code>views.reload()</code>.</p></div>
|
|
1484
|
+
|
|
1485
|
+
<h2 id="diff">grid.diff</h2>
|
|
1486
|
+
<p>Audit mode. Give it a prior snapshot and every row reports whether it was added, removed or changed, and which cells moved.</p>
|
|
1487
|
+
<div class="table-wrap">
|
|
1488
|
+
<table>
|
|
1489
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1490
|
+
<tbody>
|
|
1491
|
+
<tr><td class="sig">setSnapshot(rows) / clear()</td><td class="type">void</td><td class="desc">Also settable as <code>config.diff.snapshot</code>.</td></tr>
|
|
1492
|
+
<tr><td class="sig">summary()</td><td class="type">object</td><td class="desc"><code>{ added, removed, changed, unchanged }</code>.</td></tr>
|
|
1493
|
+
<tr><td class="sig">statusOf(key)</td><td class="type">string</td><td class="desc"><code>'added'</code>, <code>'removed'</code>, <code>'changed'</code> or <code>'unchanged'</code>.</td></tr>
|
|
1494
|
+
<tr><td class="sig">changedColumns(key)</td><td class="type">string[]</td><td class="desc"></td></tr>
|
|
1495
|
+
<tr><td class="sig">before(key, colId)</td><td class="type">unknown</td><td class="desc">The prior value. Also on the cell as <code>data-before</code>.</td></tr>
|
|
1496
|
+
<tr><td class="sig">enabled</td><td class="type">boolean</td><td class="desc"></td></tr>
|
|
1497
|
+
<tr><td class="sig">swap()</td><td class="type">boolean</td><td class="desc">Show the snapshot as the grid's data, and compare it against what was live until now. The snapshot is held as plain objects and never enters the columnar store, so a removed row cannot be sorted or filtered among live ones; swapping is the answer to that — the old rows become real rows with the whole pipeline behind them. Costs one ingest of each set, so it is a deliberate action rather than a toggle. <strong>The comparison reverses</strong>: what was an addition is now a removal. <code>swapped</code> reports which way round the grid is, and it is worth saying so in your interface.</td></tr>
|
|
1498
|
+
<tr><td class="sig">swapped</td><td class="type">boolean</td><td class="desc">True while the snapshot is the data.</td></tr>
|
|
1499
|
+
<tr><td class="sig">removedRows</td><td class="type">false | 'pinned' | 'data'</td><td class="desc">Whether a row in the snapshot but gone from the data is shown, and whether it counts as data. <code>false</code> (the default) leaves it out. <code>'pinned'</code> shows it beneath the rows, struck through, outside the row set — not counted, not exported, not selectable. <code>'data'</code> appends it to the set, so it <em>is</em> counted and exported. Neither is sorted or filtered among the live rows, because its values are the snapshot's; neither can be edited, because there is nothing left to write to.</td></tr>
|
|
1500
|
+
<tr><td class="sig">strictNull</td><td class="type">boolean</td><td class="desc">Off by default, so <code>null</code>, <code>undefined</code> and an absent field all count as the same absence. Set it to tell them apart — for an audit where a field being cleared and a field never being sent are different events. It compares the data as supplied, not as stored, so it is a statement about your snapshot rather than about the grid.</td></tr>
|
|
1501
|
+
</tbody>
|
|
1502
|
+
</table>
|
|
1503
|
+
</div>
|
|
1504
|
+
|
|
1505
|
+
<h2 id="builtin-renderers">Built-in renderers and editors</h2>
|
|
1506
|
+
<p>Both are addressable by name. Anything you register through <code>components</code> is
|
|
1507
|
+
addressable the same way, and a name you register wins over a built-in one.</p>
|
|
1508
|
+
<p><strong>Cell renderers</strong>, for <code>cell.render</code>:</p>
|
|
1509
|
+
<div class="table-wrap">
|
|
1510
|
+
<table>
|
|
1511
|
+
<thead><tr><th>Name</th><th>Draws</th></tr></thead>
|
|
1512
|
+
<tbody>
|
|
1513
|
+
<tr><td class="sig">area</td><td class="desc">A filled sparkline over a series.</td></tr>
|
|
1514
|
+
<tr><td class="sig">bullet</td><td class="desc">A value against a target and qualitative bands.</td></tr>
|
|
1515
|
+
<tr><td class="sig">checkbox</td><td class="desc">A boolean, optionally as a switch.</td></tr>
|
|
1516
|
+
<tr><td class="sig">colour</td><td class="desc">A colour swatch with its value.</td></tr>
|
|
1517
|
+
<tr><td class="sig">column</td><td class="desc">A column sparkline.</td></tr>
|
|
1518
|
+
<tr><td class="sig">delta</td><td class="desc">Movement since the last value, with direction.</td></tr>
|
|
1519
|
+
<tr><td class="sig">detailExpander</td><td class="desc">The master-detail chevron. Generated; not usually named directly.</td></tr>
|
|
1520
|
+
<tr><td class="sig">donut</td><td class="desc">A donut chart from a series.</td></tr>
|
|
1521
|
+
<tr><td class="sig">gauge</td><td class="desc">A value on an arc against a range.</td></tr>
|
|
1522
|
+
<tr><td class="sig">group</td><td class="desc">The group and tree label, with its expander and indent.</td></tr>
|
|
1523
|
+
<tr><td class="sig">icon</td><td class="desc">An icon chosen from the value.</td></tr>
|
|
1524
|
+
<tr><td class="sig">image</td><td class="desc">A picture from a URL. Selected automatically for <code>type: 'image'</code>.</td></tr>
|
|
1525
|
+
<tr><td class="sig">line</td><td class="desc">A line sparkline.</td></tr>
|
|
1526
|
+
<tr><td class="sig">link</td><td class="desc">An anchor, with the text and href drawn from the row.</td></tr>
|
|
1527
|
+
<tr><td class="sig">pie</td><td class="desc">A pie chart from a series.</td></tr>
|
|
1528
|
+
<tr><td class="sig">pill</td><td class="desc">A status chip carrying a semantic variant.</td></tr>
|
|
1529
|
+
<tr><td class="sig">progress</td><td class="desc">A progress bar with an optional label.</td></tr>
|
|
1530
|
+
<tr><td class="sig">qrcode</td><td class="desc">A QR code of the value.</td></tr>
|
|
1531
|
+
<tr><td class="sig">range</td><td class="desc">A span between a low and a high value.</td></tr>
|
|
1532
|
+
<tr><td class="sig">rating</td><td class="desc">A star rating.</td></tr>
|
|
1533
|
+
<tr><td class="sig">skeleton</td><td class="desc">A loading placeholder for a row not yet arrived.</td></tr>
|
|
1534
|
+
<tr><td class="sig">stacked</td><td class="desc">A stacked proportion bar.</td></tr>
|
|
1535
|
+
<tr><td class="sig">twoline</td><td class="desc">A primary value with a secondary line beneath it.</td></tr>
|
|
1536
|
+
<tr><td class="sig">winloss</td><td class="desc">A win/loss sparkline of signed values.</td></tr>
|
|
1537
|
+
</tbody>
|
|
1538
|
+
</table>
|
|
1539
|
+
</div>
|
|
1540
|
+
<p><strong>Editors</strong>, for <code>edit.editor</code>. Each column type already selects a
|
|
1541
|
+
sensible one, so naming an editor is for overriding that choice:</p>
|
|
1542
|
+
<div class="table-wrap">
|
|
1543
|
+
<table>
|
|
1544
|
+
<thead><tr><th>Name</th><th>Edits</th></tr></thead>
|
|
1545
|
+
<tbody>
|
|
1546
|
+
<tr><td class="sig">checkbox</td><td class="desc">A boolean.</td></tr>
|
|
1547
|
+
<tr><td class="sig">code</td><td class="desc">Source text, in a monospace field.</td></tr>
|
|
1548
|
+
<tr><td class="sig">colour</td><td class="desc">A colour.</td></tr>
|
|
1549
|
+
<tr><td class="sig">date</td><td class="desc">A calendar date.</td></tr>
|
|
1550
|
+
<tr><td class="sig">datetime</td><td class="desc">A date and a time together.</td></tr>
|
|
1551
|
+
<tr><td class="sig">duration</td><td class="desc">A length of time.</td></tr>
|
|
1552
|
+
<tr><td class="sig">iconPicker</td><td class="desc">One icon from a set.</td></tr>
|
|
1553
|
+
<tr><td class="sig">ipaddress</td><td class="desc">An IPv4 or IPv6 address.</td></tr>
|
|
1554
|
+
<tr><td class="sig">multiSelect</td><td class="desc">Several options, as chips.</td></tr>
|
|
1555
|
+
<tr><td class="sig">number</td><td class="desc">A number, with the column's constraints.</td></tr>
|
|
1556
|
+
<tr><td class="sig">objectPicker</td><td class="desc">A record chosen from a list.</td></tr>
|
|
1557
|
+
<tr><td class="sig">password</td><td class="desc">A masked secret.</td></tr>
|
|
1558
|
+
<tr><td class="sig">radix</td><td class="desc">A value in its own base.</td></tr>
|
|
1559
|
+
<tr><td class="sig">rating</td><td class="desc">A star rating.</td></tr>
|
|
1560
|
+
<tr><td class="sig">segmented</td><td class="desc">One of a few options, as a segmented control.</td></tr>
|
|
1561
|
+
<tr><td class="sig">select</td><td class="desc">One option from a list.</td></tr>
|
|
1562
|
+
<tr><td class="sig">slider</td><td class="desc">A number on a track.</td></tr>
|
|
1563
|
+
<tr><td class="sig">text</td><td class="desc">A single line. The default.</td></tr>
|
|
1564
|
+
<tr><td class="sig">textarea</td><td class="desc">Several lines.</td></tr>
|
|
1565
|
+
<tr><td class="sig">time</td><td class="desc">A time of day.</td></tr>
|
|
1566
|
+
<tr><td class="sig">treeSelect</td><td class="desc">A value from a hierarchy.</td></tr>
|
|
1567
|
+
<tr><td class="sig">unit</td><td class="desc">A quantity with a unit.</td></tr>
|
|
1568
|
+
</tbody>
|
|
1569
|
+
</table>
|
|
1570
|
+
</div>
|
|
1571
|
+
|
|
1572
|
+
<h2 id="permissions">grid.permissions</h2>
|
|
1573
|
+
<p>Four levels per column, resolved from configuration or a callback. They are the four corners of read × write rather than a ladder:</p>
|
|
1574
|
+
<div class="table-wrap">
|
|
1575
|
+
<table>
|
|
1576
|
+
<thead><tr><th>Level</th><th>Visible</th><th>Readable</th><th>Editable</th><th>For</th></tr></thead>
|
|
1577
|
+
<tbody>
|
|
1578
|
+
<tr><td class="sig">hidden</td><td>—</td><td>—</td><td>—</td><td class="desc">Absent from the grid, the tool panel, exports, the clipboard, saved state, the filter model and formula references.</td></tr>
|
|
1579
|
+
<tr><td class="sig">read</td><td>yes</td><td>yes</td><td>—</td><td class="desc">No editor opens; paste, fill and range-clear skip it.</td></tr>
|
|
1580
|
+
<tr><td class="sig">writeOnly</td><td>yes</td><td>—</td><td>yes</td><td class="desc">A secret: an API key a user may rotate but never read. The cell shows a mask and the editor opens empty, and a formula in another cell cannot reference it.</td></tr>
|
|
1581
|
+
<tr><td class="sig">write</td><td>yes</td><td>yes</td><td>yes</td><td class="desc">The default, so the feature is opt-in.</td></tr>
|
|
1582
|
+
</tbody>
|
|
1583
|
+
</table>
|
|
1584
|
+
</div>
|
|
1585
|
+
<pre><code>permissions: 'read' // blanket
|
|
1586
|
+
permissions: { salary: 'read', ssn: 'hidden' } // map; '*' sets the default
|
|
1587
|
+
permissions: (column, ctx) => ctx.context.role === 'admin' ? 'write' : 'read'
|
|
1588
|
+
permissions: { default: 'read', columns: { name: 'write' }, resolve }
|
|
1589
|
+
|
|
1590
|
+
grid.permissions.setContext({ role: 'clerk' }); // re-resolves everything</code></pre>
|
|
1591
|
+
<div class="note"><p><strong>For three of the four this is a usability control, not a security boundary.</strong> Anything the grid can render it has already loaded, and devtools reaches it. <code>writeOnly</code> is the exception, and the reason it is worth having: nothing in the grid needs the value, so your server can send <code>null</code> for that field and the column still works — at which point the secret is genuinely not on the page. Enforce everything else on the server; <code>permittedColumns</code> and <code>permittedExport</code> are pure and dependency-free so the same policy can run there.</p></div>
|
|
1592
|
+
|
|
1593
|
+
<h2 id="ai">grid.ai</h2>
|
|
1594
|
+
<p>The grid composes a prompt describing its own columns and operators, you send it to whichever model you like, and it validates the reply before anything is applied. It makes no network call and has no default model.</p>
|
|
1595
|
+
<div class="table-wrap">
|
|
1596
|
+
<table>
|
|
1597
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1598
|
+
<tbody>
|
|
1599
|
+
<tr><td class="sig">schema(opts?)</td><td class="type">object</td><td class="desc">The schema: columns, types, permitted operators.</td></tr>
|
|
1600
|
+
<tr><td class="sig">prompt(text, opts?)</td><td class="type">string</td><td class="desc">The message to send, schema included.</td></tr>
|
|
1601
|
+
<tr><td class="sig">plan(reply, opts?)</td><td class="type">object</td><td class="desc">Parse and validate. Unknown columns and operators are rejected with a reason; valid actions in the same reply are kept. <code>plan.describe()</code> renders it in plain English for confirmation.</td></tr>
|
|
1602
|
+
<tr><td class="sig">apply(plan)</td><td class="type">object</td><td class="desc">Applies an approved plan as <strong>one</strong> undo entry, labelled with what it did.</td></tr>
|
|
1603
|
+
</tbody>
|
|
1604
|
+
</table>
|
|
1605
|
+
</div>
|
|
1606
|
+
<div class="note"><p>Actions: <code>setFilters</code>, <code>setSort</code>, <code>groupBy</code>, <code>showColumns</code>, <code>hideColumns</code>, <code>setQuick</code>, <code>clear</code>. Nothing else is executable, so a model cannot be talked into an operation the vocabulary does not contain. <code>docs/AI-SKILL.md</code> is the reference to hand your model.</p></div>
|
|
1607
|
+
|
|
1608
|
+
<h2 id="overlay">grid.overlay</h2>
|
|
1609
|
+
<div class="table-wrap">
|
|
1610
|
+
<table>
|
|
1611
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1612
|
+
<tbody>
|
|
1613
|
+
<tr><td class="sig">show(kind, message?)</td><td class="type">void</td><td class="desc"><code>'loading'</code> or <code>'empty'</code>.</td></tr>
|
|
1614
|
+
<tr><td class="sig">hide()</td><td class="type">void</td><td class="desc"></td></tr>
|
|
1615
|
+
</tbody>
|
|
1616
|
+
</table>
|
|
1617
|
+
</div>
|
|
1618
|
+
|
|
1619
|
+
<h2 id="maximise">grid.maximise</h2>
|
|
1620
|
+
<p>Fills the browser window with the grid, and puts it back. The rail's last button is
|
|
1621
|
+
this; <code>grid.maximise</code> is the same thing, so an application can bind its own
|
|
1622
|
+
control or keyboard shortcut. <kbd>Esc</kbd> restores.</p>
|
|
1623
|
+
|
|
1624
|
+
<pre><code>grid.maximise.toggle(); <span class="cmt">// what the rail button calls</span>
|
|
1625
|
+
grid.maximise.enter();
|
|
1626
|
+
grid.maximise.active(); <span class="cmt">// true while it fills the window</span>
|
|
1627
|
+
grid.maximise.exit();</code></pre>
|
|
1628
|
+
|
|
1629
|
+
<div class="table-wrap">
|
|
1630
|
+
<table>
|
|
1631
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1632
|
+
<tbody>
|
|
1633
|
+
<tr><td class="sig">enter()</td><td class="type">boolean</td><td class="desc">Fill the window. <code>false</code> when the host element is not in the document.</td></tr>
|
|
1634
|
+
<tr><td class="sig">exit()</td><td class="type">boolean</td><td class="desc">Back to the page. <code>false</code> when it was not maximised.</td></tr>
|
|
1635
|
+
<tr><td class="sig">toggle()</td><td class="type">boolean</td><td class="desc">Whether the grid is maximised afterwards.</td></tr>
|
|
1636
|
+
<tr><td class="sig">active()</td><td class="type">boolean</td><td class="desc">Whether it is filling the window now.</td></tr>
|
|
1637
|
+
</tbody>
|
|
1638
|
+
</table>
|
|
1639
|
+
</div>
|
|
1640
|
+
|
|
1641
|
+
<div class="note"><p>The host element is <strong>moved</strong> to <code><body></code> and pinned to
|
|
1642
|
+
the viewport, then moved back between the same two siblings. A <code>position: fixed</code> element is
|
|
1643
|
+
positioned against the nearest ancestor carrying a <code>transform</code>, <code>filter</code>,
|
|
1644
|
+
<code>contain</code> or <code>will-change</code> — any card, animated panel or sticky shell — so styling
|
|
1645
|
+
alone fills the window on one page and lands in a small box on the next. A hidden placeholder holds the
|
|
1646
|
+
vacated space at the size the grid had, so the page behind neither reflows nor loses its scroll position.</p></div>
|
|
1647
|
+
|
|
1648
|
+
<div class="note"><p>Geometry is applied as inline styles and every displaced property is handed back
|
|
1649
|
+
exactly as it was found, because the element being restyled is <em>yours</em>. While maximised the element
|
|
1650
|
+
carries <code>.lat-maximised</code> and <code><body></code> carries <code>.lat-maximised-host</code>,
|
|
1651
|
+
as hooks for your own CSS.</p></div>
|
|
1652
|
+
|
|
1653
|
+
<h2 id="licence">grid.licence</h2>
|
|
1654
|
+
<p><strong>There is one Lattice Grid and every copy is feature-identical.</strong> No community
|
|
1655
|
+
edition, no pro tier, no feature held back behind a key. A licence removes the trial watermark;
|
|
1656
|
+
that is the whole of what it does.</p>
|
|
1657
|
+
<p>Free to develop against, licensed to deploy. A grid on <strong>localhost</strong> — or any
|
|
1658
|
+
loopback host — needs no key at all. On any other domain an unlicensed grid still renders
|
|
1659
|
+
everything and carries a small trial watermark linking to latticegrid.dev. Nothing ever refuses
|
|
1660
|
+
to render: the failure worth avoiding is a broken production screen, and no licensing state is
|
|
1661
|
+
worth causing one.</p>
|
|
1662
|
+
|
|
1663
|
+
<pre><code><span class="cmt">// Before creating a grid.</span>
|
|
1664
|
+
LatticeGrid.setLicence('LG1.…'); <span class="cmt">// your key; setLicense also works</span>
|
|
1665
|
+
|
|
1666
|
+
<span class="kw">const</span> grid = LatticeGrid.createGrid(el, config);
|
|
1667
|
+
grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'</span></code></pre>
|
|
1668
|
+
|
|
1669
|
+
<div class="table-wrap">
|
|
1670
|
+
<table>
|
|
1671
|
+
<thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1672
|
+
<tbody>
|
|
1673
|
+
<tr><td class="sig">set(key)</td><td class="type">object</td><td class="desc">Install a key for the process. Returns the provisional verdict; the check is asynchronous and <code>licence:changed</code> fires when it settles.</td></tr>
|
|
1674
|
+
<tr><td class="sig">state()</td><td class="type">string</td><td class="desc"><code>'licensed'</code>, <code>'localhost'</code> or <code>'trial'</code>.</td></tr>
|
|
1675
|
+
<tr><td class="sig">info()</td><td class="type">object</td><td class="desc"><code>{ valid, reason, issuedTo, expires, product }</code>.</td></tr>
|
|
1676
|
+
<tr><td class="sig">watermark()</td><td class="type">boolean</td><td class="desc">Whether the trial mark is showing.</td></tr>
|
|
1677
|
+
<tr><td class="sig">ready</td><td class="type">Promise</td><td class="desc">Settles when the licence check finishes.</td></tr>
|
|
1678
|
+
</tbody>
|
|
1679
|
+
</table>
|
|
1680
|
+
</div>
|
|
1681
|
+
|
|
1682
|
+
<h3>Domains</h3>
|
|
1683
|
+
<p>A key names the hosts it covers. <code>*.acme.com</code> matches <code>app.acme.com</code>,
|
|
1684
|
+
<code>a.b.acme.com</code> <em>and</em> <code>acme.com</code> itself — a wildcard that refused the
|
|
1685
|
+
apex would be a puzzle rather than a licence. A bare <code>acme.com</code> matches only itself,
|
|
1686
|
+
and a key naming no domains is valid anywhere. Matching ignores case and a trailing dot.</p>
|
|
1687
|
+
|
|
1688
|
+
<div class="table-wrap">
|
|
1689
|
+
<table>
|
|
1690
|
+
<thead><tr><th>Host</th><th>No key</th><th>Key for <code>*.acme.com</code></th></tr></thead>
|
|
1691
|
+
<tbody>
|
|
1692
|
+
<tr><td class="sig">localhost, 127.0.0.1, ::1, *.localhost</td><td>everything, no mark</td><td>everything, no mark</td></tr>
|
|
1693
|
+
<tr><td class="sig">app.acme.com</td><td>everything, trial watermark</td><td>everything, no mark</td></tr>
|
|
1694
|
+
<tr><td class="sig">acme.com</td><td>everything, trial watermark</td><td>everything, no mark</td></tr>
|
|
1695
|
+
<tr><td class="sig">other.example.org</td><td>everything, trial watermark</td><td>everything, trial watermark</td></tr>
|
|
1696
|
+
</tbody>
|
|
1697
|
+
</table>
|
|
1698
|
+
</div>
|
|
1699
|
+
<div class="note"><p><code>.local</code>, <code>.internal</code> and private IP ranges are <strong>not</strong> exempt. They are ordinary LAN names, and a corporate intranet is a deployment like any other.</p></div>
|
|
1700
|
+
|
|
1701
|
+
<h3>Getting a key</h3>
|
|
1702
|
+
<p>Keys are issued from <a href="https://www.latticegrid.dev">latticegrid.dev</a>. A key is
|
|
1703
|
+
issued per deployment rather than per developer or per seat: name the domains the grid will run
|
|
1704
|
+
on and one key covers every developer, every build and every user on them.</p>
|
|
1705
|
+
<p>Checking a key needs no network. There is no licence server, no call home, and nothing that
|
|
1706
|
+
can fail at three in the morning — a key carries its own answer and the grid reads it locally.
|
|
1707
|
+
An expired key, a key for the wrong domain, or a key that will not read all do the same thing:
|
|
1708
|
+
log one console warning and show the watermark.</p>
|
|
1709
|
+
<div class="note"><p>Install the key before creating a grid. Setting one later still works —
|
|
1710
|
+
<code>licence:changed</code> fires and the watermark is removed — but the first frames of the
|
|
1711
|
+
grid will carry it.</p></div>
|
|
1712
|
+
|
|
1713
|
+
<h2 id="sources">Sources</h2>
|
|
1714
|
+
<p class="section-note">Where rows come from. <code>memory</code> is the default and needs no configuration.</p>
|
|
1715
|
+
<div class="table-wrap">
|
|
1716
|
+
<table>
|
|
1717
|
+
<thead><tr><th>Mode</th><th>Needs</th><th>Description</th></tr></thead>
|
|
1718
|
+
<tbody>
|
|
1719
|
+
<tr><td class="sig">memory</td><td class="type">rows</td><td class="desc">Everything is present. The grid filters, sorts, groups and totals it.</td></tr>
|
|
1720
|
+
<tr><td class="sig">paged</td><td class="type">fetch</td><td class="desc">A page at a time from a server that paginates.</td></tr>
|
|
1721
|
+
<tr><td class="sig">remote</td><td class="type">fetch</td><td class="desc">Blocks fetched as the viewport reaches them, with sort, filter and grouping pushed to the server.</td></tr>
|
|
1722
|
+
<tr><td class="sig">stream</td><td class="type">connect</td><td class="desc">Rows arriving over time. Promotes to memory once complete.</td></tr>
|
|
1723
|
+
</tbody>
|
|
1724
|
+
</table>
|
|
1725
|
+
</div>
|
|
1726
|
+
|
|
1727
|
+
<h3>The remote request</h3>
|
|
1728
|
+
<p class="section-note">Your <code>fetch</code> receives one object and returns <code>{ rows, total }</code>.</p>
|
|
1729
|
+
<div class="table-wrap">
|
|
1730
|
+
<table>
|
|
1731
|
+
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
1732
|
+
<tbody>
|
|
1733
|
+
<tr><td class="name">range</td><td class="type">{ start, end }</td><td class="desc">The block wanted, end exclusive. <strong>Not</strong> <code>from</code>/<code>to</code>.</td></tr>
|
|
1734
|
+
<tr><td class="name">sort</td><td class="type">{ col, dir }[]</td><td class="desc">In priority order.</td></tr>
|
|
1735
|
+
<tr><td class="name">filters</td><td class="type">FilterSet</td><td class="desc">The condition tree, in the wire form described under <a href="#operators">operators</a>.</td></tr>
|
|
1736
|
+
<tr><td class="name">quick</td><td class="type">string</td><td class="desc">Present only when the quick filter is set.</td></tr>
|
|
1737
|
+
<tr><td class="name">groupBy / groupPath</td><td class="type">string[] / unknown[]</td><td class="desc">Which columns group, and which node this block belongs to.</td></tr>
|
|
1738
|
+
<tr><td class="name">pivotBy / pivotMode</td><td class="type">string[] / boolean</td><td class="desc"></td></tr>
|
|
1739
|
+
<tr><td class="name">totals</td><td class="type">string[]</td><td class="desc">Columns wanting an aggregate, so the server can compute them.</td></tr>
|
|
1740
|
+
<tr><td class="name">context</td><td class="type">unknown</td><td class="desc">Your own <code>config.context</code>, passed through untouched.</td></tr>
|
|
1741
|
+
<tr><td class="name">signal</td><td class="type">AbortSignal</td><td class="desc">Aborted when the request is superseded — pass it to <code>fetch</code>.</td></tr>
|
|
1742
|
+
<tr><td class="name">protocol</td><td class="type">number</td><td class="desc">Wire version, so a server can tell what it is talking to.</td></tr>
|
|
1743
|
+
</tbody>
|
|
1744
|
+
</table>
|
|
1745
|
+
</div>
|
|
1746
|
+
<div class="note"><p>Blocks are requested as the viewport reaches them and cached. Changing the sort, the filter or the grouping invalidates the cache and re-queries.</p></div>
|
|
1747
|
+
|
|
1748
|
+
<h2 id="events">Events</h2>
|
|
1749
|
+
<p class="section-note">
|
|
1750
|
+
One bus. There are no <code>onX</code> configuration properties. Every payload also carries
|
|
1751
|
+
<code>type</code>, <code>origin</code> and <code>grid</code>.
|
|
1752
|
+
</p>
|
|
1753
|
+
|
|
1754
|
+
<pre><code><span class="kw">const</span> off = grid.on('cell:changed', e => save(e.row.data));
|
|
1755
|
+
grid.once('ready', init);
|
|
1756
|
+
grid.on('*', e => console.log(e.type, e)); <span class="cmt">// wildcard, for debugging</span>
|
|
1757
|
+
off(); <span class="cmt">// on() returns its own unsubscribe</span></code></pre>
|
|
1758
|
+
|
|
1759
|
+
<div class="note">
|
|
1760
|
+
<p><strong>origin</strong> is <code>'api'</code>, <code>'user'</code> or <code>'init'</code>. A host persisting state reads it to ignore its own writes and avoid a feedback loop.</p>
|
|
1761
|
+
</div>
|
|
1762
|
+
|
|
1763
|
+
<div class="table-wrap">
|
|
1764
|
+
<table>
|
|
1765
|
+
<thead><tr><th>Event</th><th>Payload</th><th>Fires when</th></tr></thead>
|
|
1766
|
+
<tbody>
|
|
1767
|
+
<tr><td class="name">ready</td><td class="type">{}</td><td class="desc">First layout is complete and the API is safe to drive.</td></tr>
|
|
1768
|
+
<tr><td class="name">render:first</td><td class="type">{}</td><td class="desc">First paint — the number to measure time-to-first-row against.</td></tr>
|
|
1769
|
+
<tr><td class="name">destroy</td><td class="type">{}</td><td class="desc"><code>grid.destroy()</code> has run.</td></tr>
|
|
1770
|
+
<tr><td class="name">model:changed</td><td class="type">{ reason }</td><td class="desc">Columns, grouping, pivot or another structural change.</td></tr>
|
|
1771
|
+
<tr><td class="name">rows:changed</td><td class="type">{ added, updated, removed, plan }</td><td class="desc">The display row set changed. Counts, not row objects, on the streaming path.</td></tr>
|
|
1772
|
+
<tr><td class="name">rows:queued</td><td class="type">{ pending }</td><td class="desc">A batched change is waiting for the next frame.</td></tr>
|
|
1773
|
+
<tr><td class="name">cell:changed</td><td class="type">{ row, key, colId, value, oldValue, undo }</td><td class="desc">A committed edit reached the data. <code>undo</code> distinguishes a rollback.</td></tr>
|
|
1774
|
+
<tr><td class="name">cell:pending</td><td class="type">{ row, key, colId, value, before, id }</td><td class="desc">Applied optimistically, not yet durable. Only with <code>edit.commit</code>.</td></tr>
|
|
1775
|
+
<tr><td class="name">cell:confirmed</td><td class="type">{ row, key, colId, value, id, superseded }</td><td class="desc">The write reached the server.</td></tr>
|
|
1776
|
+
<tr><td class="name">cell:reverted</td><td class="type">{ row, key, colId, rejected, restored, reason, id, superseded, applied }</td><td class="desc">The write failed. <code>applied: false</code> means a newer edit owned the cell, so nothing was written back.</td></tr>
|
|
1777
|
+
<tr><td class="name">cell:edit:start</td><td class="type">{ row, key, colId, column }</td><td class="desc">An edit session opened.</td></tr>
|
|
1778
|
+
<tr><td class="name">cell:edit:end</td><td class="type">{ row, key, colId, valid, errors }</td><td class="desc">It closed — committed or cancelled.</td></tr>
|
|
1779
|
+
<tr><td class="name">cell:clicked</td><td class="type">{ row, key, index, colId, column, value, text, event }</td><td class="desc">A cell was clicked. Announcement only — nothing is consumed, so editing and selection behave unchanged.</td></tr>
|
|
1780
|
+
<tr><td class="name">cell:dblclicked</td><td class="type">{ ...as cell:clicked }</td><td class="desc"></td></tr>
|
|
1781
|
+
<tr><td class="name">row:clicked</td><td class="type">{ row, key, index, event }</td><td class="desc">Emitted alongside the cell event, cell first.</td></tr>
|
|
1782
|
+
<tr><td class="name">row:dblclicked</td><td class="type">{ row, key, index, event }</td><td class="desc"></td></tr>
|
|
1783
|
+
<tr><td class="name">row:edit:start</td><td class="type">{ row, key, colId, column }</td><td class="desc">Replaces the cell pair when <code>edit.mode</code> is <code>'row'</code>.</td></tr>
|
|
1784
|
+
<tr><td class="name">row:edit:end</td><td class="type">{ row, key, colId, valid, errors }</td><td class="desc">In row mode an invalid cell blocks the whole commit and the session stays open.</td></tr>
|
|
1785
|
+
<tr><td class="name">cell:contextmenu</td><td class="type">{ ...cellParams, row }</td><td class="desc">Right-click on a cell.</td></tr>
|
|
1786
|
+
<tr><td class="name">sort:changed</td><td class="type">{ sort }</td><td class="desc">The full sort entry list.</td></tr>
|
|
1787
|
+
<tr><td class="name">filter:changed</td><td class="type">{ filters } | { quick }</td><td class="desc">The condition tree or the quick filter changed.</td></tr>
|
|
1788
|
+
<tr><td class="name">group:toggled</td><td class="type">{ expanded, all? }</td><td class="desc">A group row opened or closed.</td></tr>
|
|
1789
|
+
<tr><td class="name">column:moved</td><td class="type">{ colId, to, origin }</td><td class="desc">Reordered by drag or by API.</td></tr>
|
|
1790
|
+
<tr><td class="name">column:resized</td><td class="type">{ colId, width, origin }</td><td class="desc"></td></tr>
|
|
1791
|
+
<tr><td class="name">column:visible</td><td class="type">{ ids, hidden }</td><td class="desc">Columns shown or hidden.</td></tr>
|
|
1792
|
+
<tr><td class="name">column:pinned</td><td class="type">{ id, side }</td><td class="desc"><code>side</code> is <code>'start'</code>, <code>'end'</code> or <code>null</code>.</td></tr>
|
|
1793
|
+
<tr><td class="name">column:grouped</td><td class="type">{ columns }</td><td class="desc">The row-group column list changed.</td></tr>
|
|
1794
|
+
<tr><td class="name">column:pivoted</td><td class="type">{ columns } | { pivotFields, remote }</td><td class="desc"></td></tr>
|
|
1795
|
+
<tr><td class="name">column:menu:open</td><td class="type">{ colId }</td><td class="desc">Header menu opened.</td></tr>
|
|
1796
|
+
<tr><td class="name">column:filter:open</td><td class="type">{ colId }</td><td class="desc">Header filter popup opened.</td></tr>
|
|
1797
|
+
<tr><td class="name">selection:changed</td><td class="type">{ keys, rows }</td><td class="desc"></td></tr>
|
|
1798
|
+
<tr><td class="name">range:changed</td><td class="type">{ ranges }</td><td class="desc">Cell range selection changed.</td></tr>
|
|
1799
|
+
<tr><td class="name">page:changed</td><td class="type">{ page, pageSize, total, pageCount }</td><td class="desc">Fired after the rows have moved, whether the page changed by API or by the pager control.</td></tr>
|
|
1800
|
+
<tr><td class="name">config:changed</td><td class="type">{ key, value, oldValue } | { keys, values, oldValues }</td><td class="desc">A configuration key changed. Emitted after the grid has rebuilt, so a listener reading the grid back sees the change rather than what it replaced.</td></tr>
|
|
1801
|
+
<tr><td class="name">scroll</td><td class="type">{ top, left }</td><td class="desc">Throttled to the frame.</td></tr>
|
|
1802
|
+
<tr><td class="name">scroll:end</td><td class="type">{}</td><td class="desc">Scrolling settled — the moment to trigger deferred work.</td></tr>
|
|
1803
|
+
<tr><td class="name">size:changed</td><td class="type">{}</td><td class="desc">The viewport resized.</td></tr>
|
|
1804
|
+
<tr><td class="name">state:changed</td><td class="type">{ state, report }</td><td class="desc"><code>report</code> lists anything a restore could not apply.</td></tr>
|
|
1805
|
+
<tr><td class="name">stream:chunk</td><td class="type">{ loaded, estimated, count, renders }</td><td class="desc">A streamed chunk landed.</td></tr>
|
|
1806
|
+
<tr><td class="name">stream:end</td><td class="type">{ loaded, promoted, threshold }</td><td class="desc">Streaming finished; <code>promoted</code> means it switched to in-memory.</td></tr>
|
|
1807
|
+
<tr><td class="name">source:error</td><td class="type">{ error, block?, range? }</td><td class="desc">A source or block load failed.</td></tr>
|
|
1808
|
+
<tr><td class="name">clipboard:copy</td><td class="type">{ text, ok, rows }</td><td class="desc">A copy left the grid.</td></tr>
|
|
1809
|
+
<tr><td class="name">export:progress</td><td class="type">{ ... }</td><td class="desc">Progress on a streamed export.</td></tr>
|
|
1810
|
+
<tr><td class="name">toolpanel:focus</td><td class="type">{}</td><td class="desc">The documented keyboard shortcut reached the tool panel.</td></tr>
|
|
1811
|
+
<tr><td class="name">history:changed</td><td class="type">{ canUndo, canRedo, undo, redo }</td><td class="desc">Emitted <em>after</em> the entry is pushed, so a toolbar reading it names the right action. Repainting from <code>sort:changed</code> instead reads the timeline one action behind.</td></tr>
|
|
1812
|
+
<tr><td class="name">history:applied</td><td class="type">{ direction, step }</td><td class="desc">An action was undone or redone. Distinct from <code>history:changed</code>, which also fires when a new action is pushed onto the stacks and so cannot tell you anything was reversed.</td></tr>
|
|
1813
|
+
<tr><td class="name">state:reset</td><td class="type">{ state }</td><td class="desc">The grid was returned to its baseline.</td></tr>
|
|
1814
|
+
<tr><td class="name">highlight:changed</td><td class="type">{ highlights }</td><td class="desc">A highlight was added or cleared.</td></tr>
|
|
1815
|
+
<tr><td class="name">redaction:changed</td><td class="type">{ columns }</td><td class="desc">A column was redacted or restored.</td></tr>
|
|
1816
|
+
<tr><td class="name">header:contextmenu</td><td class="type">{ colId, column, element, x, y }</td><td class="desc">A column heading was right-clicked.</td></tr>
|
|
1817
|
+
<tr><td class="name">render:done</td><td class="type">{ first, last }</td><td class="desc">The cells are written and stable. Anything decorating them from outside must run after this — the cell layer rewrites each cell's <code>className</code> wholesale and would otherwise erase it.</td></tr>
|
|
1818
|
+
<tr><td class="name">views:changed</td><td class="type">{ views, reason, view, activeId? }</td><td class="desc">The whole list, plus what moved and why.</td></tr>
|
|
1819
|
+
<tr><td class="name">view:saved</td><td class="type">{ view, views, reason }</td><td class="desc">Carries the one view that moved — enough to POST a single record without diffing two lists.</td></tr>
|
|
1820
|
+
<tr><td class="name">view:renamed</td><td class="type">{ view, views, reason }</td><td class="desc"></td></tr>
|
|
1821
|
+
<tr><td class="name">view:removed</td><td class="type">{ view, views, reason }</td><td class="desc"></td></tr>
|
|
1822
|
+
<tr><td class="name">view:default</td><td class="type">{ view, views, reason }</td><td class="desc"><code>view</code> is null when the default was cleared.</td></tr>
|
|
1823
|
+
<tr><td class="name">view:applied</td><td class="type">{ view, views, activeId }</td><td class="desc">Emits no storage write: applying a view changes nothing to persist.</td></tr>
|
|
1824
|
+
<tr><td class="name">permissions:changed</td><td class="type">{ levels }</td><td class="desc">The context moved and every column re-resolved.</td></tr>
|
|
1825
|
+
<tr><td class="name">diff:changed</td><td class="type">{ summary }</td><td class="desc">A snapshot was set or cleared.</td></tr>
|
|
1826
|
+
<tr><td class="name">licence:changed</td><td class="type">{ info, state }</td><td class="desc">A key was installed, and again when verification settles.</td></tr>
|
|
1827
|
+
</tbody>
|
|
1828
|
+
</table>
|
|
1829
|
+
</div>
|
|
1830
|
+
|
|
1831
|
+
<div class="note">
|
|
1832
|
+
<p><strong>Every event listed above is emitted</strong>, and these beyond them:
|
|
1833
|
+
<code>config:changed</code>, <code>clipboard:copy</code>, <code>column:menu:open</code>,
|
|
1834
|
+
<code>column:filter:open</code>, <code>export:progress</code>, <code>toolpanel:focus</code>,
|
|
1835
|
+
<code>history:changed</code>, <code>state:reset</code>, <code>highlight:changed</code>,
|
|
1836
|
+
<code>render:done</code>, <code>views:changed</code>, the four <code>view:*</code> events,
|
|
1837
|
+
<code>permissions:changed</code> and <code>diff:changed</code>.</p>
|
|
1838
|
+
<p>Two carry a condition worth knowing. <code>row:edit:start</code> and
|
|
1839
|
+
<code>row:edit:end</code> replace the <code>cell:edit:*</code> pair only when
|
|
1840
|
+
<code>edit.mode</code> is <code>'row'</code> — you get one pair or the other, never both.
|
|
1841
|
+
And <code>page:changed</code> needs <code>pagination</code> configured, or a
|
|
1842
|
+
<code>grid.pagination.set()</code> call; without a page size there are no pages to change.</p>
|
|
1843
|
+
</div>
|
|
1844
|
+
|
|
1845
|
+
<h2 id="rules">Conditional formatting</h2>
|
|
1846
|
+
<p>Rules compile into the function <code>cell.style</code> already takes, so a compiled rule set
|
|
1847
|
+
installs exactly like a hand-written style function.</p>
|
|
1848
|
+
<pre><code><span class="kw">import</span> { compileRules } <span class="kw">from</span> '@toclocoinc/lattice-grid';
|
|
1849
|
+
|
|
1850
|
+
{ field: 'margin', cell: { style: compileRules([
|
|
1851
|
+
{ when: { op: 'lt', value: 0 }, style: { background: '#fdecea', colour: '#b91c1c' } },
|
|
1852
|
+
{ when: { op: 'between', value: 0, value2: 5 }, style: { background: '#fdf3e0' } },
|
|
1853
|
+
{ scale: { min: 0, max: 100, colours: ['#f8f9fa', '#1a6bc7'] } },
|
|
1854
|
+
]) } }</code></pre>
|
|
1855
|
+
<div class="table-wrap">
|
|
1856
|
+
<table>
|
|
1857
|
+
<thead><tr><th>Key</th><th>Description</th></tr></thead>
|
|
1858
|
+
<tbody>
|
|
1859
|
+
<tr><td class="sig">when</td><td class="desc"><code>{ op, value, value2 }</code>, using the same operators as filters: <code>eq</code>, <code>ne</code>, <code>gt</code>, <code>gte</code>, <code>lt</code>, <code>lte</code>, <code>between</code>, <code>outside</code>, <code>contains</code>, <code>notContains</code>, <code>startsWith</code>, <code>endsWith</code>, <code>blank</code>, <code>notBlank</code>, <code>true</code>, <code>false</code>.</td></tr>
|
|
1860
|
+
<tr><td class="sig">style</td><td class="desc">A style object, or a function of the cell params.</td></tr>
|
|
1861
|
+
<tr><td class="sig">scale</td><td class="desc"><code>{ min, max, colours }</code> — a colour scale. Two or more stops, reached evenly.</td></tr>
|
|
1862
|
+
<tr><td class="sig">stopIfTrue</td><td class="desc">Default <code>true</code>. <code>false</code> lets a later rule add to this one.</td></tr>
|
|
1863
|
+
<tr><td class="sig">enabled</td><td class="desc"><code>false</code> skips the rule without removing it.</td></tr>
|
|
1864
|
+
</tbody>
|
|
1865
|
+
</table>
|
|
1866
|
+
</div>
|
|
1867
|
+
<div class="note"><p>Rules are evaluated in order and the first match wins, as in a spreadsheet:
|
|
1868
|
+
"red if overdue, amber if due this week" reads top to bottom and stops. A blank cell satisfies no
|
|
1869
|
+
comparison, so an empty cell is not swept into "less than 100".</p></div>
|
|
1870
|
+
<div class="note"><p>A scale's <code>min</code> and <code>max</code> are required rather than
|
|
1871
|
+
derived from the data. A scale that rescaled as rows were filtered would change a cell's colour
|
|
1872
|
+
without its value changing.</p></div>
|
|
1873
|
+
|
|
1874
|
+
<h2 id="quickfilter">Quick filter modes</h2>
|
|
1875
|
+
<p>One box, four ways to match. The mode persists until changed, so a host sets it once and goes
|
|
1876
|
+
on passing text alone.</p>
|
|
1877
|
+
<pre><code>grid.filters.quick('acme london', { mode: 'words' });
|
|
1878
|
+
grid.filters.quickState(); <span class="cmt">// { text: 'acme london', mode: 'words' }</span></code></pre>
|
|
1879
|
+
<div class="table-wrap">
|
|
1880
|
+
<table>
|
|
1881
|
+
<thead><tr><th>Mode</th><th>Matches</th><th>Example</th></tr></thead>
|
|
1882
|
+
<tbody>
|
|
1883
|
+
<tr><td class="sig">contains</td><td>The text appears somewhere in the row. The default.</td><td class="desc"><code>cir</code> finds <code>CIR-100</code></td></tr>
|
|
1884
|
+
<tr><td class="sig">words</td><td>Every term appears, in any order and any column.</td><td class="desc"><code>acme london</code> finds a row with one in each</td></tr>
|
|
1885
|
+
<tr><td class="sig">fuzzy</td><td>The characters appear in order, not necessarily together.</td><td class="desc"><code>crc</code> finds <code>CIR-200 Manchester</code></td></tr>
|
|
1886
|
+
<tr><td class="sig">regex</td><td>A regular expression, case-insensitive.</td><td class="desc"><code>^CIR-[12]</code></td></tr>
|
|
1887
|
+
</tbody>
|
|
1888
|
+
</table>
|
|
1889
|
+
</div>
|
|
1890
|
+
<div class="note"><p>Matching is against one cached text blob per row, built from the columns the
|
|
1891
|
+
viewer is permitted to see. Hidden and unreadable columns are excluded, so the row count cannot
|
|
1892
|
+
become an oracle for a value behind them.</p></div>
|
|
1893
|
+
<div class="note"><p>An <strong>unfinished regular expression</strong> — <code>foo(</code> on the
|
|
1894
|
+
way to <code>foo(bar)</code> — falls back to a literal search rather than matching nothing, so the
|
|
1895
|
+
grid does not blank on every open bracket. <code>fuzzy</code> does not reorder rows: ranking
|
|
1896
|
+
results would fight the sort the user chose.</p></div>
|
|
1897
|
+
|
|
1898
|
+
<h2 id="charts">In-cell charts</h2>
|
|
1899
|
+
<p>Seven chart renderers for a cell. Each is a single SVG whose path data is the only thing a
|
|
1900
|
+
repaint writes, so they cost the same as any other cell as rows recycle.</p>
|
|
1901
|
+
<div class="table-wrap">
|
|
1902
|
+
<table>
|
|
1903
|
+
<thead><tr><th>Name</th><th>Shows</th><th>Reads</th></tr></thead>
|
|
1904
|
+
<tbody>
|
|
1905
|
+
<tr><td class="sig">line</td><td>Trend across a series.</td><td class="desc">An array</td></tr>
|
|
1906
|
+
<tr><td class="sig">area</td><td>Trend, with the area beneath filled.</td><td class="desc">An array</td></tr>
|
|
1907
|
+
<tr><td class="sig">column</td><td>A bar per point, drawn from zero.</td><td class="desc">An array</td></tr>
|
|
1908
|
+
<tr><td class="sig">winloss</td><td>One equal mark per point, up or down.</td><td class="desc">An array</td></tr>
|
|
1909
|
+
<tr><td class="sig">pie</td><td>How a set of numbers divides.</td><td class="desc">An array</td></tr>
|
|
1910
|
+
<tr><td class="sig">donut</td><td>The same, with a hole.</td><td class="desc">An array</td></tr>
|
|
1911
|
+
<tr><td class="sig">bullet</td><td>One measure against a target, over bands.</td><td class="desc">A number</td></tr>
|
|
1912
|
+
<tr><td class="sig">stacked</td><td>How one row's total divides, across the cell.</td><td class="desc">An array</td></tr>
|
|
1913
|
+
<tr><td class="sig">range</td><td>The span a set of values covers, middle marked.</td><td class="desc">An array</td></tr>
|
|
1914
|
+
<tr><td class="sig">gauge</td><td>One value as a dial.</td><td class="desc">A number</td></tr>
|
|
1915
|
+
<tr><td class="sig">delta</td><td>Direction and movement over a sampling interval.</td><td class="desc">A number</td></tr>
|
|
1916
|
+
</tbody>
|
|
1917
|
+
</table>
|
|
1918
|
+
</div>
|
|
1919
|
+
<pre><code>{ id: 'trend', field: 'readings', cell: 'line' }
|
|
1920
|
+
{ id: 'spend', field: 'monthly', cell: { render: 'column', props: { min: 0, max: 100 } } }
|
|
1921
|
+
{ id: 'mix', field: 'split', cell: { render: 'donut', props: { hole: 0.55 } } }
|
|
1922
|
+
{ id: 'sla', field: 'uptime',
|
|
1923
|
+
cell: { render: 'bullet', props: { target: 80, bands: [60, 85], max: 120 } } }
|
|
1924
|
+
|
|
1925
|
+
<span class="cmt">// When the series lives on another property than the cell's value.</span>
|
|
1926
|
+
{ id: 'trend', field: 'latest', cell: { render: 'line', props: { series: 'readings' } } }</code></pre>
|
|
1927
|
+
<div class="table-wrap">
|
|
1928
|
+
<table>
|
|
1929
|
+
<thead><tr><th>Prop</th><th>Applies to</th><th>Description</th></tr></thead>
|
|
1930
|
+
<tbody>
|
|
1931
|
+
<tr><td class="sig">series</td><td>sparklines</td><td class="desc">Property name holding the array, when it is not the cell's value.</td></tr>
|
|
1932
|
+
<tr><td class="sig">min / max</td><td>all</td><td class="desc">Pin the scale so several columns compare like for like.</td></tr>
|
|
1933
|
+
<tr><td class="sig">label</td><td>all</td><td class="desc"><code>false</code> hides the number beside the chart.</td></tr>
|
|
1934
|
+
<tr><td class="sig">marker</td><td>line, area</td><td class="desc"><code>false</code> hides the dot on the last point.</td></tr>
|
|
1935
|
+
<tr><td class="sig">hole</td><td>donut</td><td class="desc">Inner radius as a fraction, default 0.55.</td></tr>
|
|
1936
|
+
<tr><td class="sig">target</td><td>bullet</td><td class="desc">Draws the target marker.</td></tr>
|
|
1937
|
+
<tr><td class="sig">bands</td><td>bullet</td><td class="desc">Edges of the qualitative bands, e.g. <code>[60, 85]</code>.</td></tr>
|
|
1938
|
+
<tr><td class="sig">interval</td><td>delta</td><td class="desc">Milliseconds between samples. Default 1000.</td></tr>
|
|
1939
|
+
<tr><td class="sig">mode</td><td>delta</td><td class="desc"><code>'change'</code> (default) or <code>'against'</code>.</td></tr>
|
|
1940
|
+
<tr><td class="sig">against</td><td>delta</td><td class="desc">Property to compare with in <code>against</code> mode.</td></tr>
|
|
1941
|
+
<tr><td class="sig">show</td><td>delta</td><td class="desc"><code>'both'</code>, <code>'arrow'</code> or <code>'delta'</code>.</td></tr>
|
|
1942
|
+
</tbody>
|
|
1943
|
+
</table>
|
|
1944
|
+
</div>
|
|
1945
|
+
<div class="note"><p>Entries that are not numbers are gaps rather than zeroes: a line breaks
|
|
1946
|
+
across them and a bar is omitted. Pin <code>min</code> and <code>max</code> when comparing
|
|
1947
|
+
columns — a sparkline scaled to its own data fills its cell whatever the magnitude.</p></div>
|
|
1948
|
+
<div class="note"><p>The chart is <code>aria-hidden</code> and the cell carries a text summary,
|
|
1949
|
+
so a screen reader is told "12 points, 9 to 20, ending 18" rather than each value in turn.</p></div>
|
|
1950
|
+
|
|
1951
|
+
<h2 id="formulas">Formulas</h2>
|
|
1952
|
+
<p>A leading <code>=</code> in a numeric cell is a formula. The grid stores what it comes to.</p>
|
|
1953
|
+
<pre><code>=5 + 5
|
|
1954
|
+
=quantity * unitPrice
|
|
1955
|
+
=[Unit Price] * 1.2
|
|
1956
|
+
=ROUND(quantity * unitPrice, 2)
|
|
1957
|
+
=IF(quantity > 10, "bulk", "single")
|
|
1958
|
+
=SUM(readings) <span class="cmt">// an array property on the row</span></code></pre>
|
|
1959
|
+
|
|
1960
|
+
<p>References name <strong>columns of the same row</strong>, not cells — a grid sorts, filters,
|
|
1961
|
+
groups and pages, so <code>A1</code> would mean a different row from one moment to the next.
|
|
1962
|
+
Matching is on field or title, ignoring case and spacing; bracket a name that contains spaces.
|
|
1963
|
+
A property with no column of its own is reachable too.</p>
|
|
1964
|
+
|
|
1965
|
+
<div class="table-wrap">
|
|
1966
|
+
<table>
|
|
1967
|
+
<thead><tr><th>Group</th><th>Functions</th></tr></thead>
|
|
1968
|
+
<tbody>
|
|
1969
|
+
<tr><td class="sig">Maths</td><td><code>SUM</code>, <code>AVERAGE</code>/<code>AVG</code>, <code>MIN</code>, <code>MAX</code>, <code>COUNT</code>, <code>PRODUCT</code>, <code>ABS</code>, <code>SQRT</code>, <code>POWER</code>, <code>MOD</code></td></tr>
|
|
1970
|
+
<tr><td class="sig">Rounding</td><td><code>ROUND</code>, <code>ROUNDUP</code>, <code>ROUNDDOWN</code>, <code>FLOOR</code>, <code>CEILING</code></td></tr>
|
|
1971
|
+
<tr><td class="sig">Logic</td><td><code>IF</code>, <code>AND</code>, <code>OR</code>, <code>NOT</code>, <code>COALESCE</code></td></tr>
|
|
1972
|
+
<tr><td class="sig">Text</td><td><code>CONCAT</code>, <code>LEN</code>, <code>UPPER</code>, <code>LOWER</code>, <code>TRIM</code>, <code>LEFT</code>, <code>RIGHT</code></td></tr>
|
|
1973
|
+
</tbody>
|
|
1974
|
+
</table>
|
|
1975
|
+
</div>
|
|
1976
|
+
|
|
1977
|
+
<p>Operators <code>+ - * / ^</code> with parentheses, comparison for <code>IF</code>, and postfix
|
|
1978
|
+
<code>%</code>. <code>^</code> is right-associative and unary minus binds tighter than it, so
|
|
1979
|
+
<code>-2^2</code> is 4 — Excel's answer rather than mathematics'.</p>
|
|
1980
|
+
|
|
1981
|
+
<pre><code><span class="cmt">// Your own functions, on top of the built-in library.</span>
|
|
1982
|
+
createGrid(el, {
|
|
1983
|
+
formulaFunctions: {
|
|
1984
|
+
MARGIN: ([revenue, cost]) => (revenue - cost) / revenue,
|
|
1985
|
+
},
|
|
1986
|
+
});</code></pre>
|
|
1987
|
+
|
|
1988
|
+
<div class="note"><p>The <strong>result</strong> is stored, not the expression. A formula is a way
|
|
1989
|
+
of entering a value — like <code>1,200</code>, <code>(50)</code> or <code>12%</code> — and it
|
|
1990
|
+
commits as one undo step with the column's own validation. Persisting a formula and recalculating
|
|
1991
|
+
it when a dependency changes is a separate feature; <code>referencesOf()</code> is exported for
|
|
1992
|
+
anyone building it.</p></div>
|
|
1993
|
+
|
|
1994
|
+
<div class="note"><p><strong>No <code>eval</code>, no <code>new Function</code>.</strong> A formula
|
|
1995
|
+
is text a user typed, so evaluating it with the JavaScript engine would let anyone who can edit a
|
|
1996
|
+
cell run code in your page. It is a hand-written parser and the only callable things are the
|
|
1997
|
+
functions above.</p></div>
|
|
1998
|
+
|
|
1999
|
+
<div class="note"><p>Bare arithmetic is <strong>not</strong> a formula. <code>2-1</code> is a
|
|
2000
|
+
plausible product code and <code>1/2</code> a plausible date, so both are refused rather than
|
|
2001
|
+
guessed at. Declare a formula with <code>=</code>.</p></div>
|
|
2002
|
+
|
|
2003
|
+
<h2 id="custom-menu">Your own menu items and buttons</h2>
|
|
2004
|
+
<p>The cell menu's function form is handed the cell that was clicked <em>and</em> the built-in
|
|
2005
|
+
items, so adding one entry does not mean reproducing the other thirteen.</p>
|
|
2006
|
+
<pre><code>createGrid(el, {
|
|
2007
|
+
contextMenu: (params, defaults) => [
|
|
2008
|
+
...defaults,
|
|
2009
|
+
{ separator: <span class="kw">true</span> },
|
|
2010
|
+
{
|
|
2011
|
+
name: `Open ${params.value} in CRM`,
|
|
2012
|
+
action: (ctx) => open(`/crm/${ctx.data.accountId}`),
|
|
2013
|
+
},
|
|
2014
|
+
],
|
|
2015
|
+
});</code></pre>
|
|
2016
|
+
<p><code>params</code> and the <code>action</code>'s argument carry the same cell:
|
|
2017
|
+
<code>{ key, colId, value, row, data, column, index, grid }</code>, where <code>data</code> is
|
|
2018
|
+
your original row object. Return the array you want shown — add, remove, reorder or replace.
|
|
2019
|
+
Returning an empty array suppresses the menu; returning nothing at all leaves the defaults
|
|
2020
|
+
alone, so a missing <code>return</code> cannot silently delete the menu.</p>
|
|
2021
|
+
|
|
2022
|
+
<p>The rail takes host buttons the same way. A string names a built-in and an object is yours,
|
|
2023
|
+
placed where it appears in the list rather than appended after the built-ins.</p>
|
|
2024
|
+
<pre><code>createGrid(el, {
|
|
2025
|
+
toolPanel: {
|
|
2026
|
+
side: 'left',
|
|
2027
|
+
actions: ['undo', 'redo', {
|
|
2028
|
+
name: 'sync',
|
|
2029
|
+
title: 'Sync to the server', <span class="cmt">// or a function, re-read on every repaint</span>
|
|
2030
|
+
icon: 'restore',
|
|
2031
|
+
run: ({ grid, keys, cells }) => api.sync(keys),
|
|
2032
|
+
enabled: () => grid.history.canUndo(),
|
|
2033
|
+
}],
|
|
2034
|
+
},
|
|
2035
|
+
});</code></pre>
|
|
2036
|
+
|
|
2037
|
+
<h2 id="styling">Styling and your page's CSS</h2>
|
|
2038
|
+
<p><strong>Forced colours.</strong> In Windows High Contrast Mode the grid translates state that
|
|
2039
|
+
is normally a background tint into borders and system colours: selection takes the system's own
|
|
2040
|
+
selection colours, pinned regions swap their shadow for a rule, and diff states are told apart by
|
|
2041
|
+
border style rather than by hue. A colour swatch and a collaborator's presence colour keep their
|
|
2042
|
+
own colour, because there the colour is the information.</p>
|
|
2043
|
+
<p>Every selector is namespaced under <code>.lattice</code>, so the grid cannot restyle your
|
|
2044
|
+
page. Since 1.4.0 the reverse holds too: the elements the grid builds are given a floor for the
|
|
2045
|
+
properties a host page commonly sets on a bare tag — margin, padding, border, radius,
|
|
2046
|
+
background, shadow, text transform, letter spacing, and type and colour on form controls. A rule
|
|
2047
|
+
such as <code>section { padding: 5.5rem 0 }</code> no longer reaches inside the grid.</p>
|
|
2048
|
+
<p>No <code>!important</code> is involved. The reset is specificity (0,1,1); every rule that
|
|
2049
|
+
dresses a grid element is (0,2,0) or higher, and so is any rule of yours aimed at a Lattice
|
|
2050
|
+
class. Deliberate overrides work exactly as before — only bare-tag rules are shut out. The reset
|
|
2051
|
+
covers box model and decoration only, never <code>display</code>, <code>position</code> or any
|
|
2052
|
+
dimension.</p>
|
|
2053
|
+
|
|
2054
|
+
<h2 id="accessibility">Accessibility</h2>
|
|
2055
|
+
<p>Built to WCAG 2.2 level AA. Every operation is reachable without a pointer, including resizing
|
|
2056
|
+
and reordering a column, which have key bindings and column-menu items rather than depending on a
|
|
2057
|
+
drag. The grid reports itself as <code>grid</code> or <code>treegrid</code> following its
|
|
2058
|
+
configuration; rows and cells carry their position in the dataset rather than in the rendered
|
|
2059
|
+
window, so a reader on row 500,000 is told so; and rows in a hierarchy carry their position among
|
|
2060
|
+
their siblings, which a reader cannot count for itself when most of a branch was never rendered.</p>
|
|
2061
|
+
<p>Focus is real focus rather than <code>aria-activedescendant</code>, and survives row recycling.
|
|
2062
|
+
Sorting, filtering, selection, grouping, expanding, paging, undo, paste and a refused edit are all
|
|
2063
|
+
announced. In Windows High Contrast Mode state is translated into borders and system colours
|
|
2064
|
+
instead of tints. No information is carried by hue alone.</p>
|
|
2065
|
+
<p>The full keyboard map, the screen reader support statement and the known limits —
|
|
2066
|
+
including the drag-only pivot zones — are in
|
|
2067
|
+
<a href="api-detail.html#accessibility-guide">the guide</a>.</p>
|
|
2068
|
+
|
|
2069
|
+
<h2 id="names">Built-in names</h2>
|
|
2070
|
+
<p class="section-note">Every registry accepts a custom entry under the same name, which then wins over ours.</p>
|
|
2071
|
+
|
|
2072
|
+
<h3>Data types</h3>
|
|
2073
|
+
<p>Seven built-in, and twenty in the extended catalogue. Inference only ever reaches the built-in
|
|
2074
|
+
names: the candidates are tried in registration order, and every string settles on
|
|
2075
|
+
<code>text</code> and every number on <code>number</code> before an extended type is reached. So a
|
|
2076
|
+
column asks for an extended type by name.</p>
|
|
2077
|
+
<div class="chips">
|
|
2078
|
+
<span class="chip">text</span><span class="chip">number</span><span class="chip">boolean</span><span class="chip">date</span><span class="chip">dateString</span><span class="chip">lookup</span><span class="chip">object</span>
|
|
2079
|
+
</div>
|
|
2080
|
+
<div class="chips">
|
|
2081
|
+
<span class="chip chip--new">time</span><span class="chip chip--new">datetime</span><span class="chip chip--new">duration</span>
|
|
2082
|
+
<span class="chip chip--new">ipv4</span><span class="chip chip--new">cidr</span><span class="chip chip--new">ipv6</span>
|
|
2083
|
+
<span class="chip chip--new">json</span><span class="chip chip--new">secret</span>
|
|
2084
|
+
<span class="chip chip--new">hex</span><span class="chip chip--new">hex8</span><span class="chip chip--new">hex16</span><span class="chip chip--new">hex32</span>
|
|
2085
|
+
<span class="chip chip--new">binary</span><span class="chip chip--new">binary8</span><span class="chip chip--new">octal</span>
|
|
2086
|
+
<span class="chip chip--new">bytes</span><span class="chip chip--new">megabytes</span><span class="chip chip--new">gigabytes</span>
|
|
2087
|
+
<span class="chip chip--new">bitrate</span><span class="chip chip--new">gigabits</span>
|
|
2088
|
+
</div>
|
|
2089
|
+
|
|
2090
|
+
<h3>Editors</h3>
|
|
2091
|
+
<div class="chips">
|
|
2092
|
+
<span class="chip">text</span><span class="chip">textarea</span><span class="chip">number</span><span class="chip">date</span><span class="chip">checkbox</span><span class="chip">select</span><span class="chip">multiSelect</span>
|
|
2093
|
+
<span class="chip">time</span><span class="chip">datetime</span><span class="chip">duration</span><span class="chip">ipaddress</span><span class="chip">password</span><span class="chip">code</span>
|
|
2094
|
+
<span class="chip">unit</span><span class="chip">radix</span><span class="chip">slider</span><span class="chip">rating</span><span class="chip">segmented</span>
|
|
2095
|
+
<span class="chip">treeSelect</span><span class="chip">objectPicker</span><span class="chip">iconPicker</span><span class="chip">colour</span>
|
|
2096
|
+
</div>
|
|
2097
|
+
|
|
2098
|
+
<h3>Cell renderers</h3>
|
|
2099
|
+
<div class="chips">
|
|
2100
|
+
<span class="chip">group</span><span class="chip">checkbox</span><span class="chip">progress</span><span class="chip">link</span><span class="chip">pill</span><span class="chip">icon</span><span class="chip">skeleton</span><span class="chip">rating</span><span class="chip">colour</span><span class="chip">qrcode</span>
|
|
2101
|
+
</div>
|
|
2102
|
+
|
|
2103
|
+
<h3>Filters and aggregations</h3>
|
|
2104
|
+
<div class="chips">
|
|
2105
|
+
<span class="chip">text</span><span class="chip">number</span><span class="chip">date</span><span class="chip">set</span><span class="chip">multi</span><span class="chip">advanced</span>
|
|
2106
|
+
</div>
|
|
2107
|
+
<div class="chips">
|
|
2108
|
+
<span class="chip">sum</span><span class="chip">min</span><span class="chip">max</span><span class="chip">avg</span><span class="chip">count</span><span class="chip">countValues</span><span class="chip">first</span><span class="chip">last</span>
|
|
2109
|
+
</div>
|
|
2110
|
+
|
|
2111
|
+
<h3>Icons</h3>
|
|
2112
|
+
<p>Inline SVG sprites, overridable by name through <code>registerIcon(name, def)</code>.</p>
|
|
2113
|
+
<div class="chips">
|
|
2114
|
+
<span class="chip">chevronRight</span><span class="chip">chevronDown</span><span class="chip">chevronUp</span><span class="chip">chevronLeft</span>
|
|
2115
|
+
<span class="chip">check</span><span class="chip">dash</span><span class="chip">close</span><span class="chip">plus</span><span class="chip">minus</span>
|
|
2116
|
+
<span class="chip">info</span><span class="chip">success</span><span class="chip">warning</span><span class="chip">danger</span>
|
|
2117
|
+
<span class="chip">clock</span><span class="chip">lock</span><span class="chip">link</span><span class="chip">external</span><span class="chip">filter</span>
|
|
2118
|
+
<span class="chip">sortAsc</span><span class="chip">sortDesc</span><span class="chip">menu</span><span class="chip">drag</span>
|
|
2119
|
+
<span class="chip">star</span><span class="chip">heart</span><span class="chip">circleFilled</span><span class="chip">square</span><span class="chip">bolt</span><span class="chip">flag</span><span class="chip">thumbUp</span>
|
|
2120
|
+
<span class="chip">eye</span><span class="chip">eyeOff</span><span class="chip">copy</span><span class="chip">blank</span>
|
|
2121
|
+
</div>
|
|
2122
|
+
|
|
2123
|
+
<h2 id="operators">Filter grammar</h2>
|
|
2124
|
+
<p class="section-note">
|
|
2125
|
+
The condition tree is a published wire protocol, not an internal shape. It serialises into
|
|
2126
|
+
saved state and travels to a remote source unchanged.
|
|
2127
|
+
</p>
|
|
2128
|
+
|
|
2129
|
+
<pre><code>grid.filters.set({
|
|
2130
|
+
op: <span class="kw">'and'</span>,
|
|
2131
|
+
conditions: [
|
|
2132
|
+
{ col: <span class="kw">'region'</span>, op: <span class="kw">'eq'</span>, value: <span class="kw">'EMEA'</span> },
|
|
2133
|
+
{ col: <span class="kw">'capacity'</span>, op: <span class="kw">'between'</span>, value: [100, 500], bounds: <span class="kw">'[)'</span> },
|
|
2134
|
+
{ op: <span class="kw">'not'</span>, conditions: [
|
|
2135
|
+
{ col: <span class="kw">'status'</span>, op: <span class="kw">'in'</span>, value: [<span class="kw">'closed'</span>] },
|
|
2136
|
+
] },
|
|
2137
|
+
],
|
|
2138
|
+
});</code></pre>
|
|
2139
|
+
|
|
2140
|
+
<div class="table-wrap">
|
|
2141
|
+
<table>
|
|
2142
|
+
<thead><tr><th>Group</th><th>Operators</th></tr></thead>
|
|
2143
|
+
<tbody>
|
|
2144
|
+
<tr><td class="name">Equality</td><td class="desc"><code>eq</code>, <code>ne</code></td></tr>
|
|
2145
|
+
<tr><td class="name">Ordering</td><td class="desc"><code>lt</code>, <code>lte</code>, <code>gt</code>, <code>gte</code></td></tr>
|
|
2146
|
+
<tr><td class="name">Ranges</td><td class="desc"><code>between</code>, <code>notBetween</code> — with <code>bounds</code> of <code>'[]'</code>, <code>'[)'</code>, <code>'(]'</code> or <code>'()'</code></td></tr>
|
|
2147
|
+
<tr><td class="name">Sets</td><td class="desc"><code>in</code>, <code>notIn</code></td></tr>
|
|
2148
|
+
<tr><td class="name">Text</td><td class="desc"><code>contains</code>, <code>notContains</code>, <code>startsWith</code>, <code>endsWith</code>, <code>matches</code></td></tr>
|
|
2149
|
+
<tr><td class="name">Blankness</td><td class="desc"><code>blank</code>, <code>notBlank</code></td></tr>
|
|
2150
|
+
<tr><td class="name">Multi-value</td><td class="desc"><code>containsAny</code>, <code>containsAll</code>, <code>containsNone</code> — for cells holding an array of ids</td></tr>
|
|
2151
|
+
<tr><td class="name">Grouping</td><td class="desc"><code>and</code>, <code>or</code>, <code>not</code></td></tr>
|
|
2152
|
+
</tbody>
|
|
2153
|
+
</table>
|
|
2154
|
+
</div>
|
|
2155
|
+
|
|
2156
|
+
<div class="note">
|
|
2157
|
+
<p><strong>One filter, not two.</strong> A condition set from a header popup, from the tool panel, or through <code>grid.filters.set()</code> all merge into the same tree. Reading <code>grid.filters.get()</code> always gives the whole truth.</p>
|
|
2158
|
+
</div>
|
|
2159
|
+
|
|
2160
|
+
<footer>
|
|
2161
|
+
Lattice Grid 1.5.4 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
2162
|
+
This document describes the behaviour of the shipped library. Where this guide and the code
|
|
2163
|
+
disagree, the code wins — please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
2164
|
+
</footer>
|
|
2165
|
+
</main>
|
|
2166
|
+
</div>
|
|
2167
|
+
</body>
|
|
2168
|
+
</html>
|