@toclocoinc/lattice-grid 1.11.0 → 1.12.0
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/README.md +34 -34
- package/docs/AI-SKILL.md +22 -22
- package/docs/API.html +2910 -247
- package/docs/CHART-CODES.md +60 -60
- package/docs/api-detail.html +365 -365
- package/lattice-grid.d.ts +345 -73
- package/lattice-grid.esm.min.js +493 -58
- package/lattice-grid.min.cjs +493 -58
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +493 -58
- package/modules/charts.esm.min.js +308 -18
- package/modules/devtools.esm.min.js +8 -8
- package/modules/dhtmlx-compat.esm.min.js +499 -64
- package/modules/htmx.esm.min.js +460 -59
- package/modules/htmx.min.cjs +460 -59
- package/modules/htmx.min.js +460 -59
- 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 +493 -58
- package/package.json +1 -1
package/docs/api-detail.html
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<meta name="description" content="Lattice Grid
|
|
6
|
+
<meta name="description" content="Lattice Grid, a high-performance JavaScript data grid with no dependencies and no build step. What every part of the API does, and why.">
|
|
7
7
|
<!--
|
|
8
|
-
Lattice Grid
|
|
8
|
+
Lattice Grid: developer guide.
|
|
9
9
|
Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
10
10
|
|
|
11
11
|
Self-contained: no stylesheet, script or font is fetched, so this opens from
|
|
12
12
|
disk, from a file share or from behind a firewall with nothing else present.
|
|
13
13
|
API.html is the reference; this is the explanation.
|
|
14
14
|
-->
|
|
15
|
-
<title>Lattice Grid
|
|
15
|
+
<title>Lattice Grid: Developer Guide</title>
|
|
16
16
|
<style>
|
|
17
17
|
/* ---------------------------------------------------------------
|
|
18
18
|
Palette lifted from the grid's own theme, so the reference reads as an
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
|
|
211
211
|
a { color: var(--accent); }
|
|
212
212
|
|
|
213
|
-
/* Inline identifiers
|
|
213
|
+
/* Inline identifiers, the primary content of the whole document. */
|
|
214
214
|
code {
|
|
215
215
|
font-family: var(--mono);
|
|
216
216
|
font-size: 0.885em;
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
.kw { color: var(--accent); }
|
|
239
239
|
|
|
240
240
|
/* ---------------------------------------------------------------
|
|
241
|
-
Reference tables. Dense, hairline, sticky-headed
|
|
241
|
+
Reference tables. Dense, hairline, sticky-headed, the grid's own
|
|
242
242
|
visual language, and the right density for scanning an API.
|
|
243
243
|
--------------------------------------------------------------- */
|
|
244
244
|
.table-wrap {
|
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
/* Why a thing is the way it is. Distinct from .note, which is a
|
|
390
|
-
caveat
|
|
390
|
+
caveat: this is the reasoning, and it is the part a developer
|
|
391
391
|
evaluating the product actually reads. */
|
|
392
392
|
.why {
|
|
393
393
|
/* Constrained to the reading measure plus its own padding. Left to fill
|
|
@@ -553,7 +553,7 @@ createGrid(element, { locale: 'fr-FR', messages: FR_FR });</code></pre>
|
|
|
553
553
|
|
|
554
554
|
<p>Where <code>locale</code> is not set, the grid takes the language the page declares in its <code>lang</code> attribute. Overrides merge over the default, so translating part of the interface leaves the remainder in English rather than showing raw keys, and a key that is not in the catalogue is ignored with a warning.</p>
|
|
555
555
|
|
|
556
|
-
<p>Catalogues ship for twenty-one locales: British and American English, French (France and Canada), Italian, Spanish, Brazilian Portuguese, German, Dutch, Swedish, Danish, Norwegian, Finnish, Polish, Czech, Hungarian, Romanian, Ukrainian, Greek, Japanese and Arabic. Each is an export of the package, so importing one does not reduce what is bundled. <code>EN_US</code> is a partial overlay carrying only what differs from British English and merging over it. <code>AR_SA</code> is an alias for <code>AR</code> rather than a separate catalogue
|
|
556
|
+
<p>Catalogues ship for twenty-one locales: British and American English, French (France and Canada), Italian, Spanish, Brazilian Portuguese, German, Dutch, Swedish, Danish, Norwegian, Finnish, Polish, Czech, Hungarian, Romanian, Ukrainian, Greek, Japanese and Arabic. Each is an export of the package, so importing one does not reduce what is bundled. <code>EN_US</code> is a partial overlay carrying only what differs from British English and merging over it. <code>AR_SA</code> is an alias for <code>AR</code> rather than a separate catalogue: Arabic ships pan-Arabic, and a region appears in a name only where two variants exist. For anything else, <code>resolveCatalogue(tag)</code> resolves a tag to a catalogue and falls back to the base language, so <code>ar-EG</code> and <code>es-MX</code> both find one. <code>FR_CA</code> is a complete catalogue that follows Quebec usage where it differs, a column there is <em>figée</em> rather than <em>épinglée</em>.</p>
|
|
557
557
|
|
|
558
558
|
<p class="section-note">None of the translations has been reviewed by a native speaker. They are structurally complete and checked for placeholder integrity and plural coverage, but they are a starting point for review rather than finished copy.</p>
|
|
559
559
|
|
|
@@ -565,17 +565,17 @@ createGrid(element, { locale: 'fr-FR', messages: FR_FR });</code></pre>
|
|
|
565
565
|
'count.rows': { one: '{count} rad', other: '{count} rader' },
|
|
566
566
|
}</code></pre>
|
|
567
567
|
|
|
568
|
-
<p>Placeholders are named, so word order is yours to choose. The parameter called <code>count</code> selects the plural form, using the categories the language actually has
|
|
568
|
+
<p>Placeholders are named, so word order is yours to choose. The parameter called <code>count</code> selects the plural form, using the categories the language actually has: English needs two, French treats zero as singular, Arabic has six. Numbers are formatted for the locale automatically; do not format them yourself.</p>
|
|
569
569
|
|
|
570
570
|
<p><code>MESSAGE_KEYS</code> lists every key. <code>auditCatalogue(yours)</code> returns what is missing and what is not a real key, which is the quickest way to check a translation before shipping it.</p>
|
|
571
571
|
|
|
572
572
|
<div class="why">
|
|
573
|
-
<p><strong>The grid is checked in the other direction too.</strong> <code>auditCatalogue</code> tells you a catalogue is complete
|
|
573
|
+
<p><strong>The grid is checked in the other direction too.</strong> <code>auditCatalogue</code> tells you a catalogue is complete: that a translator covered every key. It cannot tell you the grid only ever renders text that came from a catalogue in the first place, and a string written into the source passes every test, because the tests assert on the English the grid happens to produce.</p>
|
|
574
574
|
<p>So the build refuses one. Any literal reaching an element's text, or an announced attribute such as <code>aria-label</code>, <code>title</code> or <code>placeholder</code>, has to come from the catalogue. That is what stops a localised grid drifting back into English one plausible change at a time.</p>
|
|
575
575
|
</div>
|
|
576
576
|
|
|
577
577
|
<h3>Right-to-left</h3>
|
|
578
|
-
<p>The grid lays out right to left. Set <code>direction: 'rtl'</code> outright, or leave it unset and it follows the element's computed <code>dir</code> first and the locale second
|
|
578
|
+
<p>The grid lays out right to left. Set <code>direction: 'rtl'</code> outright, or leave it unset and it follows the element's computed <code>dir</code> first and the locale second, so <code>locale: 'ar'</code> renders right to left with no further configuration, and a grid inside a page that has already declared <code>dir="rtl"</code> agrees with it.</p>
|
|
579
579
|
|
|
580
580
|
<pre><code>createGrid(element, { locale: 'ar' }); <span class="cmt">// direction follows the locale</span>
|
|
581
581
|
createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so outright</span></code></pre>
|
|
@@ -584,7 +584,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
584
584
|
|
|
585
585
|
<p>Pinned columns swap sides, the header and pinned rows follow the scroll the other way, and column resize and reorder, the fill handle, annotations, the facet band, the comment marker and menu placement all follow the writing direction rather than the physical one.</p>
|
|
586
586
|
|
|
587
|
-
<p><strong>Not yet exercised with bidirectional text.</strong> The layout is verified, but only with Latin text in a right-to-left grid. Cells, headings and editors holding actual Arabic or Hebrew
|
|
587
|
+
<p><strong>Not yet exercised with bidirectional text.</strong> The layout is verified, but only with Latin text in a right-to-left grid. Cells, headings and editors holding actual Arabic or Hebrew (particularly mixed with Latin text or numbers) have not been tested.</p>
|
|
588
588
|
|
|
589
589
|
<h2 id="what">What it is</h2>
|
|
590
590
|
<p class="lead-in">
|
|
@@ -596,7 +596,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
596
596
|
<p class="lead-in">
|
|
597
597
|
A grid of a hundred thousand rows and thirty columns is three million values. Held as row
|
|
598
598
|
objects, that is three million property lookups per pass and a great deal of memory the
|
|
599
|
-
garbage collector has opinions about. Lattice stores each column as one typed array
|
|
599
|
+
garbage collector has opinions about. Lattice stores each column as one typed array: numbers
|
|
600
600
|
in a <code>Float64Array</code>, repeated strings as integers into a dictionary, booleans as
|
|
601
601
|
bits in a bitset.
|
|
602
602
|
</p>
|
|
@@ -614,7 +614,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
614
614
|
flatten. Each remembers its result and the inputs it was computed from.
|
|
615
615
|
</p>
|
|
616
616
|
<div class="why">
|
|
617
|
-
<p>Change a sort and the filter stage is not recomputed
|
|
617
|
+
<p>Change a sort and the filter stage is not recomputed: its inputs did not change. Edit a
|
|
618
618
|
cell in a column nobody sorts, filters or groups on and <em>none</em> of the first five run;
|
|
619
619
|
only the totals move. This is why a grid that is heavily filtered and grouped still feels
|
|
620
620
|
immediate when you type in a cell.</p>
|
|
@@ -629,7 +629,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
629
629
|
|
|
630
630
|
<h3>No dependencies, and no build step</h3>
|
|
631
631
|
<p class="lead-in">
|
|
632
|
-
Not "few dependencies"
|
|
632
|
+
Not "few dependencies": none, at runtime and at build time. No lodash, no date library, no
|
|
633
633
|
virtualisation library, no icon font. The bundler and minifier that produce the distribution
|
|
634
634
|
are part of the repository. You can drop two files into a page and be finished.
|
|
635
635
|
</p>
|
|
@@ -640,7 +640,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
640
640
|
</div>
|
|
641
641
|
|
|
642
642
|
<h2 id="install">Install</h2>
|
|
643
|
-
<p class="lead-in">Two files. Nothing is fetched at runtime
|
|
643
|
+
<p class="lead-in">Two files. Nothing is fetched at runtime (no CDN, no font, no sprite sheet) however the two files themselves got onto the page. Four equally valid ways to get them there:</p>
|
|
644
644
|
|
|
645
645
|
<div class="example">
|
|
646
646
|
<p class="example__label">npm</p>
|
|
@@ -651,7 +651,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
651
651
|
</div>
|
|
652
652
|
|
|
653
653
|
<div class="example">
|
|
654
|
-
<p class="example__label">jsDelivr
|
|
654
|
+
<p class="example__label">jsDelivr, no npm install, no bundler</p>
|
|
655
655
|
<pre><code><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.7.1/lattice-grid.min.css">
|
|
656
656
|
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.7.1/lattice-grid.min.js"></script>
|
|
657
657
|
|
|
@@ -678,7 +678,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
678
678
|
|
|
679
679
|
<div class="note">
|
|
680
680
|
<p><strong>jsDelivr mirrors every version published to npm</strong> at
|
|
681
|
-
<code>cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@<version>/<file></code
|
|
681
|
+
<code>cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@<version>/<file></code>: pin an
|
|
682
682
|
exact version, e.g. <code>@1.7.1</code> rather than <code>@latest</code>, so a later release
|
|
683
683
|
does not change what a page already in production loads. The same convention reaches a
|
|
684
684
|
module: <code>.../modules/htmx.esm.min.js</code>, <code>.../modules/dhtmlx-compat.esm.min.js</code>,
|
|
@@ -694,9 +694,9 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
694
694
|
<thead><tr><th>File</th><th>Gzipped</th><th>What it is</th></tr></thead>
|
|
695
695
|
<tbody>
|
|
696
696
|
<tr><td class="sig">lattice-grid.min.js</td><td class="desc">The whole product as a UMD build. Defines <code>window.LatticeGrid</code>, and works with AMD and CommonJS loaders.</td></tr>
|
|
697
|
-
<tr><td class="sig">lattice-grid.min.css</td><td class="desc">The single stylesheet. Without it the grid is in the DOM and unreadable
|
|
697
|
+
<tr><td class="sig">lattice-grid.min.css</td><td class="desc">The single stylesheet. Without it the grid is in the DOM and unreadable, no widths, no scrolling, no theme.</td></tr>
|
|
698
698
|
<tr><td class="sig">lattice-grid.esm.min.js</td><td class="desc">The same, as an ES module.</td></tr>
|
|
699
|
-
<tr><td class="sig">lattice-core.esm.js</td><td class="desc">Headless core for Node
|
|
699
|
+
<tr><td class="sig">lattice-core.esm.js</td><td class="desc">Headless core for Node, no renderer. See <a href="#export-guide">server-side export</a>.</td></tr>
|
|
700
700
|
<tr><td class="sig">lattice-grid.d.ts</td><td class="desc">Type declarations.</td></tr>
|
|
701
701
|
</tbody>
|
|
702
702
|
</table>
|
|
@@ -724,7 +724,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
724
724
|
|
|
725
725
|
<p class="lead-in">
|
|
726
726
|
A few things happened there without being asked for. <code>region</code> got a title of
|
|
727
|
-
"Region"
|
|
727
|
+
"Region", a field name is turned into a readable heading rather than left as-is. The number
|
|
728
728
|
column right-aligned itself, because numbers align right and a grid should not need telling.
|
|
729
729
|
The date column parsed <code>2024-03-11</code> and rendered <code>11 Mar 2024</code>. And
|
|
730
730
|
<code>total: 'sum'</code> put a figure in the totals row.
|
|
@@ -733,7 +733,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
733
733
|
<div class="why">
|
|
734
734
|
<p><strong>On <code>rowKey</code>:</strong> it names the field that identifies a row. Set it
|
|
735
735
|
if you have one. Without it the grid assigns keys per row object, which is enough for
|
|
736
|
-
sorting, filtering, selection and copying within a session
|
|
736
|
+
sorting, filtering, selection and copying within a session, but not across a reload, because
|
|
737
737
|
new objects are new rows. Change tracking, streaming dedupe, selection persistence and remote
|
|
738
738
|
reload all want a real key, and the grid warns once, naming them.</p>
|
|
739
739
|
</div>
|
|
@@ -785,7 +785,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
785
785
|
deep-comparing a million-row array on every render would cost more than the reload it
|
|
786
786
|
avoids. Build <code>columns</code> once outside the component, or memoise it; a fresh array
|
|
787
787
|
literal on each render tells the grid the columns changed and it will rebuild them. Rows are
|
|
788
|
-
the same
|
|
788
|
+
the same: hand back a new array when the data actually changes, not before.</p>
|
|
789
789
|
<p>StrictMode is handled. React 18 deliberately mounts, unmounts and mounts again in
|
|
790
790
|
development; the effect cleanup destroys the first grid, so the second starts clean and
|
|
791
791
|
nothing leaks.</p>
|
|
@@ -808,8 +808,8 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
808
808
|
/></code></pre>
|
|
809
809
|
</div>
|
|
810
810
|
<div class="why">
|
|
811
|
-
<p>Events are re-emitted under dashed names
|
|
812
|
-
<code>@cell-changed</code
|
|
811
|
+
<p>Events are re-emitted under dashed names: <code>cell:changed</code> becomes
|
|
812
|
+
<code>@cell-changed</code>, because a colon in a Vue template is directive syntax and
|
|
813
813
|
cannot be bound. Every event in the table below is declared in <code>emits</code>.</p>
|
|
814
814
|
</div>
|
|
815
815
|
|
|
@@ -840,20 +840,20 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
840
840
|
</div>
|
|
841
841
|
|
|
842
842
|
<div class="why">
|
|
843
|
-
<p><strong>What the adapters do not do.</strong> They add no features and wrap no API
|
|
843
|
+
<p><strong>What the adapters do not do.</strong> They add no features and wrap no API: the
|
|
844
844
|
grid instance is the same object the vanilla examples use, and anything without a prop is
|
|
845
845
|
reached through it directly (via the ref in React, <code>expose</code> in Vue, or a
|
|
846
846
|
reference you keep in Svelte). Nothing is proxied, so nothing can lag behind the grid.</p>
|
|
847
847
|
</div>
|
|
848
848
|
|
|
849
|
-
<h3>The web component is self-contained
|
|
849
|
+
<h3>The web component is self-contained: use it or the API, not both</h3>
|
|
850
850
|
<p class="lead-in">
|
|
851
851
|
The custom element carries the grid inside it, rather than being handed one. That is the point
|
|
852
852
|
of the format: you add the element and it works, with nothing to wire up.
|
|
853
853
|
</p>
|
|
854
854
|
<div class="why">
|
|
855
855
|
<p><strong>Do not load it alongside <code>createGrid</code> in the same page.</strong> You would
|
|
856
|
-
get two independent copies of the grid, and the cost is not the download
|
|
856
|
+
get two independent copies of the grid, and the cost is not the download, it is that
|
|
857
857
|
each copy keeps its own registries. A renderer, editor, data type or variant registered
|
|
858
858
|
through one is invisible to the other, and a licence key validated in one is not validated in
|
|
859
859
|
the other. Nothing errors; the custom renderer you registered simply never appears.</p>
|
|
@@ -865,9 +865,9 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
865
865
|
<h2 id="dhtmlx-guide">Coming from dhtmlx Grid</h2>
|
|
866
866
|
<p class="lead-in">
|
|
867
867
|
<code>lattice-grid/modules/dhtmlx-compat</code> exposes a <code>Grid</code> class shaped
|
|
868
|
-
like dhtmlx's own <code>dhx.Grid</code
|
|
868
|
+
like dhtmlx's own <code>dhx.Grid</code>, the same constructor call, the same
|
|
869
869
|
<code>.data</code>, <code>.selection</code>, <code>.history</code>, <code>.export</code> and
|
|
870
|
-
<code>.events</code> namespaces
|
|
870
|
+
<code>.events</code> namespaces: sitting on top of a real Lattice grid underneath.
|
|
871
871
|
Swap the import and, for the surface below, the calling code does not change.
|
|
872
872
|
</p>
|
|
873
873
|
|
|
@@ -884,7 +884,7 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
|
|
|
884
884
|
});
|
|
885
885
|
|
|
886
886
|
grid.events.on('cellClick', (row, column, event) => {
|
|
887
|
-
<span class="cmt">// row and column carry .id, row's own fields sit alongside it
|
|
887
|
+
<span class="cmt">// row and column carry .id, row's own fields sit alongside it , </span>
|
|
888
888
|
<span class="cmt">// the same shape dhtmlx's own IRow/ICol declare.</span>
|
|
889
889
|
});</code></pre>
|
|
890
890
|
</div>
|
|
@@ -908,19 +908,19 @@ grid.events.on('cellClick', (row, column, event) => {
|
|
|
908
908
|
<code>afterColumnHide</code>, <code>afterColumnShow</code>, <code>afterExpand</code>,
|
|
909
909
|
<code>afterCollapse</code>, <code>afterSelect</code>, <code>afterUnSelect</code>,
|
|
910
910
|
<code>afterCopy</code>, <code>afterRowDrop</code> and <code>scroll</code>. Grid-level
|
|
911
|
-
<code>dragItem: 'row'</code> becomes <code>rowReorder: true</code
|
|
911
|
+
<code>dragItem: 'row'</code> becomes <code>rowReorder: true</code>: same-grid
|
|
912
912
|
drag-to-reorder.</p>
|
|
913
913
|
<p><code>cellClick</code>, <code>cellDblClick</code>, <code>cellRightClick</code>,
|
|
914
914
|
<code>afterEditStart</code>, <code>afterEditEnd</code> and <code>afterSort</code> call your
|
|
915
|
-
handler with dhtmlx's own positional arguments
|
|
916
|
-
not Lattice's own event object
|
|
915
|
+
handler with dhtmlx's own positional arguments: <code>(row, column, event)</code>,
|
|
916
|
+
not Lattice's own event object, because that is dhtmlx's own documented signature for
|
|
917
917
|
them. <code>afterRowDrop</code> calls your handler with <code>(data, event)</code>, firing
|
|
918
|
-
from either a same-grid reorder settling or a row landing here from another grid
|
|
918
|
+
from either a same-grid reorder settling or a row landing here from another grid ,
|
|
919
919
|
dhtmlx has one event name for what Lattice models as two. Every other mapped event calls your
|
|
920
920
|
handler with Lattice's own event object, under Lattice's own field names, since a wrong guess
|
|
921
921
|
at a fabricated positional shape is worse than an honest one.</p>
|
|
922
|
-
<p><strong>Index means current display order</strong
|
|
923
|
-
grouping
|
|
922
|
+
<p><strong>Index means current display order</strong>: after sort, filter and
|
|
923
|
+
grouping: everywhere <code>.data</code> takes or returns one. dhtmlx is not fully
|
|
924
924
|
consistent about this across its own methods; this is the one meaning, held everywhere.</p>
|
|
925
925
|
<p><strong>Cross-grid dragging needs an explicit <code>rowTransfer</code>.</strong> dhtmlx
|
|
926
926
|
lets any two grids with <code>dragItem: 'row'</code> on the same page exchange rows by
|
|
@@ -932,15 +932,15 @@ grid.events.on('cellClick', (row, column, event) => {
|
|
|
932
932
|
<div class="why">
|
|
933
933
|
<p><strong>What is not.</strong> Every <code>before*</code>/<code>can*</code>/<code>cancel*</code>
|
|
934
934
|
event is unmapped: dhtmlx's convention lets a handler return <code>false</code> to cancel the
|
|
935
|
-
action, and Lattice has no cancelable-event model to honour that with
|
|
935
|
+
action, and Lattice has no cancelable-event model to honour that with: approximating one
|
|
936
936
|
would silently ignore a <code>return false</code> a caller depends on. Row and column
|
|
937
|
-
drag-<em>negotiation</em> events
|
|
937
|
+
drag-<em>negotiation</em> events (<code>beforeRowDrag</code>, <code>dragRowOut</code>,
|
|
938
938
|
<code>canRowDrop</code>, <code>cancelRowDrop</code>, <code>beforeRowDrop</code> and their
|
|
939
|
-
column equivalents
|
|
939
|
+
column equivalents) are unmapped for the same reason: each can refuse or steer a drag
|
|
940
940
|
mid-gesture, which Lattice has no live protocol to offer. <code>export.pdf()</code> and
|
|
941
|
-
<code>export.png()</code> throw
|
|
941
|
+
<code>export.png()</code> throw: there is no raster export to translate to.
|
|
942
942
|
<code>.rangeSelection</code> is offered on a best-effort basis and its range shape is this
|
|
943
|
-
wrapper's own design, not dhtmlx's
|
|
943
|
+
wrapper's own design, not dhtmlx's, a genuine <code>RangeSelection</code> module is a
|
|
944
944
|
separate part of dhtmlx's own product, and this wrapper does not know its exact shape.
|
|
945
945
|
Classic <code>dhtmlXGridObject</code> (pre-Suite 5, string-configured,
|
|
946
946
|
index-addressed) is a different product in every respect that matters here and is not covered
|
|
@@ -954,7 +954,7 @@ grid.events.on('cellClick', (row, column, event) => {
|
|
|
954
954
|
<p class="lead-in">
|
|
955
955
|
<code>lattice-grid/modules/htmx</code> lets a grid survive htmx's own DOM swaps,
|
|
956
956
|
hydrate from a server-rendered <code><table></code>, and drive sort, filter and
|
|
957
|
-
infinite scroll over plain htmx requests
|
|
957
|
+
infinite scroll over plain htmx requests, the server owns pagination and the
|
|
958
958
|
request lifecycle; this module only wires the grid's own state to it. Importing it
|
|
959
959
|
is enough for the lifecycle half: it registers itself against
|
|
960
960
|
<code>document</code> on load.
|
|
@@ -977,7 +977,7 @@ autoInit(document);</code></pre>
|
|
|
977
977
|
|
|
978
978
|
<div class="why">
|
|
979
979
|
<p><strong>One file, not two.</strong> <code>autoInit</code> comes from
|
|
980
|
-
<code>modules/htmx</code> itself here, not from the base package
|
|
980
|
+
<code>modules/htmx</code> itself here, not from the base package: this module
|
|
981
981
|
already carries a complete, independently-bundled copy of <code>createGrid</code> and
|
|
982
982
|
everything it depends on, the same way every module built this way does (a bundle
|
|
983
983
|
inlines what it imports; it has no way to reach across to a copy some other
|
|
@@ -986,18 +986,18 @@ autoInit(document);</code></pre>
|
|
|
986
986
|
again for the base package's own <code>createGrid</code>. This module re-exports
|
|
987
987
|
<code>createGrid</code>, <code>autoInit</code>, <code>hydrateTable</code>,
|
|
988
988
|
<code>readTable</code>, <code>serialiseState</code> and <code>restoreState</code>
|
|
989
|
-
precisely so a page never has to choose between the two
|
|
989
|
+
precisely so a page never has to choose between the two, it is the complete package
|
|
990
990
|
for anything touching htmx, not an add-on alongside the base one.</p>
|
|
991
991
|
<p><strong>Surviving a swap.</strong> <code>autoInit(root)</code> builds a grid on
|
|
992
992
|
every <code>[data-lattice-grid]</code> element under <code>root</code> it has not
|
|
993
|
-
already built one for
|
|
993
|
+
already built one for: idempotent, so calling it again after a swap only picks
|
|
994
994
|
up what is new. A sibling <code><script type="application/json"
|
|
995
995
|
data-lattice-config></code> supplies columns and options; without one, a
|
|
996
996
|
<code><table></code> element is hydrated instead, reading its header row for
|
|
997
997
|
columns and its body rows for data, then replacing itself with the grid. Once
|
|
998
998
|
imported, this module listens for htmx's own <code>htmx:beforeCleanupElement</code>
|
|
999
999
|
and <code>htmx:load</code> and calls <code>grid.destroy()</code> / <code>autoInit</code>
|
|
1000
|
-
at the right moments automatically
|
|
1000
|
+
at the right moments automatically, a grid inside a swapped-out subtree is torn
|
|
1001
1001
|
down before htmx detaches it; a grid inside newly-loaded content is built without
|
|
1002
1002
|
re-scanning the whole page. A page with JavaScript disabled sees the plain
|
|
1003
1003
|
<code><table></code>, still readable, since it is only ever replaced once the
|
|
@@ -1015,10 +1015,10 @@ autoInit(document);</code></pre>
|
|
|
1015
1015
|
<span class="kw">const</span> grid = createGrid(host, { columns, rows: [], rowKey: 'id' });
|
|
1016
1016
|
<span class="kw">const</span> COLUMNS = columns.map(c => ({ field: c.field }));
|
|
1017
1017
|
|
|
1018
|
-
<span class="cmt">// Replaces the view outright
|
|
1018
|
+
<span class="cmt">// Replaces the view outright: fires whenever sort or filter changes.</span>
|
|
1019
1019
|
driveServerMode(grid, document.getElementById('query-trigger'), { columns: COLUMNS });
|
|
1020
1020
|
|
|
1021
|
-
<span class="cmt">// Appends the next chunk
|
|
1021
|
+
<span class="cmt">// Appends the next chunk: fires as the grid's own visible rows near the end.</span>
|
|
1022
1022
|
driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMNS });</code></pre>
|
|
1023
1023
|
<pre><code><div id="query-trigger" hx-get="/rows" hx-trigger="lattice:query-changed" hx-swap="none" hidden></div>
|
|
1024
1024
|
<div id="sentinel" hx-get="/rows" hx-trigger="revealed, lattice:scroll-near-end" hx-swap="none" hidden></div></code></pre>
|
|
@@ -1026,38 +1026,38 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
|
|
|
1026
1026
|
|
|
1027
1027
|
<div class="why">
|
|
1028
1028
|
<p><strong>Two triggers, two elements, deliberately.</strong> A sort or filter
|
|
1029
|
-
change and a scroll asking for more rows are different operations
|
|
1030
|
-
replaces every loaded row, the other appends to them
|
|
1029
|
+
change and a scroll asking for more rows are different operations: one
|
|
1030
|
+
replaces every loaded row, the other appends to them, and there is no way to
|
|
1031
1031
|
tell the two apart once a response has landed if they share a trigger. Each function
|
|
1032
1032
|
configures the request (<code>offset</code>, <code>limit</code>, <code>sort</code>,
|
|
1033
|
-
<code>filters</code
|
|
1033
|
+
<code>filters</code>, a small, stable convention any server-side language can
|
|
1034
1034
|
read with a JSON parser and a slice) and reads the response back into the grid
|
|
1035
1035
|
itself, so <code>hx-swap="none"</code> is required on both: htmx sends the request
|
|
1036
1036
|
and nothing else, since a grid is not an HTML swap target.</p>
|
|
1037
1037
|
<p><strong>The sentinel's own trigger names two events for a reason.</strong>
|
|
1038
1038
|
<code>revealed</code> is htmx's own once-per-element mechanism and gets the very
|
|
1039
|
-
first chunk, firing the moment htmx has processed the element
|
|
1039
|
+
first chunk, firing the moment htmx has processed the element: reliable
|
|
1040
1040
|
because it needs nothing from this module's own timing. Every chunk after that fires
|
|
1041
1041
|
through <code>lattice:scroll-near-end</code>, which <code>driveInfiniteScroll</code>
|
|
1042
1042
|
dispatches once the grid's own visible row window comes within
|
|
1043
1043
|
<code>opts.threshold</code> rows (default 20) of what is loaded. That split matters
|
|
1044
1044
|
for a fixed-height, virtualised grid specifically: nothing about it ever leaves the
|
|
1045
|
-
page's own viewport once first revealed
|
|
1046
|
-
scroll area, not the page's
|
|
1045
|
+
page's own viewport once first revealed: new rows land inside the grid's own
|
|
1046
|
+
scroll area, not the page's, so a page-scroll-only trigger would fire exactly
|
|
1047
1047
|
once and then go silent. Reading the grid's own render position instead is what
|
|
1048
1048
|
makes every later chunk fire on genuine scroll, not on the repaint a successful load
|
|
1049
1049
|
causes by itself.</p>
|
|
1050
1050
|
<p><strong>Out-of-band updates.</strong> <code>driveOobUpdates(grid, opts)</code>
|
|
1051
1051
|
watches for htmx's own out-of-band swaps landing on an element carrying
|
|
1052
1052
|
<code>data-lattice-row="<key>"</code>, reads the swapped fragment as that row's
|
|
1053
|
-
cells, and applies it to the grid in place
|
|
1053
|
+
cells, and applies it to the grid in place: scroll position, selection and
|
|
1054
1054
|
filter state are untouched, since nothing about the view is reloaded.</p>
|
|
1055
1055
|
<p><strong>Browser history.</strong> On <code>htmx:beforeHistorySave</code>, every
|
|
1056
|
-
live grid's state (<code>serialiseState</code
|
|
1056
|
+
live grid's state (<code>serialiseState</code>: sort, filters, column order and
|
|
1057
1057
|
widths, scroll position and selection, base64url-encoded and diffed against defaults
|
|
1058
1058
|
so an untouched grid costs almost nothing) is written onto its element as
|
|
1059
1059
|
<code>data-lattice-state</code>, which rides along in htmx's own history snapshot. On
|
|
1060
|
-
<code>htmx:historyRestore</code>, it is read back and applied
|
|
1060
|
+
<code>htmx:historyRestore</code>, it is read back and applied: browser back
|
|
1061
1061
|
returns a visitor to the sort, filter and scroll position they had, not a blank
|
|
1062
1062
|
slate. A cache miss (the page was re-fetched from the server) restores nothing, since
|
|
1063
1063
|
a fresh response is already the truth.</p>
|
|
@@ -1070,13 +1070,13 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
|
|
|
1070
1070
|
|
|
1071
1071
|
<div class="why">
|
|
1072
1072
|
<p><strong>What this does not do.</strong> It does not call <code>fetch</code> or
|
|
1073
|
-
<code>htmx.ajax()</code> anywhere
|
|
1073
|
+
<code>htmx.ajax()</code> anywhere: htmx owns every request end to end; this
|
|
1074
1074
|
only supplies the moment and the parameters, and reads the response back in. It does
|
|
1075
1075
|
not import htmx: every htmx-specific call goes through <code>globalThis.htmx</code>,
|
|
1076
1076
|
read at call time, so loading this module never requires htmx to already be on the
|
|
1077
1077
|
page, only to be present by the time a driven request actually fires. It ships as
|
|
1078
1078
|
ESM and as a plain <code><script src></code> build with no bundler required,
|
|
1079
|
-
with zero runtime dependencies beyond the grid itself and, at call time, htmx
|
|
1079
|
+
with zero runtime dependencies beyond the grid itself and, at call time, htmx ,
|
|
1080
1080
|
but because it references the grid's own internals directly rather than the copy
|
|
1081
1081
|
already on the page, the bundle carries a full copy of the grid core alongside its
|
|
1082
1082
|
own code, the same trade-off the web component and dhtmlx wrappers already make.</p>
|
|
@@ -1084,7 +1084,7 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
|
|
|
1084
1084
|
|
|
1085
1085
|
<h2 id="concepts">How it works</h2>
|
|
1086
1086
|
<p class="lead-in">
|
|
1087
|
-
Four ideas explain most of the API. If you read nothing else, read this section
|
|
1087
|
+
Four ideas explain most of the API. If you read nothing else, read this section, the rest of
|
|
1088
1088
|
the guide assumes it.
|
|
1089
1089
|
</p>
|
|
1090
1090
|
|
|
@@ -1092,7 +1092,7 @@ driveInfiniteScroll(grid, document.getElementById('sentinel'), { columns: COLUMN
|
|
|
1092
1092
|
<p class="lead-in">
|
|
1093
1093
|
Your data objects are held by reference and never copied. What the grid hands back is a
|
|
1094
1094
|
<em>row wrapper</em>: your object under <code>data</code>, plus the identity and position the
|
|
1095
|
-
grid needs
|
|
1095
|
+
grid needs: <code>key</code>, <code>index</code>, <code>level</code>, whether it is a group
|
|
1096
1096
|
row, whether it is expanded.
|
|
1097
1097
|
</p>
|
|
1098
1098
|
<div class="example">
|
|
@@ -1111,20 +1111,20 @@ grid.rows.values('r1') <span class="cmt">// every readable column, as an o
|
|
|
1111
1111
|
|
|
1112
1112
|
<h3>Display index and row key are different things</h3>
|
|
1113
1113
|
<p class="lead-in">
|
|
1114
|
-
A display index is a position
|
|
1114
|
+
A display index is a position: row 0 is whatever is at the top right now, and it changes when
|
|
1115
1115
|
you sort. A key identifies a record for as long as it exists. Anything that has to survive a
|
|
1116
1116
|
sort, a filter or a page change is keyed.
|
|
1117
1117
|
</p>
|
|
1118
1118
|
<div class="split">
|
|
1119
1119
|
<div>
|
|
1120
|
-
<p class="split__title split__title--no">Position
|
|
1120
|
+
<p class="split__title split__title--no">Position: fragile</p>
|
|
1121
1121
|
<pre><code>grid.rows.get(4)
|
|
1122
1122
|
grid.selection.setRange({
|
|
1123
1123
|
startRow: 0, endRow: 9, columns: ['cap'],
|
|
1124
1124
|
})</code></pre>
|
|
1125
1125
|
</div>
|
|
1126
1126
|
<div>
|
|
1127
|
-
<p class="split__title split__title--yes">Identity
|
|
1127
|
+
<p class="split__title split__title--yes">Identity: durable</p>
|
|
1128
1128
|
<pre><code>grid.rows.byKey('CIR-100042')
|
|
1129
1129
|
grid.edit.setCells([
|
|
1130
1130
|
{ key: 'CIR-100042', colId: 'cap', value: 99 },
|
|
@@ -1147,7 +1147,7 @@ grid.on('*', e => console.log(e.type, e)); <span class="cmt">// wildcard, f
|
|
|
1147
1147
|
off(); <span class="cmt">// every subscription returns its own unsubscribe</span></code></pre>
|
|
1148
1148
|
</div>
|
|
1149
1149
|
<div class="why">
|
|
1150
|
-
<p><code>origin</code> tells you where a change came from
|
|
1150
|
+
<p><code>origin</code> tells you where a change came from: <code>'user'</code>,
|
|
1151
1151
|
<code>'api'</code>, <code>'init'</code>, <code>'undo'</code>. It is what stops a feedback
|
|
1152
1152
|
loop when you persist changes: a handler that writes to a server on
|
|
1153
1153
|
<code>cell:changed</code> should usually ignore its own <code>'undo'</code> traffic, or at
|
|
@@ -1178,14 +1178,14 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
1178
1178
|
</p>
|
|
1179
1179
|
<h2 id="columns-guide">Defining columns</h2>
|
|
1180
1180
|
<p class="lead-in">
|
|
1181
|
-
A column is an object with a <code>field</code>
|
|
1181
|
+
A column is an object with a <code>field</code> (the path to read from your data) and
|
|
1182
1182
|
whatever else it needs. Everything except <code>field</code> or <code>id</code> is optional.
|
|
1183
1183
|
</p>
|
|
1184
1184
|
|
|
1185
1185
|
<div class="sig-line">{ field: 'site.address.postcode', title: 'Postcode', type: 'text', width: 110 }</div>
|
|
1186
1186
|
<p class="lead-in">
|
|
1187
1187
|
<code>field</code> reads a dot path, so nested data needs no flattening. The column's
|
|
1188
|
-
<code>id</code> defaults to the field, which is what you use everywhere else
|
|
1188
|
+
<code>id</code> defaults to the field, which is what you use everywhere else: in
|
|
1189
1189
|
<code>setCells</code>, in filters, in saved state.
|
|
1190
1190
|
</p>
|
|
1191
1191
|
|
|
@@ -1222,7 +1222,7 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
1222
1222
|
},
|
|
1223
1223
|
}
|
|
1224
1224
|
|
|
1225
|
-
<span class="cmt">// compute(deps, ctx)
|
|
1225
|
+
<span class="cmt">// compute(deps, ctx): deps holds the resolved values of the columns you</span>
|
|
1226
1226
|
<span class="cmt">// named; ctx carries { data, row, column, grid, context } when you need more.</span></code></pre>
|
|
1227
1227
|
</div>
|
|
1228
1228
|
<div class="why">
|
|
@@ -1249,13 +1249,13 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
1249
1249
|
<code>grid.columns.fit()</code> distributes the viewport width across visible columns, and
|
|
1250
1250
|
<code>autoSize</code> measures content.
|
|
1251
1251
|
</p>
|
|
1252
|
-
<p>Both are also on the column menu
|
|
1253
|
-
a Width submenu
|
|
1252
|
+
<p>Both are also on the column menu: Move left, Move right, Move to start, Move to end, and
|
|
1253
|
+
a Width submenu, and bound to the keyboard with a heading focused: <kbd>Alt</kbd> with a
|
|
1254
1254
|
left or right arrow resizes, <kbd>Shift</kbd> with one moves the column. Neither operation
|
|
1255
1255
|
depends on dragging.</p>
|
|
1256
1256
|
<p>A pinned region holds the edge of the viewport only while there is something to scroll. Where
|
|
1257
|
-
the columns are narrower than the grid
|
|
1258
|
-
has reached its <code>max</code
|
|
1257
|
+
the columns are narrower than the grid: fixed widths, or a <code>flex</code> column that
|
|
1258
|
+
has reached its <code>max</code>: nothing scrolls, so the pinned columns sit directly
|
|
1259
1259
|
after the centre ones and the spare width falls beyond them all, at the right of the grid.</p>
|
|
1260
1260
|
<p>To take that space up rather than leave it, give a column <code>flex</code> and no
|
|
1261
1261
|
<code>max</code>, or call <code>grid.columns.fit()</code>. Note that removing a column’s
|
|
@@ -1278,10 +1278,10 @@ LatticeGrid.getVersion(); <span class="cmt">// the same, when you have no grid
|
|
|
1278
1278
|
<tr><td class="name">Temporal</td><td class="desc"><code>datetime</code>, <code>time</code>, <code>duration</code></td></tr>
|
|
1279
1279
|
<tr><td class="name">Network</td><td class="desc"><code>ipv4</code>, <code>ipv6</code>, <code>cidr</code>, <code>mac</code></td></tr>
|
|
1280
1280
|
<tr><td class="name">Numeric bases</td><td class="desc"><code>hex</code>, <code>hex8</code>, <code>hex16</code>, <code>hex32</code>, <code>binary</code>, <code>binary8</code>, <code>octal</code></td></tr>
|
|
1281
|
-
<tr><td class="name">Units
|
|
1282
|
-
<tr><td class="name">Units
|
|
1283
|
-
<tr><td class="name">Units
|
|
1284
|
-
<tr><td class="name">Units
|
|
1281
|
+
<tr><td class="name">Units: computing</td><td class="desc"><code>bytes</code>, <code>megabytes</code>, <code>gigabytes</code>, <code>bitrate</code>, <code>gigabits</code></td></tr>
|
|
1282
|
+
<tr><td class="name">Units: physical</td><td class="desc"><code>metres</code>, <code>millimetres</code>, <code>kilometres</code>, <code>grams</code>, <code>kilograms</code>, <code>tonnes</code>, <code>seconds</code>, <code>milliseconds</code>, <code>hours</code></td></tr>
|
|
1283
|
+
<tr><td class="name">Units: engineering</td><td class="desc"><code>speed</code>, <code>kph</code>, <code>mph</code>, <code>knots</code>, <code>acceleration</code>, <code>area</code>, <code>hectares</code>, <code>volume</code>, <code>cubicMetres</code>, <code>energy</code>, <code>kilowattHours</code>, <code>power</code>, <code>kilowatts</code>, <code>force</code>, <code>pressure</code>, <code>bar</code>, <code>psi</code>, <code>torque</code>, <code>density</code>, <code>flow</code>, <code>litresPerMinute</code>, <code>radians</code>, <code>degrees</code></td></tr>
|
|
1284
|
+
<tr><td class="name">Units: electrical and scientific</td><td class="desc"><code>voltage</code>, <code>current</code>, <code>resistance</code>, <code>capacitance</code>, <code>inductance</code>, <code>charge</code>, <code>conductance</code>, <code>fluxDensity</code>, <code>luminousFlux</code>, <code>illuminance</code>, <code>substance</code>, <code>absorbedDose</code>, <code>equivalentDose</code>, <code>radioactivity</code>, <code>luminousIntensity</code>, <code>doseRate</code>, <code>rpm</code>, <code>angularVelocity</code>, <code>ppm</code>, <code>ppb</code>, <code>basisPoints</code>, <code>molarity</code>, <code>massFlow</code>, <code>tonnesPerHour</code>, <code>viscosity</code>, <code>kinematicViscosity</code>, <code>thermalConductivity</code>, <code>specificHeat</code>, <code>frequency</code></td></tr>
|
|
1285
1285
|
<tr><td class="name">Temperature</td><td class="desc"><code>celsius</code>, <code>fahrenheit</code>, <code>kelvin</code></td></tr>
|
|
1286
1286
|
<tr><td class="name">Structured</td><td class="desc"><code>json</code>, <code>colour</code>, <code>rating</code>, <code>percent</code></td></tr>
|
|
1287
1287
|
</tbody>
|
|
@@ -1326,14 +1326,14 @@ dataTypes: {
|
|
|
1326
1326
|
<div class="why">
|
|
1327
1327
|
<p><strong>Ambiguous units are refused, not guessed.</strong> A US gallon and an imperial
|
|
1328
1328
|
gallon differ by about a fifth, and "ton" means three different masses. Each has its own
|
|
1329
|
-
symbol
|
|
1329
|
+
symbol: <code>gal (US)</code>, <code>ton (UK)</code>, and the bare word is claimed by all of
|
|
1330
1330
|
them, so typing it is rejected rather than resolved. A <code>gal</code> silently taken as US
|
|
1331
1331
|
in a UK deployment is data corruption that reads as rounding.</p>
|
|
1332
1332
|
<p>The same rule catches case: <code>mV</code> and <code>MV</code> are a billion apart, so both
|
|
1333
1333
|
exact spellings work and the case-folded <code>mv</code> is refused.</p>
|
|
1334
1334
|
<p><strong>Customary units are accepted but never chosen.</strong> <code>display: 'auto'</code>
|
|
1335
1335
|
walks the coherent SI ladder only. With the calorie, the BTU and the kilojoule all on one
|
|
1336
|
-
ladder, 4,000 J would render as <code>3.79 BTU</code
|
|
1336
|
+
ladder, 4,000 J would render as <code>3.79 BTU</code>: auto picks the largest unit that fits,
|
|
1337
1337
|
and the BTU happens to be larger than the kilojoule. Ask for a BTU by name and you get one.</p>
|
|
1338
1338
|
</div>
|
|
1339
1339
|
|
|
@@ -1348,7 +1348,7 @@ dataTypes: {
|
|
|
1348
1348
|
|
|
1349
1349
|
<div class="why">
|
|
1350
1350
|
<p><strong>Angles wrap, so their mean is replaced.</strong> The average of 359° and 1° is 0°,
|
|
1351
|
-
and the arithmetic answer
|
|
1351
|
+
and the arithmetic answer (180°) is a confident, plausible number pointing in exactly the
|
|
1352
1352
|
wrong direction. A <code>degrees</code> or <code>radians</code> column averages by direction
|
|
1353
1353
|
instead, and reports nothing where the angles cancel and there is no mean direction to give.
|
|
1354
1354
|
The <em>sum</em> stays arithmetic, because a total rotation of 720° is two turns and that is a
|
|
@@ -1356,8 +1356,8 @@ dataTypes: {
|
|
|
1356
1356
|
<p><strong>Temperature is its own type, not a unit.</strong> Every other unit is a
|
|
1357
1357
|
multiplication; Celsius to Fahrenheit carries an offset, and zero Celsius is not zero
|
|
1358
1358
|
anything, so no factor converts it. <code>celsius</code>, <code>fahrenheit</code> and
|
|
1359
|
-
<code>kelvin</code> convert on input
|
|
1360
|
-
stores 22.2
|
|
1359
|
+
<code>kelvin</code> convert on input: type <code>72 F</code> into a Celsius column and it
|
|
1360
|
+
stores 22.2, and <strong>refuse to be summed</strong>: twenty degrees plus twenty degrees is
|
|
1361
1361
|
not forty degrees, and a footer saying so would be believed.</p>
|
|
1362
1362
|
</div>
|
|
1363
1363
|
|
|
@@ -1371,14 +1371,14 @@ dataTypes: {
|
|
|
1371
1371
|
<p class="example__label">One key each, and the types arrive</p>
|
|
1372
1372
|
<pre><code>columns: [
|
|
1373
1373
|
{ field: 'sku' }, <span class="cmt">// text</span>
|
|
1374
|
-
{ field: 'quantity' }, <span class="cmt">// number
|
|
1374
|
+
{ field: 'quantity' }, <span class="cmt">// number: aligned right, numeric filter</span>
|
|
1375
1375
|
{ field: 'shipped' }, <span class="cmt">// date</span>
|
|
1376
|
-
{ field: 'expedited' } <span class="cmt">// boolean
|
|
1376
|
+
{ field: 'expedited' } <span class="cmt">// boolean: checkbox editor</span>
|
|
1377
1377
|
]</code></pre>
|
|
1378
1378
|
</div>
|
|
1379
1379
|
<p>Sampling happens once, when rows first arrive. A grid built empty and filled later infers on
|
|
1380
1380
|
that first load, so fetching after construction is no reason to declare types you would otherwise
|
|
1381
|
-
leave out. Later loads keep the types already settled on
|
|
1381
|
+
leave out. Later loads keep the types already settled on: data that arrives tomorrow cannot
|
|
1382
1382
|
change a column's type under a formatter or an editor that was configured around it.</p>
|
|
1383
1383
|
<p>Only the built-in names are ever inferred. Candidates are tried in registration order, so every
|
|
1384
1384
|
string reaches <code>text</code> and every number reaches <code>number</code> before an extended
|
|
@@ -1397,7 +1397,7 @@ dataTypes: {
|
|
|
1397
1397
|
parsed back into a value, how the value is formatted, which filter kind the header offers, how
|
|
1398
1398
|
the column is stored, and what lands in Excel and on the clipboard. That is what inference
|
|
1399
1399
|
supplies for free on a plain column, and what naming a type explicitly gives you where the data
|
|
1400
|
-
cannot say it
|
|
1400
|
+
cannot say it, a duration, a byte count, a network address.</p>
|
|
1401
1401
|
</div>
|
|
1402
1402
|
|
|
1403
1403
|
<h3>Dates are stored as strings, deliberately</h3>
|
|
@@ -1413,7 +1413,7 @@ dataTypes: {
|
|
|
1413
1413
|
convert.</p>
|
|
1414
1414
|
<p>It is also faster and smaller: ISO 8601 sorts lexicographically in the same order it sorts
|
|
1415
1415
|
chronologically, so a date column sorts as text, and repeated dates dictionary-encode well.</p>
|
|
1416
|
-
<p>When you genuinely mean an instant
|
|
1416
|
+
<p>When you genuinely mean an instant (a log timestamp) use <code>datetime</code> and set
|
|
1417
1417
|
<code>format.timeZone</code>.</p>
|
|
1418
1418
|
</div>
|
|
1419
1419
|
|
|
@@ -1429,14 +1429,14 @@ format: { <span class="cmt">// when the short
|
|
|
1429
1429
|
style: 'currency', currency: 'GBP', decimals: 2,
|
|
1430
1430
|
negative: 'parentheses', <span class="cmt">// (£1,234.50)</span>
|
|
1431
1431
|
negativeClass: 'is-loss',
|
|
1432
|
-
nullDisplay: '
|
|
1432
|
+
nullDisplay: ', ',
|
|
1433
1433
|
}</code></pre>
|
|
1434
1434
|
</div>
|
|
1435
1435
|
|
|
1436
1436
|
<h3>Lookups</h3>
|
|
1437
1437
|
<p class="lead-in">
|
|
1438
1438
|
A lookup column stores an id and shows a label. Sorting, filtering, grouping, copying and
|
|
1439
|
-
exporting all use the label, because that is the thing the user is reasoning about
|
|
1439
|
+
exporting all use the label, because that is the thing the user is reasoning about, but the
|
|
1440
1440
|
data keeps the id.
|
|
1441
1441
|
</p>
|
|
1442
1442
|
<div class="example">
|
|
@@ -1487,7 +1487,7 @@ columns: [{ field: 'flags', type: 'reg32' }],
|
|
|
1487
1487
|
</p>
|
|
1488
1488
|
|
|
1489
1489
|
<div class="example">
|
|
1490
|
-
<p class="example__label">Decorations
|
|
1490
|
+
<p class="example__label">Decorations, the common cases, without writing a renderer</p>
|
|
1491
1491
|
<pre><code>cell: { decoration: 'pill' } <span class="cmt">// a status chip</span>
|
|
1492
1492
|
cell: { decoration: 'bar', min: 0, max: 1 } <span class="cmt">// an inline bar</span>
|
|
1493
1493
|
cell: { decoration: 'heat', ramp: 'redGreen' } <span class="cmt">// a heat fill</span>
|
|
@@ -1495,7 +1495,7 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
|
|
|
1495
1495
|
</div>
|
|
1496
1496
|
|
|
1497
1497
|
<div class="example">
|
|
1498
|
-
<p class="example__label">Variants
|
|
1498
|
+
<p class="example__label">Variants: mapping a value to a semantic colour</p>
|
|
1499
1499
|
<pre><code>cell: {
|
|
1500
1500
|
decoration: 'pill',
|
|
1501
1501
|
variant: { when: [
|
|
@@ -1505,7 +1505,7 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
|
|
|
1505
1505
|
}</code></pre>
|
|
1506
1506
|
</div>
|
|
1507
1507
|
<div class="why">
|
|
1508
|
-
<p>Variants are semantic tokens rather than colours
|
|
1508
|
+
<p>Variants are semantic tokens rather than colours: <code>danger</code>, not
|
|
1509
1509
|
<code>#c22b2b</code>. The theme decides what danger looks like, and it looks the same in the
|
|
1510
1510
|
status pill, the filter chip and the validation message. Changing the palette is one custom
|
|
1511
1511
|
property, not a search for hex codes.</p>
|
|
@@ -1525,7 +1525,7 @@ columns: [{ field: 'history', cell: { render: 'sparkline' } }],</code></pre>
|
|
|
1525
1525
|
<div class="why">
|
|
1526
1526
|
<p><code>refresh</code> returning <code>true</code> is the contract that makes recycling work:
|
|
1527
1527
|
it means "I updated in place, keep this element". Return <code>false</code> and the grid
|
|
1528
|
-
rebuilds the cell. A renderer that only implements <code>render</code> still works
|
|
1528
|
+
rebuilds the cell. A renderer that only implements <code>render</code> still works, it is
|
|
1529
1529
|
just rebuilt on every reuse.</p>
|
|
1530
1530
|
</div>
|
|
1531
1531
|
|
|
@@ -1540,8 +1540,8 @@ cell: { template: '<span>{{{ value }}}</span>' }</code></pre>
|
|
|
1540
1540
|
<div class="why">
|
|
1541
1541
|
<p><strong>The flag permits markup, not code.</strong> Without it, <code>{{ }}</code> escapes
|
|
1542
1542
|
and a <code>{{{ }}}</code> segment is refused outright. With it, an interpolated value may
|
|
1543
|
-
carry presentational markup
|
|
1544
|
-
<code><span></code
|
|
1543
|
+
carry presentational markup: <code><b></code>, <code><a href></code>, a
|
|
1544
|
+
<code><span></code>, and everything executable is still stripped from it:
|
|
1545
1545
|
<code><script></code>, <code><iframe></code>, <code><style></code> and the
|
|
1546
1546
|
other code-bearing tags, every <code>on*</code> handler attribute, and
|
|
1547
1547
|
<code>javascript:</code> or <code>data:</code> URLs including entity-encoded spellings of
|
|
@@ -1562,21 +1562,21 @@ cell: { template: '<span>{{{ value }}}</span>' }</code></pre>
|
|
|
1562
1562
|
</p>
|
|
1563
1563
|
<div class="example">
|
|
1564
1564
|
<p class="example__label">By scope</p>
|
|
1565
|
-
<pre><code><span class="cmt">// Cells and columns
|
|
1565
|
+
<pre><code><span class="cmt">// Cells and columns: declared on the column</span>
|
|
1566
1566
|
{ field: 'margin', cell: {
|
|
1567
1567
|
class: 'tabular',
|
|
1568
1568
|
classWhen: { 'is-loss': (p) => p.value < 0 },
|
|
1569
1569
|
style: (p) => ({ fontWeight: p.value > 1e6 ? 650 : 400 }),
|
|
1570
1570
|
}}
|
|
1571
1571
|
|
|
1572
|
-
<span class="cmt">// Rows
|
|
1572
|
+
<span class="cmt">// Rows: on the grid</span>
|
|
1573
1573
|
rowClass: (p) => p.data.slaBreached ? 'row-breach' : null,
|
|
1574
1574
|
rowStyle: (p) => p.data.region === 'AMER' ? { borderLeft: '3px solid #7c3aed' } : null,</code></pre>
|
|
1575
1575
|
</div>
|
|
1576
1576
|
<div class="why">
|
|
1577
1577
|
<p>All of these are re-evaluated on every repaint and remove what they added last time first.
|
|
1578
1578
|
That is not caution. Rows and cells come from pools, so an element that carried a class for
|
|
1579
|
-
one row will later carry a different row
|
|
1579
|
+
one row will later carry a different row, a class written once and left alone smears down
|
|
1580
1580
|
the grid as the user scrolls.</p>
|
|
1581
1581
|
</div>
|
|
1582
1582
|
|
|
@@ -1596,8 +1596,8 @@ rowStyle: (p) => p.data.region === 'AMER' ? { borderLeft: '3px solid #7c3aed'
|
|
|
1596
1596
|
<p class="lead-in">
|
|
1597
1597
|
Four themes ship. With no <code>theme</code> set, the grid follows the viewer's
|
|
1598
1598
|
<code>prefers-color-scheme</code> between light and dark; naming one pins it.
|
|
1599
|
-
Density is separate
|
|
1600
|
-
or <code>spacious</code
|
|
1599
|
+
Density is separate: <code>compact</code>, <code>standard</code>, <code>comfortable</code>
|
|
1600
|
+
or <code>spacious</code>, and combines with any of them.
|
|
1601
1601
|
</p>
|
|
1602
1602
|
<div class="example">
|
|
1603
1603
|
<p class="example__label">Pinning a theme, at build time or at runtime</p>
|
|
@@ -1611,14 +1611,14 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
|
|
|
1611
1611
|
<tbody>
|
|
1612
1612
|
<tr><td class="name"><code>light</code></td><td class="desc">The default. Follows <code>prefers-color-scheme</code> when <code>theme</code> is unset.</td></tr>
|
|
1613
1613
|
<tr><td class="name"><code>dark</code></td><td class="desc">The same palette inverted, with the accent and status hues re-picked for a dark ground rather than reused.</td></tr>
|
|
1614
|
-
<tr><td class="name"><code>high-contrast</code></td><td class="desc">Not "dark with more contrast". Text is 21:1 and borders 6.1:1 against the background, where the other themes sit near 1.3:1 on borders
|
|
1614
|
+
<tr><td class="name"><code>high-contrast</code></td><td class="desc">Not "dark with more contrast". Text is 21:1 and borders 6.1:1 against the background, where the other themes sit near 1.3:1 on borders, WCAG 1.4.11 asks for 3:1 on the boundaries a user has to find. Cell borders are drawn rather than implied, selected rows carry an outline as well as a fill, and every status pill has a solid border so it does not depend on hue alone.</td></tr>
|
|
1615
1615
|
<tr><td class="name"><code>terminal</code></td><td class="desc">A phosphor console: one hue on near-black, monospaced throughout. Status is carried by brightness rather than colour, so the palette stays a palette.</td></tr>
|
|
1616
1616
|
</tbody>
|
|
1617
1617
|
</table>
|
|
1618
1618
|
</div>
|
|
1619
1619
|
<h3>Forced colours</h3>
|
|
1620
1620
|
<p class="lead-in">
|
|
1621
|
-
Windows High Contrast Mode replaces the palette outright
|
|
1621
|
+
Windows High Contrast Mode replaces the palette outright: that is the point of it, and
|
|
1622
1622
|
no stylesheet should fight it. What the grid does instead is translate every piece of meaning
|
|
1623
1623
|
it normally carries in a background tint into something the mode preserves.
|
|
1624
1624
|
</p>
|
|
@@ -1626,8 +1626,8 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
|
|
|
1626
1626
|
which forced colours do not render, and gains a rule in its place. Status pills, fill
|
|
1627
1627
|
decorations, progress tracks and histogram bars each gain a border, because a fill with no
|
|
1628
1628
|
edge is invisible once its colour is discarded. Diff states stop depending on hue altogether:
|
|
1629
|
-
added, removed and changed are told apart by border style
|
|
1630
|
-
|
|
1629
|
+
added, removed and changed are told apart by border style: solid, dashed and doubled
|
|
1630
|
+
: since the mode offers no way to keep four distinct colours.</p>
|
|
1631
1631
|
<p>Two things deliberately keep their colour, declared with
|
|
1632
1632
|
<code>forced-color-adjust</code>: a colour swatch, where the colour <em>is</em> the value being
|
|
1633
1633
|
shown, and a collaborator's presence colour, which is how one person is told from another.
|
|
@@ -1645,21 +1645,21 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
|
|
|
1645
1645
|
<p class="lead-in">
|
|
1646
1646
|
Every selector in the stylesheet is namespaced under <code>.lattice</code>, so the grid cannot
|
|
1647
1647
|
restyle your page. From 1.4.0 the reverse is also true: the grid gives the elements it builds
|
|
1648
|
-
a floor for the properties a page is most likely to set on a bare tag
|
|
1648
|
+
a floor for the properties a page is most likely to set on a bare tag: margin, padding,
|
|
1649
1649
|
border, radius, background, shadow, text transform and letter spacing, plus type and colour on
|
|
1650
1650
|
form controls, which inherit neither.
|
|
1651
1651
|
</p>
|
|
1652
1652
|
<div class="why">
|
|
1653
1653
|
<p><strong>Why this is needed at all.</strong> A grid is mounted inside somebody else's
|
|
1654
|
-
stylesheet. A rule as ordinary as <code>section { padding: 5.5rem 0 }</code
|
|
1655
|
-
page, a CMS theme, a Tailwind preflight
|
|
1654
|
+
stylesheet. A rule as ordinary as <code>section { padding: 5.5rem 0 }</code>, a marketing
|
|
1655
|
+
page, a CMS theme, a Tailwind preflight: matches by tag name, and the grid builds parts of
|
|
1656
1656
|
its own interface from those tags: the tool panel's filter rows are
|
|
1657
1657
|
<code><section></code> elements. Without the reset, 5.5rem of somebody else's padding
|
|
1658
1658
|
lands on every one of them.</p>
|
|
1659
1659
|
<p>The reset uses no <code>!important</code>. It is specificity (0,1,1) and every rule that
|
|
1660
1660
|
dresses a grid element is (0,2,0) or higher, so the grid's own styling always wins and the
|
|
1661
|
-
reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class
|
|
1662
|
-
<code>.lattice .lat-cell { … }</code
|
|
1661
|
+
reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class ,
|
|
1662
|
+
<code>.lattice .lat-cell { … }</code>: outranks it, so overriding the grid deliberately works
|
|
1663
1663
|
exactly as before. Only bare-tag rules are shut out.</p>
|
|
1664
1664
|
<p>It touches box model and decoration only. Nothing in it sets <code>display</code>,
|
|
1665
1665
|
<code>position</code> or any dimension: those belong to the renderer, and a reset that reached
|
|
@@ -1667,7 +1667,7 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
|
|
|
1667
1667
|
</div>
|
|
1668
1668
|
<h2 id="loading">Loading and updating data</h2>
|
|
1669
1669
|
<p class="lead-in">
|
|
1670
|
-
Data usually arrives after the grid does. Build it empty, then load
|
|
1670
|
+
Data usually arrives after the grid does. Build it empty, then load, the sort, filters,
|
|
1671
1671
|
grouping and column layout you set up in the meantime all survive and apply to the new data.
|
|
1672
1672
|
</p>
|
|
1673
1673
|
<div class="example">
|
|
@@ -1682,8 +1682,8 @@ grid.overlay.hide();</code></pre>
|
|
|
1682
1682
|
|
|
1683
1683
|
<h3>Incremental changes</h3>
|
|
1684
1684
|
<p class="lead-in">
|
|
1685
|
-
<code>rows.load</code> replaces everything. When you have a delta
|
|
1686
|
-
save that returned the updated record
|
|
1685
|
+
<code>rows.load</code> replaces everything. When you have a delta, a websocket message, a
|
|
1686
|
+
save that returned the updated record: apply just that.
|
|
1687
1687
|
</p>
|
|
1688
1688
|
<div class="example">
|
|
1689
1689
|
<p class="example__label">Adds, updates and removals in one call</p>
|
|
@@ -1698,15 +1698,15 @@ grid.overlay.hide();</code></pre>
|
|
|
1698
1698
|
<div class="why">
|
|
1699
1699
|
<p><strong>An update is a patch.</strong> Fields absent from it are untouched, so a delta
|
|
1700
1700
|
arriving from a websocket or coming back from a save can be applied as-is without reading the
|
|
1701
|
-
row first. This has to be said explicitly because the opposite
|
|
1702
|
-
over the row
|
|
1701
|
+
row first. This has to be said explicitly because the opposite: assigning the patch
|
|
1702
|
+
over the row: looks identical for a caller who happens to send whole rows and silently
|
|
1703
1703
|
destroys data for one who does not.</p>
|
|
1704
1704
|
<p><strong>Coalescing merges fields rather than keeping the last message.</strong> A feed
|
|
1705
1705
|
sending <code>{price}</code> and <code>{volume}</code> as separate messages inside one window
|
|
1706
1706
|
keeps both. Coalescing may reorder work; it may not lose it.</p>
|
|
1707
1707
|
<p><strong>Flushing happens on a frame, with a timer behind it.</strong> A queued batch lands
|
|
1708
1708
|
on a paint boundary, which is what makes "ten thousand updates, one repaint" true rather than
|
|
1709
|
-
usually true
|
|
1709
|
+
usually true, a timer can fire twice between two paints. But
|
|
1710
1710
|
<code>requestAnimationFrame</code> is not guaranteed to fire at all: a backgrounded tab stops
|
|
1711
1711
|
firing it entirely. So a frame and a timer are armed together and the first to arrive wins. In
|
|
1712
1712
|
a foreground tab the frame always wins, at ~16ms against a 50ms fallback; in a hidden tab the
|
|
@@ -1715,7 +1715,7 @@ grid.overlay.hide();</code></pre>
|
|
|
1715
1715
|
<p><strong>A long flush defers rather than blocks.</strong> <code>updates.budgetMs</code> caps
|
|
1716
1716
|
how long one flush spends applying; over budget, the remainder returns to the queue and lands
|
|
1717
1717
|
next frame, and the promise a caller is holding resolves when their rows actually land rather
|
|
1718
|
-
than when the first slice does. Slicing is by row and only for updates
|
|
1718
|
+
than when the first slice does. Slicing is by row and only for updates, a partially
|
|
1719
1719
|
applied row is not a state the store should be in, and splitting a structural change would
|
|
1720
1720
|
re-run the pipeline twice for one batch. <code>stats().deferrals</code> rising steadily means
|
|
1721
1721
|
the feed is arriving faster than the grid can apply it.</p>
|
|
@@ -1727,7 +1727,7 @@ grid.overlay.hide();</code></pre>
|
|
|
1727
1727
|
<p>This runs the minimum pipeline. An update touching no sorted, filtered or grouped column
|
|
1728
1728
|
skips those stages entirely and only the totals and the affected cells refresh. Adds and
|
|
1729
1729
|
removals are structural and re-run everything.</p>
|
|
1730
|
-
<p>Removals tombstone in place rather than compacting, so every existing index stays valid
|
|
1730
|
+
<p>Removals tombstone in place rather than compacting, so every existing index stays valid ,
|
|
1731
1731
|
which is what lets selection, expansion state and cached permutations survive a delete.</p>
|
|
1732
1732
|
</div>
|
|
1733
1733
|
|
|
@@ -1778,7 +1778,7 @@ grid.rows.forEachAll(r => { total += r.data.amount }); <span class="cmt">//
|
|
|
1778
1778
|
<div class="why">
|
|
1779
1779
|
<p>A remote or paged source holds the page it has fetched, not the whole set, so there is
|
|
1780
1780
|
nothing there to walk past the filters. It warns and walks what it has rather than quietly
|
|
1781
|
-
returning the filtered rows
|
|
1781
|
+
returning the filtered rows, a caller who asked for everything and silently received a
|
|
1782
1782
|
subset gets a number that looks entirely plausible and is wrong.</p>
|
|
1783
1783
|
</div>
|
|
1784
1784
|
|
|
@@ -1799,7 +1799,7 @@ grid.sort.clear();</code></pre>
|
|
|
1799
1799
|
<p class="lead-in">
|
|
1800
1800
|
Clicking a header cycles ascending, descending, none; shift-clicking a second header adds to
|
|
1801
1801
|
the sort rather than replacing it. A column can supply its own <code>compare</code>, and a
|
|
1802
|
-
type already has one
|
|
1802
|
+
type already has one: dates compare chronologically, IP addresses numerically rather than as
|
|
1803
1803
|
strings, durations by length.
|
|
1804
1804
|
</p>
|
|
1805
1805
|
|
|
@@ -1890,7 +1890,7 @@ grid.rows.collapse('EMEA');</code></pre>
|
|
|
1890
1890
|
</table>
|
|
1891
1891
|
</div>
|
|
1892
1892
|
<p class="lead-in">
|
|
1893
|
-
Nothing has to be configured for this, and the reported number is the same either way
|
|
1893
|
+
Nothing has to be configured for this, and the reported number is the same either way ,
|
|
1894
1894
|
where a running value cannot be trusted, the column falls back to a full pass rather than
|
|
1895
1895
|
reporting a value it is unsure of.
|
|
1896
1896
|
</p>
|
|
@@ -1898,7 +1898,7 @@ grid.rows.collapse('EMEA');</code></pre>
|
|
|
1898
1898
|
<h3 id="show-total-in-header">showTotalInHeader</h3>
|
|
1899
1899
|
<p class="lead-in">
|
|
1900
1900
|
Under grouping or pivot, a totalled column's cells hold an aggregate rather than a row's own
|
|
1901
|
-
value. On by default, the heading says which
|
|
1901
|
+
value. On by default, the heading says which, a small <code>SUM</code> line above
|
|
1902
1902
|
<code>Capacity</code>, <code>AVERAGE</code> above <code>Margin</code>. The heading returns to
|
|
1903
1903
|
the column's own title when grouping and pivot are both off.
|
|
1904
1904
|
</p>
|
|
@@ -1909,7 +1909,7 @@ grid.rows.collapse('EMEA');</code></pre>
|
|
|
1909
1909
|
<p class="lead-in">
|
|
1910
1910
|
The reduction goes on its own line rather than reading <code>Sum of Capacity</code> across
|
|
1911
1911
|
one. A header cell reserves width for its sort, filter and menu buttons whether or not they
|
|
1912
|
-
are showing, so the label gets well under half the column
|
|
1912
|
+
are showing, so the label gets well under half the column: on a default column, 54px of
|
|
1913
1913
|
129px. One line truncated to <code>Sum o…</code>, trading the column's identity for its
|
|
1914
1914
|
reduction. Stacked, it costs no width at all.
|
|
1915
1915
|
</p>
|
|
@@ -1936,7 +1936,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1936
1936
|
the near edge, beside the row headings; <code>'after'</code> puts it at the far edge, which is
|
|
1937
1937
|
where a spreadsheet puts a grand total.</p>
|
|
1938
1938
|
<p><strong>It costs a column, not a pass.</strong> A pivoted group row still carries its
|
|
1939
|
-
reduction over every one of its leaves
|
|
1939
|
+
reduction over every one of its leaves, which is exactly the total across all pivot values ,
|
|
1940
1940
|
so these columns read a number that has already been computed. They also count towards
|
|
1941
1941
|
<code>maxColumns</code>, since they are columns like any other.</p>
|
|
1942
1942
|
<p><strong>Opt in.</strong> Omitted, a pivot has the columns it has always had, so the option
|
|
@@ -1950,7 +1950,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1950
1950
|
</div>
|
|
1951
1951
|
<p class="lead-in">
|
|
1952
1952
|
By default a total describes what is on screen: filter the grid and every total moves with
|
|
1953
|
-
it. Setting this to <code>false</code> makes the filter a lens instead
|
|
1953
|
+
it. Setting this to <code>false</code> makes the filter a lens instead: totals report the
|
|
1954
1954
|
whole dataset no matter what is filtered out. Both the grand total and each group total
|
|
1955
1955
|
follow the setting, so a group row shows the total for every row belonging to that group,
|
|
1956
1956
|
not only the ones currently visible.
|
|
@@ -1958,7 +1958,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1958
1958
|
<p class="lead-in">
|
|
1959
1959
|
A group whose every row the filter removed has no row to appear on, but its rows still count
|
|
1960
1960
|
toward the totals above it. Editing a hidden row moves the totals, because it is part of the
|
|
1961
|
-
dataset they describe
|
|
1961
|
+
dataset they describe: under the default it does not, because it is not part of the view
|
|
1962
1962
|
they describe.
|
|
1963
1963
|
</p>
|
|
1964
1964
|
<p class="lead-in">
|
|
@@ -1981,7 +1981,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1981
1981
|
<p class="lead-in">
|
|
1982
1982
|
Group totals re-reduce every totalled column on every change, including columns the change
|
|
1983
1983
|
did not touch. Switching this on reduces only the columns whose values actually moved, and
|
|
1984
|
-
an update that rewrites a field with the value it already held reduces nothing at all
|
|
1984
|
+
an update that rewrites a field with the value it already held reduces nothing at all ,
|
|
1985
1985
|
which is what a feed resending unchanged fields looks like.
|
|
1986
1986
|
</p>
|
|
1987
1987
|
<p class="lead-in">
|
|
@@ -1993,7 +1993,7 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
1993
1993
|
<tbody>
|
|
1994
1994
|
<tr><td class="name">One of the four columns</td><td class="desc">15.4ms</td><td class="desc">7.3ms</td></tr>
|
|
1995
1995
|
<tr><td class="name">All four columns</td><td class="desc">15.0ms</td><td class="desc">14.5ms</td></tr>
|
|
1996
|
-
<tr><td class="name">Nothing
|
|
1996
|
+
<tr><td class="name">Nothing: same values rewritten</td><td class="desc">15.6ms</td><td class="desc">7.3ms</td></tr>
|
|
1997
1997
|
</tbody>
|
|
1998
1998
|
</table>
|
|
1999
1999
|
</div>
|
|
@@ -2005,14 +2005,14 @@ pivot: { groupTotals: 'after', totalsLabel: 'All regions' }</code></pre>
|
|
|
2005
2005
|
<strong>It is off by default because it is an assertion, not just an optimisation.</strong>
|
|
2006
2006
|
Skipping a column assumes its total depends on nothing but that column's own values. That
|
|
2007
2007
|
is true of every built-in reduction. It need not be true of a <code>total</code> supplied as
|
|
2008
|
-
a function, which also receives the row, the grid and <code>config.context</code
|
|
2008
|
+
a function, which also receives the row, the grid and <code>config.context</code>: such a
|
|
2009
2009
|
total is only recomputed when its own column changes, so a function that reads application
|
|
2010
2010
|
state outside the column will report the value from the last time that column moved. Leave
|
|
2011
2011
|
the option off if any of your total functions work that way.
|
|
2012
2012
|
</p>
|
|
2013
2013
|
<p class="lead-in">
|
|
2014
|
-
Anything that changes which rows a total covers
|
|
2015
|
-
sorting, grouping, or changing which columns are totalled
|
|
2014
|
+
Anything that changes which rows a total covers: adding or removing rows, filtering,
|
|
2015
|
+
sorting, grouping, or changing which columns are totalled: reduces everything again
|
|
2016
2016
|
regardless of the option.
|
|
2017
2017
|
</p>
|
|
2018
2018
|
|
|
@@ -2043,7 +2043,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
|
|
|
2043
2043
|
</div>
|
|
2044
2044
|
|
|
2045
2045
|
<p class="lead-in">
|
|
2046
|
-
The objects are yours and are rendered through the ordinary column pipeline
|
|
2046
|
+
The objects are yours and are rendered through the ordinary column pipeline: value getters,
|
|
2047
2047
|
formatters, cell renderers and conditional formatting all run, so a pinned row looks like the
|
|
2048
2048
|
data it sits against without you rebuilding any of that.
|
|
2049
2049
|
</p>
|
|
@@ -2068,7 +2068,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
|
|
|
2068
2068
|
<thead><tr><th>Point</th><th>Behaviour</th></tr></thead>
|
|
2069
2069
|
<tbody>
|
|
2070
2070
|
<tr><td class="name">Order</td><td class="desc">Rows appear in the order of the array. At the bottom edge, the grand total comes first and your rows sit below it.</td></tr>
|
|
2071
|
-
<tr><td class="name">Height</td><td class="desc">From <code>rowHeight</code>, including the function form
|
|
2071
|
+
<tr><td class="name">Height</td><td class="desc">From <code>rowHeight</code>, including the function form, which is called with the pinned row, so you can measure your own content. The body reserves exactly the strip's height, so no data row hides underneath it.</td></tr>
|
|
2072
2072
|
<tr><td class="name">Updating</td><td class="desc">Pass a <em>new</em> array. Array identity is how the grid knows the rows changed; pushing into the array you passed before will not repaint.</td></tr>
|
|
2073
2073
|
<tr><td class="name">Editing</td><td class="desc">A pinned row has no place in the store to write to, so it is not editable.</td></tr>
|
|
2074
2074
|
</tbody>
|
|
@@ -2078,7 +2078,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
|
|
|
2078
2078
|
<h2 id="full-width-rows">Full-width rows</h2>
|
|
2079
2079
|
<p class="lead-in">
|
|
2080
2080
|
A row drawn as a single band across every column instead of being divided into them: a
|
|
2081
|
-
section banner, an explanatory note, an empty-group message, a “load more” affordance
|
|
2081
|
+
section banner, an explanatory note, an empty-group message, a “load more” affordance ,
|
|
2082
2082
|
anything that belongs between rows and is not itself divided by the columns.
|
|
2083
2083
|
</p>
|
|
2084
2084
|
|
|
@@ -2102,7 +2102,7 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
|
|
|
2102
2102
|
|
|
2103
2103
|
<p class="lead-in">
|
|
2104
2104
|
The band <strong>holds still while the columns scroll under it</strong>, which is what a
|
|
2105
|
-
banner is for
|
|
2105
|
+
banner is for: text that scrolled sideways out of view with the columns would be a worse
|
|
2106
2106
|
version of a cell. It is drawn over the pinned regions as well as the centre, so it genuinely
|
|
2107
2107
|
spans every column.
|
|
2108
2108
|
</p>
|
|
@@ -2115,8 +2115,8 @@ grid.setPinnedRows([], { edge: 'top' }); <span class="cmt">// clear</span
|
|
|
2115
2115
|
between them: <em>full-width</em> changes how a row looks, <em>pinned</em> changes whether a
|
|
2116
2116
|
row is data at all.</p>
|
|
2117
2117
|
<p>So if your banners must not appear in an export or a row count, they should not be in the
|
|
2118
|
-
data. If they are section headings that belong with the records they head
|
|
2119
|
-
filter and export alongside them
|
|
2118
|
+
data. If they are section headings that belong with the records they head, and should sort,
|
|
2119
|
+
filter and export alongside them: this is the right tool.</p>
|
|
2120
2120
|
<p>One consequence worth stating plainly: sorting reorders banners along with everything
|
|
2121
2121
|
else, because the predicate follows the row and not its position. Either do not offer sorting
|
|
2122
2122
|
on such a grid, or sort on a key that keeps each section together.</p>
|
|
@@ -2153,7 +2153,7 @@ grid.on('row:moved', ({ key, from, to }) => {
|
|
|
2153
2153
|
<p><strong>The order is your data, not a view of it.</strong> A move reorders the array you
|
|
2154
2154
|
gave the grid and tells you it happened; writing it somewhere permanent is yours, because
|
|
2155
2155
|
only you know where the order lives. A grid that rearranged rows on screen and stopped there
|
|
2156
|
-
would look finished and lose the order on the next load
|
|
2156
|
+
would look finished and lose the order on the next load, which is worse than not offering
|
|
2157
2157
|
the feature.</p>
|
|
2158
2158
|
<p>If the save fails, move it back: <code>grid.rows.move(key, from)</code>.</p>
|
|
2159
2159
|
</div>
|
|
@@ -2161,7 +2161,7 @@ grid.on('row:moved', ({ key, from, to }) => {
|
|
|
2161
2161
|
<p class="lead-in">
|
|
2162
2162
|
<strong>It refuses while a sort, filter or grouping is active</strong>, and says why out loud
|
|
2163
2163
|
rather than springing the row back in silence. The reason is that dropping between two
|
|
2164
|
-
visible rows says nothing about where the row belongs in the underlying array
|
|
2164
|
+
visible rows says nothing about where the row belongs in the underlying array: under a
|
|
2165
2165
|
filter there may be hidden rows between them, and under a sort the displayed order is
|
|
2166
2166
|
something the grid computed rather than something the data says. Rather than pick an
|
|
2167
2167
|
interpretation and put the row somewhere you did not ask for, the move is declined.
|
|
@@ -2171,7 +2171,7 @@ grid.on('row:moved', ({ key, from, to }) => {
|
|
|
2171
2171
|
|
|
2172
2172
|
<h2 id="row-transfer">Moving rows between grids</h2>
|
|
2173
2173
|
<p class="lead-in">
|
|
2174
|
-
A row can be dragged out of one grid and into another
|
|
2174
|
+
A row can be dragged out of one grid and into another, a picker beside a basket, an inbox
|
|
2175
2175
|
beside a queue, an available list beside an assigned one.
|
|
2176
2176
|
</p>
|
|
2177
2177
|
|
|
@@ -2210,10 +2210,10 @@ createGrid(right, { columns, rows,
|
|
|
2210
2210
|
<p><strong>Off by default, and both ends have to agree.</strong> Rows leaving a grid is a data
|
|
2211
2211
|
change you have to want: a grid that quietly let its rows be dragged away would lose one to a
|
|
2212
2212
|
mis-drag, and there is no gesture a user would think to try to get it back. A one-way
|
|
2213
|
-
relationship is a declaration on both grids rather than a convention
|
|
2213
|
+
relationship is a declaration on both grids rather than a convention, the sender refuses to
|
|
2214
2214
|
receive, and the receiver never starts a drag.</p>
|
|
2215
|
-
<p><strong>The target adds before the source removes.</strong> If the add is refused
|
|
2216
|
-
duplicate key, most likely
|
|
2215
|
+
<p><strong>The target adds before the source removes.</strong> If the add is refused: a
|
|
2216
|
+
duplicate key, most likely: nothing is removed, so a rejected transfer loses no data. The
|
|
2217
2217
|
other order would delete a row and then discover it had nowhere to go.</p>
|
|
2218
2218
|
<p><strong>The row object is cloned, not shared.</strong> Two grids holding the same object
|
|
2219
2219
|
would edit each other's rows through it, which is the sort of coupling nobody goes looking for
|
|
@@ -2229,7 +2229,7 @@ createGrid(right, { columns, rows,
|
|
|
2229
2229
|
<p class="lead-in">
|
|
2230
2230
|
<strong>Picking up a row shows it, wherever the pointer goes.</strong> The row being dragged
|
|
2231
2231
|
dims in its own grid, and a small label naming it follows the pointer for as long as the drag
|
|
2232
|
-
is held
|
|
2232
|
+
is held: over the gap between two grids, over one that is about to refuse the drop, anywhere
|
|
2233
2233
|
the row's own dimming cannot reach. Both clear on release, and a handle press never also starts
|
|
2234
2234
|
a range selection underneath it.
|
|
2235
2235
|
</p>
|
|
@@ -2258,7 +2258,7 @@ createGrid(right, { columns, rows,
|
|
|
2258
2258
|
<div class="why">
|
|
2259
2259
|
<p><strong>Only tagged columns are ever hidden.</strong> That is the rule the whole feature
|
|
2260
2260
|
turns on. A financial grid with sixty month columns also has an account name, a total and a
|
|
2261
|
-
variance, and none of those belong to a year
|
|
2261
|
+
variance, and none of those belong to a year: if filtering hid them the view would be
|
|
2262
2262
|
useless, and tagging every column merely to keep it visible would be busywork. So "show 2024"
|
|
2263
2263
|
does not mean "hide everything else"; it means "hide tagged columns that are not 2024".</p>
|
|
2264
2264
|
<p>The same rule runs the other way: an untagged column you hid yourself stays hidden, because
|
|
@@ -2266,7 +2266,7 @@ createGrid(right, { columns, rows,
|
|
|
2266
2266
|
</div>
|
|
2267
2267
|
|
|
2268
2268
|
<p class="lead-in">
|
|
2269
|
-
A column can carry more than one tag, which gives you a second axis for free
|
|
2269
|
+
A column can carry more than one tag, which gives you a second axis for free: tag each month
|
|
2270
2270
|
with its year <em>and</em> its quarter, and a user can pick either. The dropdown lists tags in
|
|
2271
2271
|
the order they were declared rather than alphabetically, since they are usually already in a
|
|
2272
2272
|
meaningful sequence and sorting would put <code>Q10</code> before <code>Q2</code>.
|
|
@@ -2287,7 +2287,7 @@ createGrid(right, { columns, rows,
|
|
|
2287
2287
|
<h3>Headings without the controls</h3>
|
|
2288
2288
|
<p class="lead-in">
|
|
2289
2289
|
A dense grid often wants the heading and nothing else. <code>showColumnFunctions: false</code>
|
|
2290
|
-
leaves each heading as its label, with no sort, filter or menu control
|
|
2290
|
+
leaves each heading as its label, with no sort, filter or menu control, they are not drawn
|
|
2291
2291
|
rather than hidden, so the label has the whole cell. Sorting, filtering and the column menu
|
|
2292
2292
|
stay reachable through the API, the keyboard and the tool panel; only the furniture goes. The
|
|
2293
2293
|
resize grip stays, since dragging a column wider is a view adjustment rather than a function
|
|
@@ -2328,7 +2328,7 @@ createGrid(right, { columns, rows,
|
|
|
2328
2328
|
|
|
2329
2329
|
<div class="why">
|
|
2330
2330
|
<p><strong>What is not shared is the design.</strong> If sort, filters and selection travelled
|
|
2331
|
-
too, this would not be a feature
|
|
2331
|
+
too, this would not be a feature, it would be one grid with extra steps. The reason to have
|
|
2332
2332
|
two is that the sections hold different data, so each keeps its own view of it.</p>
|
|
2333
2333
|
<p>Vertical scroll stays independent for the same reason: the grids hold different numbers of
|
|
2334
2334
|
rows, and yoking them would make the shorter one run out.</p>
|
|
@@ -2336,7 +2336,7 @@ createGrid(right, { columns, rows,
|
|
|
2336
2336
|
|
|
2337
2337
|
<p class="lead-in">
|
|
2338
2338
|
A column one grid has and another does not is skipped rather than invented, and nothing checks
|
|
2339
|
-
that the column sets match
|
|
2339
|
+
that the column sets match: aligning grids with different columns is a caller error that
|
|
2340
2340
|
produces a visibly wrong result rather than a silent one. Destroying any grid releases its
|
|
2341
2341
|
link and leaves the rest working.
|
|
2342
2342
|
</p>
|
|
@@ -2349,8 +2349,8 @@ createGrid(right, { columns, rows,
|
|
|
2349
2349
|
|
|
2350
2350
|
<div class="why">
|
|
2351
2351
|
<p><strong>The failure this prevents is a confident wrong number.</strong> You cannot add
|
|
2352
|
-
decibels
|
|
2353
|
-
is not the mean
|
|
2352
|
+
decibels: 90 dB and 90 dB make 93 dB, not 180. The mean of a column of rates
|
|
2353
|
+
is not the mean, a 100% conversion on two visits and a 1% conversion on ten thousand average
|
|
2354
2354
|
to 1.02%, not 50.5%. Both mistakes produce a plausible figure rather than an error, and a
|
|
2355
2355
|
footer nobody can check gets used. A missing total gets asked about; a wrong one does not.</p>
|
|
2356
2356
|
</div>
|
|
@@ -2382,7 +2382,7 @@ createGrid(right, { columns, rows,
|
|
|
2382
2382
|
<thead><tr><th>Type</th><th>What it does differently</th></tr></thead>
|
|
2383
2383
|
<tbody>
|
|
2384
2384
|
<tr><td class="name">decibel</td><td class="desc">Sums and averages in the linear domain and converts back. Power scale, factor 10.</td></tr>
|
|
2385
|
-
<tr><td class="name">decibelAmplitude</td><td class="desc">The same, on the field scale
|
|
2385
|
+
<tr><td class="name">decibelAmplitude</td><td class="desc">The same, on the field scale: factor 20, for voltage and current.</td></tr>
|
|
2386
2386
|
<tr><td class="name">ratio</td><td class="desc">Averages by weight, using the column named in <code>typeOptions.weight</code>. Refuses <code>sum</code>, since two rates do not add to a rate.</td></tr>
|
|
2387
2387
|
<tr><td class="name">percentRate</td><td class="desc">As <code>ratio</code>, displayed with a percent sign.</td></tr>
|
|
2388
2388
|
</tbody>
|
|
@@ -2397,7 +2397,7 @@ createGrid(right, { columns, rows,
|
|
|
2397
2397
|
|
|
2398
2398
|
<p class="lead-in">
|
|
2399
2399
|
Without a weight column the average returns nothing rather than falling back to the
|
|
2400
|
-
unweighted mean
|
|
2400
|
+
unweighted mean: falling back would be the exact mistake the type exists to prevent, arrived
|
|
2401
2401
|
at silently. Rows with no rate, or no weight, are left out rather than counted as zero.
|
|
2402
2402
|
</p>
|
|
2403
2403
|
|
|
@@ -2418,7 +2418,7 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2418
2418
|
often not in the page at all: the grid renders a window of rows, and a heading five hundred
|
|
2419
2419
|
rows above the viewport was recycled long ago. So the pinned heading is synthesised from
|
|
2420
2420
|
whichever group the top visible row belongs to, which the grid answers by binary search over
|
|
2421
|
-
an index it builds while flattening the rows
|
|
2421
|
+
an index it builds while flattening the rows, the ten-thousandth row of a group costs what
|
|
2422
2422
|
the second one does.</p>
|
|
2423
2423
|
<p>The cap exists because each heading costs a row of viewport. A five-level grouping without
|
|
2424
2424
|
one would spend a third of the screen describing what the other two thirds contain.</p>
|
|
@@ -2432,13 +2432,13 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2432
2432
|
|
|
2433
2433
|
<p class="lead-in">
|
|
2434
2434
|
The same answer is available directly as <code>grid.rows.groupHeadings(index)</code>, which
|
|
2435
|
-
returns the enclosing group rows outermost first
|
|
2435
|
+
returns the enclosing group rows outermost first, for a breadcrumb, or a heading elsewhere on
|
|
2436
2436
|
your page.
|
|
2437
2437
|
</p>
|
|
2438
2438
|
|
|
2439
2439
|
<h2 id="sources">Working with large data</h2>
|
|
2440
2440
|
<p class="lead-in">
|
|
2441
|
-
A million rows in memory is fine
|
|
2441
|
+
A million rows in memory is fine: that is what the columnar store is for. Beyond that, or
|
|
2442
2442
|
when the data lives behind an API, a source takes over.
|
|
2443
2443
|
</p>
|
|
2444
2444
|
<div class="table-wrap">
|
|
@@ -2448,7 +2448,7 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2448
2448
|
<tr><td class="name">memory</td><td class="desc">The default. Everything is present; the grid does all the work.</td></tr>
|
|
2449
2449
|
<tr><td class="name">paged</td><td class="desc">A page at a time from a server that paginates.</td></tr>
|
|
2450
2450
|
<tr><td class="name">remote</td><td class="desc">Blocks fetched on demand as the user scrolls, with sort and filter pushed to the server.</td></tr>
|
|
2451
|
-
<tr><td class="name">stream</td><td class="desc">Rows arriving over time
|
|
2451
|
+
<tr><td class="name">stream</td><td class="desc">Rows arriving over time, a query that streams, a socket. Promotes to memory once complete.</td></tr>
|
|
2452
2452
|
</tbody>
|
|
2453
2453
|
</table>
|
|
2454
2454
|
</div>
|
|
@@ -2477,7 +2477,7 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
|
|
|
2477
2477
|
viewport reaches them and cached; changing the sort or the filter invalidates the cache and
|
|
2478
2478
|
re-queries.</p>
|
|
2479
2479
|
<p>The <code>filters</code> your callback receives is the same condition tree documented
|
|
2480
|
-
above. You are not handed an opaque object to reverse-engineer
|
|
2480
|
+
above. You are not handed an opaque object to reverse-engineer, it is the published format,
|
|
2481
2481
|
and the same shape you would have written by hand.</p>
|
|
2482
2482
|
</div>
|
|
2483
2483
|
|
|
@@ -2524,7 +2524,7 @@ columns: [
|
|
|
2524
2524
|
<code>gridLines</code> chooses which rules are drawn between cells. <code>'horizontal'</code>
|
|
2525
2525
|
is the default and is what the grid has always drawn; vertical rules between body cells are
|
|
2526
2526
|
additive, so the default is unchanged and nothing moves on upgrade. <code>'rows'</code> and
|
|
2527
|
-
<code>'columns'</code> are accepted as aliases. Only the rules between data are affected
|
|
2527
|
+
<code>'columns'</code> are accepted as aliases. Only the rules between data are affected: the
|
|
2528
2528
|
header underline and the seams beside pinned columns are structure rather than decoration, and
|
|
2529
2529
|
removing them would make the pinned regions look detached.
|
|
2530
2530
|
</p>
|
|
@@ -2539,7 +2539,7 @@ columns: [
|
|
|
2539
2539
|
<h2 id="cards">Cards, lists and feeds</h2>
|
|
2540
2540
|
<p class="lead-in">
|
|
2541
2541
|
<code>rowTemplate</code> draws each row with a layout of your own instead of dividing it into
|
|
2542
|
-
columns. A card list, a feed, a search-result list, a message list
|
|
2542
|
+
columns. A card list, a feed, a search-result list, a message list: any presentation where a
|
|
2543
2543
|
record is a small piece of layout rather than a line of cells.
|
|
2544
2544
|
</p>
|
|
2545
2545
|
|
|
@@ -2559,28 +2559,28 @@ columns: [
|
|
|
2559
2559
|
<p><strong>The template compiles; it does not call back.</strong> There is deliberately no
|
|
2560
2560
|
"here is a container, build what you like for this row" hook. That shape is easy to offer and
|
|
2561
2561
|
would be used to allocate DOM per row, and at that moment the virtualisation stops paying for
|
|
2562
|
-
itself
|
|
2562
|
+
itself: quietly, and in a way nobody can attribute to a change. A row template is the same
|
|
2563
2563
|
declarative string a cell template is: parsed once, built into real DOM the first time an
|
|
2564
2564
|
element is used, and afterwards updated by writing text into the few nodes the bindings own.
|
|
2565
2565
|
Scrolling ten thousand records through a hundred pooled cards allocates nothing.</p>
|
|
2566
2566
|
<p><strong>Everything underneath is unchanged.</strong> Sorting, filtering, grouping,
|
|
2567
2567
|
selection, permissions, redaction, saved views, undo, export and the remote source all apply
|
|
2568
|
-
exactly as they do to a table
|
|
2568
|
+
exactly as they do to a table: only the drawing changes. That is the reason to build a card
|
|
2569
2569
|
view on a grid rather than beside one.</p>
|
|
2570
2570
|
</div>
|
|
2571
2571
|
|
|
2572
2572
|
<p class="lead-in">
|
|
2573
2573
|
<strong>A card is still a row.</strong> It carries the same row identity a table row does, so
|
|
2574
2574
|
<code>row:clicked</code> and <code>row:dblclicked</code> fire with the same payload, clicking
|
|
2575
|
-
selects, the context menu opens, and <code>rowReorder</code> works
|
|
2575
|
+
selects, the context menu opens, and <code>rowReorder</code> works, with the card itself as
|
|
2576
2576
|
the drag handle, since there is no cell to put a grip in. None of that is a second
|
|
2577
2577
|
implementation; it is the same code that serves a table.
|
|
2578
2578
|
</p>
|
|
2579
2579
|
|
|
2580
2580
|
<div class="why">
|
|
2581
2581
|
<p><strong>It is announced as a list, not a grid.</strong> A card has no columns, so the
|
|
2582
|
-
<code>grid</code> role
|
|
2583
|
-
two-dimensional keyboard model
|
|
2582
|
+
<code>grid</code> role, which promises columns, <code>gridcell</code> children and a
|
|
2583
|
+
two-dimensional keyboard model: would misdescribe it completely. The layer is a
|
|
2584
2584
|
<code>list</code>, each card a <code>listitem</code> carrying its position and the size of the
|
|
2585
2585
|
whole set, and the column header is not drawn. <code>role</code> and <code>itemRole</code>
|
|
2586
2586
|
override both, for a presentation that is really a <code>listbox</code>.</p>
|
|
@@ -2612,7 +2612,7 @@ columns: [
|
|
|
2612
2612
|
filling a small tablet is not. The grid already watches its own element for size changes, so
|
|
2613
2613
|
the same observer answers this.</p>
|
|
2614
2614
|
<p><strong>The state a user built survives the switch.</strong> Rotating a phone must not lose
|
|
2615
|
-
the sort, the filters, the selection or the scroll position, and it does not
|
|
2615
|
+
the sort, the filters, the selection or the scroll position, and it does not, it is one grid
|
|
2616
2616
|
throughout, and only the drawing changes. An open cell editor is closed, since the cell it
|
|
2617
2617
|
belonged to stops existing.</p>
|
|
2618
2618
|
</div>
|
|
@@ -2621,7 +2621,7 @@ columns: [
|
|
|
2621
2621
|
<strong>Sorting and filtering need a home</strong> when there are no column headings to click,
|
|
2622
2622
|
and the tool panel is it: set <code>toolPanel: true</code> and its rail stays available in card
|
|
2623
2623
|
presentation with the columns and filter panels behind it. <strong>Export is unaffected</strong>
|
|
2624
|
-
|
|
2624
|
+
, the columns are still the data model, so a CSV or an Excel file from a collapsed grid holds
|
|
2625
2625
|
every column, including ones the card does not show.
|
|
2626
2626
|
</p>
|
|
2627
2627
|
|
|
@@ -2634,7 +2634,7 @@ columns: [
|
|
|
2634
2634
|
|
|
2635
2635
|
<h3>Showing what the grid shows</h3>
|
|
2636
2636
|
<p class="lead-in">
|
|
2637
|
-
<code>{{cell.<em>column</em>}}</code> is the text the table puts in that cell
|
|
2637
|
+
<code>{{cell.<em>column</em>}}</code> is the text the table puts in that cell, the column's
|
|
2638
2638
|
own formatter, data type, number and date settings and lookup label, all of it.
|
|
2639
2639
|
<code>{{data.<em>field</em>}}</code> is the raw value underneath.
|
|
2640
2640
|
</p>
|
|
@@ -2643,10 +2643,10 @@ columns: [
|
|
|
2643
2643
|
<table>
|
|
2644
2644
|
<thead><tr><th>Binding</th><th>Reads</th></tr></thead>
|
|
2645
2645
|
<tbody>
|
|
2646
|
-
<tr><td class="sig">{{cell.value}}</td><td class="desc">£1,250.50
|
|
2647
|
-
<tr><td class="sig">{{data.value}}</td><td class="desc">1250.5
|
|
2648
|
-
<tr><td class="sig">{{cell.stage}}</td><td class="desc">Held
|
|
2649
|
-
<tr><td class="sig">{{data.stage}}</td><td class="desc">2
|
|
2646
|
+
<tr><td class="sig">{{cell.value}}</td><td class="desc">£1,250.50, the cell's rendered text</td></tr>
|
|
2647
|
+
<tr><td class="sig">{{data.value}}</td><td class="desc">1250.5, the stored number</td></tr>
|
|
2648
|
+
<tr><td class="sig">{{cell.stage}}</td><td class="desc">Held, a lookup's label</td></tr>
|
|
2649
|
+
<tr><td class="sig">{{data.stage}}</td><td class="desc">2, the lookup's id</td></tr>
|
|
2650
2650
|
</tbody>
|
|
2651
2651
|
</table>
|
|
2652
2652
|
</div>
|
|
@@ -2654,7 +2654,7 @@ columns: [
|
|
|
2654
2654
|
<p class="lead-in">
|
|
2655
2655
|
Both are wanted, which is why both exist: a card showing a value to a person wants
|
|
2656
2656
|
<code>cell</code>, and a template comparing or calculating wants <code>data</code>. A lookup is
|
|
2657
|
-
the case that decides it
|
|
2657
|
+
the case that decides it, a card showing <code>2</code> where the table shows
|
|
2658
2658
|
<code>Held</code> is not a formatting preference but a plain bug.
|
|
2659
2659
|
</p>
|
|
2660
2660
|
|
|
@@ -2662,7 +2662,7 @@ columns: [
|
|
|
2662
2662
|
<p><strong>A protected column cannot be read raw.</strong> Binding
|
|
2663
2663
|
<code>{{data.password}}</code> on a secret or redacted column would print the value the column
|
|
2664
2664
|
exists to hide, while the table beside it shows dots. Such a binding reads the masked text
|
|
2665
|
-
instead and says once that it did
|
|
2665
|
+
instead and says once that it did, a card must not become the hole a redaction closes.</p>
|
|
2666
2666
|
</div>
|
|
2667
2667
|
|
|
2668
2668
|
<h3>Several cards on a line</h3>
|
|
@@ -2681,7 +2681,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2681
2681
|
<p class="lead-in">
|
|
2682
2682
|
<code>cardsPerRow</code> is a count, for a layout that must not reflow. <code>maxCardWidth</code>
|
|
2683
2683
|
is a ceiling: the grid fits as many whole cards as it can without exceeding it, and they share
|
|
2684
|
-
the remaining space rather than leaving a ragged margin
|
|
2684
|
+
the remaining space rather than leaving a ragged margin, so 900px at a 200px ceiling is four
|
|
2685
2685
|
cards of 225px, and the count changes with the container. <code>gap</code> sets the space
|
|
2686
2686
|
between them. Where both are given, <code>cardsPerRow</code> wins, being an instruction rather
|
|
2687
2687
|
than a preference.
|
|
@@ -2690,14 +2690,14 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2690
2690
|
<div class="why">
|
|
2691
2691
|
<p><strong>The scroll height counts lines, not records.</strong> Four records on a line means
|
|
2692
2692
|
the content is a quarter as tall as the row model alone would make it, and a scrollbar sized
|
|
2693
|
-
per record would be four times too long
|
|
2693
|
+
per record would be four times too long, the last several screens empty. The tiled layout
|
|
2694
2694
|
works out its own window from the scroll position for the same reason: the window it would
|
|
2695
2695
|
otherwise be handed counts one record per line and would leave the bottom of the screen bare.
|
|
2696
2696
|
Pooling is unaffected; scrolling a tiled gallery reuses its elements exactly as a list does.</p>
|
|
2697
2697
|
</div>
|
|
2698
2698
|
|
|
2699
2699
|
<p class="lead-in">
|
|
2700
|
-
Tiles are a fixed height, taken from <code>rowHeight</code>
|
|
2700
|
+
Tiles are a fixed height, taken from <code>rowHeight</code> ,
|
|
2701
2701
|
<code>rowHeight: 'auto'</code> measures a rendered row and cannot describe a line holding
|
|
2702
2702
|
several of different heights. Variable-height tiles flowing into the shortest column is a
|
|
2703
2703
|
masonry layout, which is a different thing and is not offered.
|
|
@@ -2705,14 +2705,14 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2705
2705
|
|
|
2706
2706
|
<p class="lead-in">
|
|
2707
2707
|
Row heights work as they do everywhere else, including <code>rowHeight: 'auto'</code>, which
|
|
2708
|
-
measures the rendered card
|
|
2708
|
+
measures the rendered card: content-driven card heights need no extra configuration. The
|
|
2709
2709
|
columns are still declared and still hold the data: they are what sorting, filtering and
|
|
2710
2710
|
export operate on, and what the bindings read.
|
|
2711
2711
|
</p>
|
|
2712
2712
|
|
|
2713
2713
|
<h2 id="row-form">Editing a row on a form</h2>
|
|
2714
2714
|
<p class="lead-in">
|
|
2715
|
-
Double-clicking a row opens it in a panel
|
|
2715
|
+
Double-clicking a row opens it in a panel (a right-hand drawer or a centred dialog) with
|
|
2716
2716
|
one control per field, a Save and a Cancel. It is the shape almost every application built on
|
|
2717
2717
|
a grid ends up wanting, and until now the shape they had to build themselves.
|
|
2718
2718
|
</p>
|
|
@@ -2729,7 +2729,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2729
2729
|
<p class="lead-in">
|
|
2730
2730
|
That is the whole of it for the common case: the form is the row, edited with the same
|
|
2731
2731
|
editors, types, formats and lookups the cells use. Where the record has more to it than the
|
|
2732
|
-
grid shows, give a <code>load</code> function
|
|
2732
|
+
grid shows, give a <code>load</code> function, and then say which fields and in what order,
|
|
2733
2733
|
because nothing in the grid knows the shape of something it has never seen.
|
|
2734
2734
|
</p>
|
|
2735
2735
|
|
|
@@ -2750,21 +2750,21 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2750
2750
|
|
|
2751
2751
|
<h3>Which editor a field gets</h3>
|
|
2752
2752
|
<p class="lead-in">
|
|
2753
|
-
Every editor is available on a form, including your own from the module registry
|
|
2753
|
+
Every editor is available on a form, including your own from the module registry, the form
|
|
2754
2754
|
builds its controls through the same call a cell does, so a field gets the same editor, type,
|
|
2755
2755
|
formatting and lookup its column would have given it. A field named after a column borrows
|
|
2756
2756
|
that column outright and needs nothing further.
|
|
2757
2757
|
</p>
|
|
2758
2758
|
<p class="lead-in">
|
|
2759
|
-
A field the grid has never seen
|
|
2759
|
+
A field the grid has never seen (or one you want entered differently from its cell) says so
|
|
2760
2760
|
on the field itself. <code>editor</code> names it, and <code>type</code>, <code>props</code>
|
|
2761
2761
|
and <code>lookup</code> configure it exactly as they would on a column. Overriding the control
|
|
2762
2762
|
does not change where the value goes: a field still writes back only if it maps to a column.
|
|
2763
2763
|
</p>
|
|
2764
2764
|
|
|
2765
2765
|
<div class="why">
|
|
2766
|
-
<p><strong>A picker opens when it is asked to.</strong> A popup editor
|
|
2767
|
-
tree, a colour, a code panel
|
|
2766
|
+
<p><strong>A picker opens when it is asked to.</strong> A popup editor, a date, a dropdown, a
|
|
2767
|
+
tree, a colour, a code panel: <em>is</em> its panel: in a cell it opens the moment the cell
|
|
2768
2768
|
does, which is right, because the user has just asked to edit that one cell. A form builds
|
|
2769
2769
|
every field at once, so on a form the field shows the current value on a control and the panel
|
|
2770
2770
|
opens over it when clicked. Choosing puts the panel away again and updates the control.</p>
|
|
@@ -2780,7 +2780,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2780
2780
|
<p><strong>And a load that never answers is a failure too.</strong> A promise that neither
|
|
2781
2781
|
resolves nor rejects is what a dropped request looks like from the page; left alone it spins
|
|
2782
2782
|
until the user gives up, which reads as an application that has hung rather than a request
|
|
2783
|
-
that failed. After <code>timeout</code> milliseconds
|
|
2783
|
+
that failed. After <code>timeout</code> milliseconds, two seconds unless you say otherwise ,
|
|
2784
2784
|
the form stops waiting and shows the same message and retry as any other failure. Set
|
|
2785
2785
|
<code>timeout: false</code> to wait indefinitely, which is right only where your own loader
|
|
2786
2786
|
already has a limit and would rather report that one. A record that turns up after the form
|
|
@@ -2791,7 +2791,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2791
2791
|
<p class="lead-in">
|
|
2792
2792
|
The fields scroll and the heading and buttons do not, so Save stays reachable on a record with
|
|
2793
2793
|
forty fields. If a validator refuses one of them, the form stays open, the field is marked, and
|
|
2794
|
-
it is scrolled into view and focused
|
|
2794
|
+
it is scrolled into view and focused: on a long form the offending field is otherwise nowhere
|
|
2795
2795
|
near the button that was just pressed.
|
|
2796
2796
|
</p>
|
|
2797
2797
|
|
|
@@ -2821,7 +2821,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2821
2821
|
|
|
2822
2822
|
<div class="why">
|
|
2823
2823
|
<p><strong>The form takes the double click.</strong> On an editable grid that gesture also
|
|
2824
|
-
opens a cell editor, and the two cannot both own it
|
|
2824
|
+
opens a cell editor, and the two cannot both own it, a form that quietly did nothing where a
|
|
2825
2825
|
cell happened to be editable would be worse than no form. So where <code>rowForm</code> is
|
|
2826
2826
|
configured, double-clicking a row opens the form and the cell editor stays reachable by
|
|
2827
2827
|
Enter or by typing into the cell. Set <code>trigger: false</code> to leave opening entirely to
|
|
@@ -2831,7 +2831,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2831
2831
|
<h3>Putting the form in your own element</h3>
|
|
2832
2832
|
<p class="lead-in">
|
|
2833
2833
|
A drawer and a dialog both sit over the grid. Give <code>container</code> an element of your
|
|
2834
|
-
own and the form is built there instead
|
|
2834
|
+
own and the form is built there instead, a sidebar beside the grid, a panel below it, a
|
|
2835
2835
|
column in a layout you already have. It fills what it is given, so the size and position are
|
|
2836
2836
|
yours.
|
|
2837
2837
|
</p>
|
|
@@ -2847,7 +2847,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2847
2847
|
<p class="lead-in">
|
|
2848
2848
|
A selector is resolved when the form <em>opens</em>, not when the grid is configured, because
|
|
2849
2849
|
a grid is routinely built before the layout around it exists. A container that cannot be found
|
|
2850
|
-
falls back to opening over the grid
|
|
2850
|
+
falls back to opening over the grid: better a form in the wrong place than a double-click
|
|
2851
2851
|
that appears to do nothing.
|
|
2852
2852
|
</p>
|
|
2853
2853
|
|
|
@@ -2869,18 +2869,18 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2869
2869
|
<p class="lead-in">
|
|
2870
2870
|
The grid has always written optimistically without calling it that: an edit lands in the
|
|
2871
2871
|
model and is painted before anything else happens. What <code>edit.commit</code> adds is
|
|
2872
|
-
<em>durability</em
|
|
2872
|
+
<em>durability</em>: whether the write reached your server, and what to put back when it
|
|
2873
2873
|
did not.
|
|
2874
2874
|
</p>
|
|
2875
2875
|
<div class="why">
|
|
2876
2876
|
<p><strong>Nothing changes unless you ask for it.</strong> With no <code>commit</code> hook
|
|
2877
2877
|
the grid behaves exactly as before: the value is written, history is recorded,
|
|
2878
2878
|
<code>cell:changed</code> fires, and there is no pending state to think about. Subscribe to
|
|
2879
|
-
<code>cell:changed</code>, fire your request and ignore the result
|
|
2879
|
+
<code>cell:changed</code>, fire your request and ignore the result: that keeps working and
|
|
2880
2880
|
costs nothing.</p>
|
|
2881
2881
|
</div>
|
|
2882
2882
|
<div class="example">
|
|
2883
|
-
<p class="example__label">The usual case
|
|
2883
|
+
<p class="example__label">The usual case, the promise is the answer</p>
|
|
2884
2884
|
<pre><code>edit: {
|
|
2885
2885
|
enabled: true,
|
|
2886
2886
|
commit: async ({ key, colId, value }) => {
|
|
@@ -2894,7 +2894,7 @@ rowTemplate: { template: CARD, maxCardWidth: 260 } <span class="cmt">// as ma
|
|
|
2894
2894
|
</div>
|
|
2895
2895
|
<p class="lead-in">
|
|
2896
2896
|
Resolving confirms the write; throwing rolls it back and fires <code>cell:reverted</code>
|
|
2897
|
-
with your error message as <code>reason</code>. A synchronous hook works too
|
|
2897
|
+
with your error message as <code>reason</code>. A synchronous hook works too: returning
|
|
2898
2898
|
normally confirms, throwing reverts.
|
|
2899
2899
|
</p>
|
|
2900
2900
|
<div class="example">
|
|
@@ -2918,7 +2918,7 @@ socket.onmessage = (m) => {
|
|
|
2918
2918
|
promise to resolve. <code>confirm: 'manual'</code> says so explicitly. The grid does not infer
|
|
2919
2919
|
it from what <code>commit</code> returns, because then a synchronous hook that happens to
|
|
2920
2920
|
return nothing would leave every cell pending for ever with nothing in your code that looks
|
|
2921
|
-
wrong. If a write does stay pending, you get a console warning naming the cell
|
|
2921
|
+
wrong. If a write does stay pending, you get a console warning naming the cell: tune the
|
|
2922
2922
|
threshold with <code>pendingTimeout</code>.</p>
|
|
2923
2923
|
</div>
|
|
2924
2924
|
|
|
@@ -2940,7 +2940,7 @@ socket.onmessage = (m) => {
|
|
|
2940
2940
|
the part that is easy to get wrong by hand. Suppose a cell holding <code>1</code> is edited to
|
|
2941
2941
|
<code>2</code>, then to <code>3</code>, then to <code>4</code>, all before any answer comes
|
|
2942
2942
|
back. If the second write fails, restoring “the value before it” would put back
|
|
2943
|
-
<code>2</code
|
|
2943
|
+
<code>2</code>, a value the server never held, and one the user has since replaced twice.
|
|
2944
2944
|
So each cell remembers the newest value a confirmation has actually vouched for, and a write
|
|
2945
2945
|
that a later edit has superseded reports its failure without writing anything back. You will
|
|
2946
2946
|
see <code>cell:reverted</code> with <code>applied: false</code> for those.</p>
|
|
@@ -2951,8 +2951,8 @@ socket.onmessage = (m) => {
|
|
|
2951
2951
|
<div class="why">
|
|
2952
2952
|
<p><strong>Two behaviours worth knowing.</strong> An unconfirmed edit goes through the normal
|
|
2953
2953
|
pipeline, so if it changes a sorted or filtered column the row moves immediately and moves
|
|
2954
|
-
back if the write fails. And undo of an in-flight edit issues a <em>compensating write</em>
|
|
2955
|
-
a fresh write back to the previous value, itself tracked
|
|
2954
|
+
back if the write fails. And undo of an in-flight edit issues a <em>compensating write</em> ,
|
|
2955
|
+
a fresh write back to the previous value, itself tracked: rather than pretending to cancel a
|
|
2956
2956
|
request that has already gone out.</p>
|
|
2957
2957
|
</div>
|
|
2958
2958
|
|
|
@@ -2987,28 +2987,28 @@ grid.edit.status('r1', 'cap'); <span class="cmt">// 'pending' | null</span></
|
|
|
2987
2987
|
</div>
|
|
2988
2988
|
<p class="lead-in">
|
|
2989
2989
|
<strong>Parent-reference</strong> is what a join or a document store produces. Every node is
|
|
2990
|
-
a real row. A row whose parent is not in the data
|
|
2991
|
-
wrong
|
|
2990
|
+
a real row. A row whose parent is not in the data (filtered away, not loaded, or simply
|
|
2991
|
+
wrong) is an orphan: it goes to the root by default, or into a named bucket. It is never
|
|
2992
2992
|
dropped, because hiding a record over a bad reference loses data the user can see in a flat
|
|
2993
2993
|
view.
|
|
2994
2994
|
</p>
|
|
2995
2995
|
<p class="lead-in">
|
|
2996
2996
|
<strong>Path-based</strong> rows describe their own place, so intermediate levels may have no
|
|
2997
|
-
row at all
|
|
2997
|
+
row at all: <code>EMEA/UK/Colchester</code> with no <code>EMEA/UK</code> row still needs a
|
|
2998
2998
|
<code>UK</code> node to sit under. Those are synthesised, and render as group rows: a heading
|
|
2999
2999
|
over the rows beneath it with no record of its own. A real row arriving later for a level
|
|
3000
3000
|
already synthesised fills that node rather than appearing beside it.
|
|
3001
3001
|
</p>
|
|
3002
3002
|
<p class="lead-in">
|
|
3003
3003
|
A parent cycle is not a hierarchy and cannot be walked. It is reported once and cut, with the
|
|
3004
|
-
rows shown at the root
|
|
3004
|
+
rows shown at the root: wrong place beats vanished.
|
|
3005
3005
|
</p>
|
|
3006
3006
|
<p class="lead-in">
|
|
3007
3007
|
The grid generates a <strong>tree column</strong> to carry the expander and the indent, on the
|
|
3008
3008
|
same terms as the auto-group, selection and detail columns: pinned to the start, and absent
|
|
3009
3009
|
from <code>columns.visible()</code>, saved views, exports and the tool panel. Its text comes
|
|
3010
3010
|
from <code>tree.label</code>; without one it falls back to your first visible column, which
|
|
3011
|
-
then appears twice until you hide it
|
|
3011
|
+
then appears twice until you hide it, the grid does not remove a column you did not ask it
|
|
3012
3012
|
to remove.
|
|
3013
3013
|
</p>
|
|
3014
3014
|
<h3 id="tree-lazy">Loading a branch on demand</h3>
|
|
@@ -3022,7 +3022,7 @@ grid.edit.status('r1', 'cap'); <span class="cmt">// 'pending' | null</span></
|
|
|
3022
3022
|
</div>
|
|
3023
3023
|
<p class="lead-in">
|
|
3024
3024
|
<code>hasChildren</code> lets a row declare children it does not hold, so the expander is
|
|
3025
|
-
there before anything is fetched
|
|
3025
|
+
there before anything is fetched: without it there is nothing to click and the branch can
|
|
3026
3026
|
never load. Such a node reads as <em>closed</em> even though tree nodes are otherwise expanded
|
|
3027
3027
|
by default, because an open branch with nothing under it leaves no gesture to load it.
|
|
3028
3028
|
</p>
|
|
@@ -3046,14 +3046,14 @@ grid.edit.status('r1', 'cap'); <span class="cmt">// 'pending' | null</span></
|
|
|
3046
3046
|
|
|
3047
3047
|
<h2 id="master-detail">Master-detail</h2>
|
|
3048
3048
|
<p class="lead-in">
|
|
3049
|
-
A master row expands to reveal a detail region
|
|
3049
|
+
A master row expands to reveal a detail region: by default a nested grid over whatever
|
|
3050
3050
|
<code>detail.rows(row)</code> returns, which may be a promise. The grid adds an expander
|
|
3051
3051
|
column while the feature is on, on the same terms as the auto-group and selection columns:
|
|
3052
3052
|
pinned to the start, and absent from <code>columns.visible()</code>, saved views, exports and
|
|
3053
3053
|
the tool panel.
|
|
3054
3054
|
</p>
|
|
3055
3055
|
<div class="example">
|
|
3056
|
-
<p class="example__label">Inline
|
|
3056
|
+
<p class="example__label">Inline, a detail row beneath its master</p>
|
|
3057
3057
|
<pre><code>detail: {
|
|
3058
3058
|
rows: (row) => api.lines(row.data.id), <span class="cmt">// array or promise</span>
|
|
3059
3059
|
config: { columns: [{ field: 'port' }, { field: 'vlan' }] },
|
|
@@ -3074,7 +3074,7 @@ grid.detail.closeAll();</code></pre>
|
|
|
3074
3074
|
|
|
3075
3075
|
<h3 id="detail-target">A detail pane instead of a detail row</h3>
|
|
3076
3076
|
<div class="example">
|
|
3077
|
-
<p class="example__label">Targeted
|
|
3077
|
+
<p class="example__label">Targeted, the list-and-pane layout</p>
|
|
3078
3078
|
<pre><code>detail: {
|
|
3079
3079
|
target: '#detail-pane', <span class="cmt">// a selector or an element</span>
|
|
3080
3080
|
rows: (row) => api.lines(row.data.id),
|
|
@@ -3097,12 +3097,12 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
|
|
|
3097
3097
|
</p>
|
|
3098
3098
|
<p class="lead-in">
|
|
3099
3099
|
A <code>target</code> selector that matches no element is reported once and leaves the
|
|
3100
|
-
details unshown
|
|
3100
|
+
details unshown: silence there is indistinguishable from a detail that fails to open, and
|
|
3101
3101
|
the cause is not visible from the grid.
|
|
3102
3102
|
</p>
|
|
3103
3103
|
<p class="lead-in">
|
|
3104
3104
|
<strong>The control changes with the placement, because the gesture does.</strong> Inline it
|
|
3105
|
-
is a chevron that turns down when the row expands, carrying <code>aria-expanded</code
|
|
3105
|
+
is a chevron that turns down when the row expands, carrying <code>aria-expanded</code>: the
|
|
3106
3106
|
ordinary disclosure pattern. Targeted, nothing expands: the row is being chosen and its
|
|
3107
3107
|
detail appears elsewhere, so the control becomes the “opens elsewhere” glyph and
|
|
3108
3108
|
a toggle (<code>aria-pressed</code>) rather than a disclosure. A chevron there would promise
|
|
@@ -3113,7 +3113,7 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
|
|
|
3113
3113
|
|
|
3114
3114
|
<h3 id="detail-editing">An editable detail</h3>
|
|
3115
3115
|
<p class="lead-in">
|
|
3116
|
-
The detail is a whole grid, so it edits like one
|
|
3116
|
+
The detail is a whole grid, so it edits like one: put <code>edit</code> in
|
|
3117
3117
|
<code>detail.config</code> and its cells are editable. The rows it shows are usually a
|
|
3118
3118
|
sub-array of the master's own record, so an edit there changes the master's data directly;
|
|
3119
3119
|
there is nothing to copy back.
|
|
@@ -3129,16 +3129,16 @@ grid.detail.placement(); <span class="cmt">// 'inline' | 'targe
|
|
|
3129
3129
|
}
|
|
3130
3130
|
|
|
3131
3131
|
grid.on('detail:cell:changed', (e) => {
|
|
3132
|
-
e.masterKey; <span class="cmt">// 'C1'
|
|
3133
|
-
e.path; <span class="cmt">// 'ports.1.vlan'
|
|
3132
|
+
e.masterKey; <span class="cmt">// 'C1' , the row the detail belongs to</span>
|
|
3133
|
+
e.path; <span class="cmt">// 'ports.1.vlan': where it lands on the master's record</span>
|
|
3134
3134
|
e.value; <span class="cmt">// 999</span>
|
|
3135
3135
|
e.oldValue; <span class="cmt">// 101</span>
|
|
3136
3136
|
});</code></pre>
|
|
3137
3137
|
</div>
|
|
3138
3138
|
<p class="lead-in">
|
|
3139
3139
|
A nested grid is created by the grid, not by you, so its own events would otherwise be out of
|
|
3140
|
-
reach. The edit lifecycle
|
|
3141
|
-
<code>detail:edit:stopped</code>, <code>detail:cell:changed</code>
|
|
3140
|
+
reach. The edit lifecycle (<code>detail:edit:started</code>,
|
|
3141
|
+
<code>detail:edit:stopped</code>, <code>detail:cell:changed</code>) is re-emitted on the
|
|
3142
3142
|
master, tagged with the master it came from. You never have to hold the nested grid to hear
|
|
3143
3143
|
about an edit inside it.
|
|
3144
3144
|
</p>
|
|
@@ -3147,7 +3147,7 @@ grid.on('detail:cell:changed', (e) => {
|
|
|
3147
3147
|
a host can persist a detail edit against the master and never think about the nested grid at
|
|
3148
3148
|
all. It is worked out by identity: <code>rows(row)</code> usually returns an array that is
|
|
3149
3149
|
already a property of the record, and that property is the prefix. A detail fetched from a
|
|
3150
|
-
server is not part of the master's record, so its <code>path</code> is <code>null</code>
|
|
3150
|
+
server is not part of the master's record, so its <code>path</code> is <code>null</code> ,
|
|
3151
3151
|
set <code>detail.path</code> to name it yourself when you want one anyway.
|
|
3152
3152
|
</p>
|
|
3153
3153
|
<p class="lead-in">
|
|
@@ -3173,33 +3173,33 @@ grid.on('detail:cell:changed', (e) => {
|
|
|
3173
3173
|
|
|
3174
3174
|
<h2 id="selection-guide">Selection and ranges</h2>
|
|
3175
3175
|
<p class="lead-in">
|
|
3176
|
-
Row selection and cell ranges are separate answers to separate questions
|
|
3176
|
+
Row selection and cell ranges are separate answers to separate questions ,
|
|
3177
3177
|
<em>which records</em> versus <em>which values</em>. Dragging across cells does not tick row
|
|
3178
3178
|
checkboxes, and selecting rows does not build a range.
|
|
3179
3179
|
</p>
|
|
3180
3180
|
<div class="note">
|
|
3181
3181
|
<p><strong>Cell ranges are on by default; row selection is not.</strong> Set
|
|
3182
|
-
<code>selection: 'single'</code> or <code>'multiple'</code> to turn rows on
|
|
3182
|
+
<code>selection: 'single'</code> or <code>'multiple'</code> to turn rows on: until you do,
|
|
3183
3183
|
<code>selection.set()</code> accepts the call and <code>keys()</code> comes back empty.</p>
|
|
3184
3184
|
</div>
|
|
3185
3185
|
<div class="example">
|
|
3186
3186
|
<p class="example__label">Both</p>
|
|
3187
3187
|
<pre><code>selection: 'multiple' <span class="cmt">// rows are off until you ask</span>
|
|
3188
3188
|
|
|
3189
|
-
<span class="cmt">// Rows
|
|
3189
|
+
<span class="cmt">// Rows, which records</span>
|
|
3190
3190
|
grid.selection.set(['r1', 'r2']);
|
|
3191
3191
|
grid.selection.keys(); <span class="cmt">// the selected row keys</span>
|
|
3192
3192
|
grid.selection.rows(); <span class="cmt">// the row wrappers</span>
|
|
3193
3193
|
grid.selection.all(); <span class="cmt">// select everything that passes the filter</span>
|
|
3194
3194
|
grid.selection.clear();
|
|
3195
3195
|
|
|
3196
|
-
<span class="cmt">// Cells
|
|
3196
|
+
<span class="cmt">// Cells, which values</span>
|
|
3197
3197
|
grid.selection.setRange({ startRow: 0, endRow: 9, columns: ['cap', 'margin'] });
|
|
3198
3198
|
grid.selection.cells(); <span class="cmt">// [{ key, colId }, …]</span>
|
|
3199
3199
|
grid.selection.summary(); <span class="cmt">// count, sum, min, max, avg over the range</span>
|
|
3200
3200
|
grid.selection.clearRange();
|
|
3201
3201
|
|
|
3202
|
-
<span class="cmt">// Several blocks at once
|
|
3202
|
+
<span class="cmt">// Several blocks at once: ctrl-click, Ctrl+Shift+Arrow, or from the API</span>
|
|
3203
3203
|
grid.selection.addRange({ startRow: 20, endRow: 29, columns: ['cap'] });
|
|
3204
3204
|
grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block just added</span></code></pre>
|
|
3205
3205
|
</div>
|
|
@@ -3213,7 +3213,7 @@ grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block jus
|
|
|
3213
3213
|
pinned so it does not scroll away. <code>headerCheckbox: true</code> puts a select-all box in
|
|
3214
3214
|
its heading, which shows three states: unchecked when nothing is selected, checked when
|
|
3215
3215
|
everything is, and the native indeterminate mark when some are. Clicking it selects
|
|
3216
|
-
everything when it is not already full, and clears when it is
|
|
3216
|
+
everything when it is not already full, and clears when it is: including from the
|
|
3217
3217
|
indeterminate state, where the intent is "select the rest".
|
|
3218
3218
|
</p>
|
|
3219
3219
|
<p class="lead-in">
|
|
@@ -3239,7 +3239,7 @@ grid.selection.extendRange(34, 'cap'); <span class="cmt">// grows the block jus
|
|
|
3239
3239
|
<p><strong>What multiple ranges do and do not support.</strong> Painting, <code>cells()</code>
|
|
3240
3240
|
and the status-bar summary all work over the union of the selected blocks. Copy is narrower,
|
|
3241
3241
|
because tab-separated text is a rectangle: blocks stacked over the same columns, or joined
|
|
3242
|
-
over the same rows, copy fine
|
|
3242
|
+
over the same rows, copy fine, a diagonal pair has no rectangular form, so
|
|
3243
3243
|
<code>rangeText()</code> returns <code>''</code> and <code>clipboard:copy</code> reports
|
|
3244
3244
|
<code>reason: 'discontiguous'</code> rather than emitting misaligned rows. Filling is
|
|
3245
3245
|
narrower still: with more than one block selected there is no single source to extend, so the
|
|
@@ -3269,12 +3269,12 @@ selection: { ranges: false } <span class="cmt">// rows only, no drag-selec
|
|
|
3269
3269
|
<div class="why">
|
|
3270
3270
|
<p><strong>Months step as months.</strong> 15 January to 15 February is thirty-one days, and
|
|
3271
3271
|
continuing in days would land on 18 March and drift further every step. A month step holds
|
|
3272
|
-
the date the user picked, and clamps where the month is short
|
|
3272
|
+
the date the user picked, and clamps where the month is short: 31 January plus a month is
|
|
3273
3273
|
28 February, not 3 March.</p>
|
|
3274
3274
|
<p><strong>Month ends are their own case.</strong> <code>31 Jan, 28 Feb</code> is a month
|
|
3275
3275
|
series whose second value has already been clamped, so the two share no day of month and the
|
|
3276
3276
|
day-preserving rule cannot see it. Where every source value is the last day of its own month,
|
|
3277
|
-
the fill stays on the month end: 31 March, 30 April, 31 May
|
|
3277
|
+
the fill stays on the month end: 31 March, 30 April, 31 May, and 29 February in a leap year.
|
|
3278
3278
|
Values that share a day of month keep the day-preserving answer, so <code>30 Apr, 30 Jun</code>
|
|
3279
3279
|
still gives 30 August rather than the 31st.</p>
|
|
3280
3280
|
<p><strong>One limit worth knowing.</strong> Filling upwards is not supported; the handle
|
|
@@ -3287,7 +3287,7 @@ selection: { ranges: false } <span class="cmt">// rows only, no drag-selec
|
|
|
3287
3287
|
}</code></pre>
|
|
3288
3288
|
</div>
|
|
3289
3289
|
<div class="why">
|
|
3290
|
-
<p>A domain series
|
|
3290
|
+
<p>A domain series (order codes, fiscal periods, seat numbers) is not something the grid
|
|
3291
3291
|
can infer, so <code>selection.fill</code> takes precedence when you supply it. It must return
|
|
3292
3292
|
one value per target row; anything else is ignored in favour of the built-in detection,
|
|
3293
3293
|
rather than being partly applied.</p>
|
|
@@ -3296,7 +3296,7 @@ selection: { ranges: false } <span class="cmt">// rows only, no drag-selec
|
|
|
3296
3296
|
<h2 id="updates-guide">Holding live updates</h2>
|
|
3297
3297
|
<p class="lead-in">
|
|
3298
3298
|
A pause button for incoming data. Changes are held and merged while paused, applied when
|
|
3299
|
-
play is pressed, and counted throughout
|
|
3299
|
+
play is pressed, and counted throughout, so the coalescing that makes a live grid fast is
|
|
3300
3300
|
finally visible.
|
|
3301
3301
|
</p>
|
|
3302
3302
|
<div class="example">
|
|
@@ -3318,7 +3318,7 @@ grid.updates.log({ since: Date.now() - 60000 }); <span class="cmt">// what arr
|
|
|
3318
3318
|
no invisible state to explain.</p>
|
|
3319
3319
|
<p><strong>Merging continues while paused</strong>, so a long pause costs one entry per
|
|
3320
3320
|
changed row rather than one per update. Forty updates to one row are one row of work when
|
|
3321
|
-
play is pressed
|
|
3321
|
+
play is pressed, and <code>coalesced</code> is the thirty-nine, which is the number nobody
|
|
3322
3322
|
could see before.</p>
|
|
3323
3323
|
<p><strong>Bounding the rows themselves is separate.</strong> The log bounds change
|
|
3324
3324
|
<em>history</em>; a streaming source also needs to bound row <em>retention</em>, or a grid
|
|
@@ -3327,8 +3327,8 @@ grid.updates.log({ since: Date.now() - 60000 }); <span class="cmt">// what arr
|
|
|
3327
3327
|
many it let go through <code>evicted</code> on the progress report.</p>
|
|
3328
3328
|
<p><strong>The log keeps the raw sequence, not the merged one.</strong> Merging is right for
|
|
3329
3329
|
applying a backlog quickly and wrong for looking at what happened, because the intermediate
|
|
3330
|
-
states are exactly what a time scrubber would move between. It survives the flush
|
|
3331
|
-
<code>pending</code> is what is waiting, the log is what happened
|
|
3330
|
+
states are exactly what a time scrubber would move between. It survives the flush ,
|
|
3331
|
+
<code>pending</code> is what is waiting, the log is what happened, and it is capped, so a
|
|
3332
3332
|
grid paused over lunch holds the recent past and reports how much it dropped rather than
|
|
3333
3333
|
taking the tab with it.</p>
|
|
3334
3334
|
</div>
|
|
@@ -3355,29 +3355,29 @@ expect(grid.diagnostics.renders().dom.cellWrites - before).toBeLessThan(200);</c
|
|
|
3355
3355
|
reachable from the public API. Exposing what a system already knows is usually a better first
|
|
3356
3356
|
move than measuring something new.</p>
|
|
3357
3357
|
<p><strong>Warnings are mostly collection, not detection.</strong> The grid has 160 places
|
|
3358
|
-
that warn once per cause, each already carrying a stable de-duplication key
|
|
3358
|
+
that warn once per cause, each already carrying a stable de-duplication key, which is
|
|
3359
3359
|
exactly the stable identifier a support conversation needs. They went to the console and
|
|
3360
3360
|
nowhere else. The console interleaves with your own logging, does not survive a reload, and
|
|
3361
3361
|
cannot be asked what it has already complained about. They are now kept as records too.</p>
|
|
3362
3362
|
<p><strong>Every warning names values, not just a condition.</strong> "Something is slow" is a
|
|
3363
3363
|
warning nobody can act on. Each carries the specific numbers, a stable id, and is dismissible
|
|
3364
|
-
for the session but not permanently
|
|
3364
|
+
for the session but not permanently, a permanently dismissible warning is one nobody sees
|
|
3365
3365
|
again after the person who dismissed it leaves.</p>
|
|
3366
3366
|
<p><strong>The checks are tested for silence as much as for detection.</strong> A clean grid
|
|
3367
3367
|
must raise nothing. A checker that cries wolf is one developers learn to ignore, and then it
|
|
3368
3368
|
is worth less than no checker at all. The accessibility checks found two false positives in
|
|
3369
|
-
themselves during development
|
|
3369
|
+
themselves during development: first comparing <code>aria-rowcount</code> against the row
|
|
3370
3370
|
count when ARIA counts header rows too, then counting header row *elements*, of which a
|
|
3371
3371
|
single-level header has three because the header is built once per pinned region.</p>
|
|
3372
3372
|
<p><strong>Instrumentation must not change what it measures.</strong> Counters are integers
|
|
3373
3373
|
incremented where the work already happened. Render phases are four <code>performance.now()</code>
|
|
3374
|
-
marks around existing sections. Timings are sampled
|
|
3374
|
+
marks around existing sections. Timings are sampled, a bounded window of recent operations ,
|
|
3375
3375
|
and every report names which of its figures are sampled, because a number whose provenance is
|
|
3376
3376
|
unclear is worse than no number. Paint wait is the browser's and is deliberately not claimed.</p>
|
|
3377
3377
|
<p><strong>Render causes are captured, not inferred.</strong> By the time a paint runs,
|
|
3378
3378
|
several distinct causes have collapsed into the same dirty flags, so working backwards gives a
|
|
3379
3379
|
plausible answer rather than a true one. The renderer records the structural reason when the
|
|
3380
|
-
invalidation arrives; the semantic one
|
|
3380
|
+
invalidation arrives; the semantic one (filter, sort, data) is only knowable from the event
|
|
3381
3381
|
that preceded it, so the DOM layer supplies that and the structural reason is the fallback.</p>
|
|
3382
3382
|
<p><strong>Providers are wrapped where they are installed</strong>, not at each call site, so
|
|
3383
3383
|
one added later is instrumented by construction. The wrapper returns exactly what the original
|
|
@@ -3396,7 +3396,7 @@ expect(grid.diagnostics.renders().dom.cellWrites - before).toBeLessThan(200);</c
|
|
|
3396
3396
|
because it imports it. A devtools module that imported anything from core would put the whole grid inside a
|
|
3397
3397
|
bundle whose entire promise is that deployments not using it pay nothing.</p>
|
|
3398
3398
|
<p><strong>The support bundle carries no row data.</strong> Configuration, query state, timing,
|
|
3399
|
-
warnings, provider statistics, version and environment
|
|
3399
|
+
warnings, provider statistics, version and environment, and nothing from your data. Stated as
|
|
3400
3400
|
a guarantee because a bundle that had to be inspected for confidential values before sending
|
|
3401
3401
|
is a bundle that never gets attached to the ticket.</p>
|
|
3402
3402
|
<p><strong>It observes and never mutates.</strong> A configuration editor in a debug panel is
|
|
@@ -3422,13 +3422,13 @@ const providerFor = (id) => ({
|
|
|
3422
3422
|
<p><strong>Presence carries intent, never values.</strong> This is the line that matters most.
|
|
3423
3423
|
A peer's committed edit reaches the grid as data, through the channel you already use for
|
|
3424
3424
|
data. Presence is throttled, lossy and ephemeral <em>by design</em>, so a value carried on it
|
|
3425
|
-
is a value that can be dropped
|
|
3425
|
+
is a value that can be dropped, and that is the class of bug that appears once a month
|
|
3426
3426
|
in production and cannot be reproduced on demand.</p>
|
|
3427
3427
|
<p><strong>Positions are row keys, resolved against your view at render time.</strong> Peers
|
|
3428
3428
|
sort and filter independently, so index 12 is a different record on every screen. Publishing
|
|
3429
3429
|
an index would put a colleague's cursor on an unrelated row the moment either of you sorted.
|
|
3430
|
-
The cost of this is real
|
|
3431
|
-
|
|
3430
|
+
The cost of this is real, the grid resolves a key to a position rather than reading one
|
|
3431
|
+
, and it is the difference between the feature working and the feature lying.</p>
|
|
3432
3432
|
<p><strong>Idle is measured from when a message arrived, not from what it says.</strong>
|
|
3433
3433
|
Clocks between clients disagree by seconds routinely and by minutes occasionally. Keying idle
|
|
3434
3434
|
detection on the sender's timestamp means a peer with a fast clock never goes idle and one
|
|
@@ -3442,19 +3442,19 @@ const providerFor = (id) => ({
|
|
|
3442
3442
|
roster filling in slowly.</p>
|
|
3443
3443
|
<p><strong>A peer's cursor is dashed; your focus ring is solid.</strong> The distinction has to
|
|
3444
3444
|
be in the kind of line, not only its colour. Colour alone fails for anyone who cannot separate
|
|
3445
|
-
two hues and fails for everyone at a glance
|
|
3445
|
+
two hues and fails for everyone at a glance, and the palette originally contained the
|
|
3446
3446
|
exact value of <code>--lattice-focus-color</code>, so the first peer assigned drew a cursor
|
|
3447
3447
|
identical to the local user's own selection. An active edit is solid and tinted, because an
|
|
3448
3448
|
edit is not a cursor and those two must not be confused with each other either.</p>
|
|
3449
3449
|
<p><strong>Nothing is inserted into the grid.</strong> Every treatment is an attribute and a
|
|
3450
3450
|
custom property written onto a cell that already exists, drawn with an outline and a
|
|
3451
3451
|
pseudo-element. That is what keeps presence from shifting layout, covering an in-cell chart or
|
|
3452
|
-
swallowing a click
|
|
3452
|
+
swallowing a click: none of which survives an implementation that appends overlay
|
|
3453
3453
|
elements. The overlay layer is pointer-transparent and presence deliberately does not opt back
|
|
3454
3454
|
in; only the roster does, because it is a control.</p>
|
|
3455
3455
|
<p><strong>The roster is the part people use.</strong> More than the cursors, in practice. It
|
|
3456
3456
|
carries the name as well as the colour, because colour alone is not a signal everyone can
|
|
3457
|
-
read, and it reports peers whose rows are not in your view rather than omitting them
|
|
3457
|
+
read, and it reports peers whose rows are not in your view rather than omitting them ,
|
|
3458
3458
|
an absent peer reads as a disconnection that has not happened.</p>
|
|
3459
3459
|
<p><strong>A parked cursor does not fade.</strong> The label does, after a couple of seconds,
|
|
3460
3460
|
because permanent labels over a dense grid are unreadable. The border stays, dims at idle, and
|
|
@@ -3504,12 +3504,12 @@ const providerFor = (id) => ({
|
|
|
3504
3504
|
<p><strong>Stable row identity is a hard requirement, enforced rather than documented.</strong>
|
|
3505
3505
|
A comment is keyed on row identity plus field. Row index changes under sort, filter and
|
|
3506
3506
|
grouping, so a comment keyed on it reattaches to whichever row later occupies that position
|
|
3507
|
-
|
|
3507
|
+
, and a comment on the wrong row is worse than no comment at all. A grid with no
|
|
3508
3508
|
<code>rowKey</code> disables comments and names them in the same single warning as the other
|
|
3509
3509
|
identity-dependent features.</p>
|
|
3510
3510
|
<p><strong>Identity has to survive more than the session.</strong> Comments outlive the page
|
|
3511
|
-
that wrote them, so a key that is stable only within one load
|
|
3512
|
-
arrival order, for instance
|
|
3511
|
+
that wrote them, so a key that is stable only within one load (anything derived from
|
|
3512
|
+
arrival order, for instance) is not enough. Reload the data in a different order and
|
|
3513
3513
|
every thread points somewhere else. If you have only used the grid client-side you may never
|
|
3514
3514
|
have needed a durable identity before; you do now.</p>
|
|
3515
3515
|
<p><strong>Writes are optimistic, and rejections are taken back.</strong> The author sees
|
|
@@ -3523,14 +3523,14 @@ const providerFor = (id) => ({
|
|
|
3523
3523
|
console. Reject in the provider.</p>
|
|
3524
3524
|
<p><strong>A body is user input that has round-tripped through your storage.</strong> That is
|
|
3525
3525
|
the exact shape of a stored cross-site script, so the default path sets text and nothing else.
|
|
3526
|
-
Turning on <code>markdown</code> buys emphasis, code and links
|
|
3526
|
+
Turning on <code>markdown</code> buys emphasis, code and links: three constructs, built
|
|
3527
3527
|
as elements rather than parsed as markup, with any scheme other than <code>http</code>,
|
|
3528
3528
|
<code>https</code> and <code>mailto</code> refused. A refused link still shows its label, so
|
|
3529
3529
|
nothing the author wrote vanishes without trace.</p>
|
|
3530
3530
|
<p><strong>The marker cannot move the cell's contents.</strong> It is a corner triangle drawn
|
|
3531
3531
|
with a border on a pseudo-element, so it occupies no space in the layout: no shifted text, no
|
|
3532
3532
|
rewrapped number, no displaced sparkline. That constraint is why it is a corner rather than a
|
|
3533
|
-
badge
|
|
3533
|
+
badge: every other position in a cell is already spoken for. Only the corner opens a
|
|
3534
3534
|
thread; a click elsewhere belongs to selection, and taking it would make commented cells
|
|
3535
3535
|
behave unlike every other cell. The hit region is larger than the drawn mark, because at
|
|
3536
3536
|
compact density the triangle is about seven pixels across.</p>
|
|
@@ -3538,7 +3538,7 @@ const providerFor = (id) => ({
|
|
|
3538
3538
|
matches the cell. Without it, a note reading "this looks too high" sits beside a number it
|
|
3539
3539
|
never described and the reader concludes the comment is wrong. Changing a value never deletes
|
|
3540
3540
|
or invalidates a comment.</p>
|
|
3541
|
-
<p><strong>Filtered-out comments are hidden, not lost
|
|
3541
|
+
<p><strong>Filtered-out comments are hidden, not lost, and the grid says so.</strong>
|
|
3542
3542
|
<code>hiddenUnresolved()</code> reports what is still outstanding on rows the filter is
|
|
3543
3543
|
hiding, because a user who filters and sees no markers should not conclude there is nothing
|
|
3544
3544
|
left to deal with. The status bar carries this: its <code>comments</code> panel reads
|
|
@@ -3549,11 +3549,11 @@ const providerFor = (id) => ({
|
|
|
3549
3549
|
<pre><code>statusBar: { panels: ['rowCount', 'comments'] }</code></pre>
|
|
3550
3550
|
</div>
|
|
3551
3551
|
<p>The count returns zero rather than a number it cannot stand behind: it is only meaningful
|
|
3552
|
-
once the index covers every row, so it stays at zero
|
|
3552
|
+
once the index covers every row, so it stays at zero, and the panel stays silent ,
|
|
3553
3553
|
until <code>loadAll()</code> has resolved.</p>
|
|
3554
3554
|
<p><strong>The comments-only filter is refused rather than approximated.</strong> Restricting
|
|
3555
3555
|
the grid to rows carrying comments needs the index to cover the whole row set, not just what
|
|
3556
|
-
has been scrolled past
|
|
3556
|
+
has been scrolled past, a partial answer would hide precisely the rows the user opened
|
|
3557
3557
|
it to find. Call <code>loadAll()</code> first; until <code>complete</code> is true,
|
|
3558
3558
|
<code>filterToCommented()</code> returns false and does nothing.</p>
|
|
3559
3559
|
<p><strong>Comments stay available while streaming</strong>, unlike header histograms: a
|
|
@@ -3561,7 +3561,7 @@ const providerFor = (id) => ({
|
|
|
3561
3561
|
debounced viewport path. A thread whose row is evicted by a bounded window closes with a
|
|
3562
3562
|
short explanation rather than hovering over a row that has gone.</p>
|
|
3563
3563
|
<p><strong>Keyboard and screen reader.</strong> <kbd>Alt</kbd>+<kbd>M</kbd> opens the thread
|
|
3564
|
-
on the focused cell
|
|
3564
|
+
on the focused cell: Alt because the grid binds nearly every unmodified key to
|
|
3565
3565
|
navigation and editing. The panel traps <kbd>Tab</kbd>, which it has to: the grid behind it is
|
|
3566
3566
|
still there and still focusable, so without the trap a keyboard user would be moving through
|
|
3567
3567
|
cells with a dialog open over them. Focus returns to the originating cell on close rather than
|
|
@@ -3571,7 +3571,7 @@ const providerFor = (id) => ({
|
|
|
3571
3571
|
other than what it holds.</p>
|
|
3572
3572
|
<p><strong>Not in this release:</strong> mentions, notifications, rich text, attachments,
|
|
3573
3573
|
reactions, row-level and column-level comments, and export of comments. The grid opens no
|
|
3574
|
-
transport of its own
|
|
3574
|
+
transport of its own: if your application pushes updates, call <code>refresh()</code>
|
|
3575
3575
|
and the index reloads.</p>
|
|
3576
3576
|
</div>
|
|
3577
3577
|
|
|
@@ -3592,11 +3592,11 @@ const providerFor = (id) => ({
|
|
|
3592
3592
|
<p><strong>The column being filtered is not counted against its own filter.</strong> Every
|
|
3593
3593
|
other active filter applies; that column's own conditions are pruned out of the tree before
|
|
3594
3594
|
counting. This is the whole of faceted browsing and it is the part that is easy to get subtly
|
|
3595
|
-
wrong
|
|
3595
|
+
wrong, a self-filtered chart collapses to a single bar the moment you click one, and
|
|
3596
3596
|
there is then no way to see what you excluded or to widen the selection. Getting it wrong
|
|
3597
3597
|
does not degrade the feature, it removes it.</p>
|
|
3598
3598
|
<p><strong>Pruning is not symmetric across operators.</strong> An <code>and</code> group
|
|
3599
|
-
narrows with each condition, so dropping one widens the result
|
|
3599
|
+
narrows with each condition, so dropping one widens the result, the direction faceting
|
|
3600
3600
|
wants. An <code>or</code> group widens with each branch, so dropping one would show
|
|
3601
3601
|
<em>fewer</em> rows than the user's actual filter. There is no partial answer that is correct,
|
|
3602
3602
|
so a disjunction naming the column is dropped whole.</p>
|
|
@@ -3606,7 +3606,7 @@ const providerFor = (id) => ({
|
|
|
3606
3606
|
because the thing you are pointing at would move as you pointed at it.</p>
|
|
3607
3607
|
<p><strong>Each bar carries two readings.</strong> Its full height is the bucket's share of
|
|
3608
3608
|
the unfiltered column; the solid fill inside is how much survives the current filters. Either
|
|
3609
|
-
alone misleads
|
|
3609
|
+
alone misleads: scaling to the filtered maximum draws a full-height chart out of three
|
|
3610
3610
|
surviving rows, and scaling everything down together flattens the whole chart into a few
|
|
3611
3611
|
pixels the moment anyone filters anything.</p>
|
|
3612
3612
|
<p><strong>The filters are ordinary filters.</strong> They go through the same
|
|
@@ -3623,7 +3623,7 @@ const providerFor = (id) => ({
|
|
|
3623
3623
|
than a scan. The first column anyone points this at is a name or an id, and one hairline per
|
|
3624
3624
|
customer looks like a rendering fault rather than a distribution. Above
|
|
3625
3625
|
<code>cardinalityLimit</code> the chart is suppressed, or shows a top-N with an aggregated
|
|
3626
|
-
remainder if you ask for <code>aboveLimit: 'topN'</code
|
|
3626
|
+
remainder if you ask for <code>aboveLimit: 'topN'</code>: aggregated rather than
|
|
3627
3627
|
truncated, because silently dropping the tail would misrepresent the bars it did draw.</p>
|
|
3628
3628
|
<p><strong>Nulls are never dropped.</strong> They land in a terminal bucket, always last, and
|
|
3629
3629
|
the counts always sum to the row count. A column where nine thousand of ten thousand rows are
|
|
@@ -3633,8 +3633,8 @@ const providerFor = (id) => ({
|
|
|
3633
3633
|
<p><strong>Live streams suppress the charts.</strong> Constantly shifting distributions are
|
|
3634
3634
|
unreadable, recounting on every batch is wasteful, and a filter control whose buckets move
|
|
3635
3635
|
under the pointer is actively hostile. Filters already made stay applied, because they are
|
|
3636
|
-
ordinary filters. Pausing the stream brings the charts back
|
|
3637
|
-
one
|
|
3636
|
+
ordinary filters. Pausing the stream brings the charts back, a paused stream is a still
|
|
3637
|
+
one: unless you set <code>whilePaused: false</code>.</p>
|
|
3638
3638
|
<p><strong>Counting runs off the main thread above <code>workerThreshold</code>.</strong>
|
|
3639
3639
|
Distributions are the only work the grid moves to a Worker. Sorting, filtering and grouping
|
|
3640
3640
|
run on the main thread; nothing waits on a histogram, which is what makes this one
|
|
@@ -3643,24 +3643,24 @@ const providerFor = (id) => ({
|
|
|
3643
3643
|
detach the buffer the grid is still rendering from.</p>
|
|
3644
3644
|
<p><strong>The Worker settings.</strong> <code>useWorker</code> and
|
|
3645
3645
|
<code>workerThreshold</code> decide whether and when a distribution is offloaded. Two more
|
|
3646
|
-
control how the Worker is built, and both are settled when it is constructed
|
|
3646
|
+
control how the Worker is built, and both are settled when it is constructed: changing
|
|
3647
3647
|
either discards the running Worker so the next offload builds a new one.</p>
|
|
3648
3648
|
<div class="table-wrap">
|
|
3649
3649
|
<table>
|
|
3650
3650
|
<thead><tr><th>Setting</th><th>What it does</th></tr></thead>
|
|
3651
3651
|
<tbody>
|
|
3652
|
-
<tr><td class="name"><code>workerUrl</code></td><td class="desc">Loads the Worker from a URL you host instead of a <code>blob:</code>. Required under a Content-Security-Policy that forbids <code>blob:</code> workers
|
|
3652
|
+
<tr><td class="name"><code>workerUrl</code></td><td class="desc">Loads the Worker from a URL you host instead of a <code>blob:</code>. Required under a Content-Security-Policy that forbids <code>blob:</code> workers: without it the Worker cannot be constructed at all on such a page, and compute stays on the main thread.</td></tr>
|
|
3653
3653
|
<tr><td class="name"><code>sharedMemory</code></td><td class="desc">Off by default. Passes columns to the Worker in a <code>SharedArrayBuffer</code> rather than copying them on every message, at the cost of retaining a shared copy of each column that crosses. Needs the page to be cross-origin isolated; where it is not, it falls back to copying and says so once.</td></tr>
|
|
3654
3654
|
</tbody>
|
|
3655
3655
|
</table>
|
|
3656
3656
|
</div>
|
|
3657
3657
|
<p><code>grid.diagnostics.renders().worker</code> reports what the Worker host is actually
|
|
3658
|
-
doing
|
|
3658
|
+
doing: whether one was spawned, how many calls ran locally versus remotely, and the
|
|
3659
3659
|
threshold, <code>sharedMemory</code> and <code>workerUrl</code> it was built with.</p>
|
|
3660
3660
|
<p><strong>Server-side sources need a <code>provider</code>, and its absence is silent.</strong>
|
|
3661
3661
|
A grid holding one page of data cannot compute a distribution over the whole set. Supply a
|
|
3662
3662
|
function and it receives the column, the pruned filter state and the bucketing settings, and
|
|
3663
|
-
returns counts. Without one the charts are simply absent
|
|
3663
|
+
returns counts. Without one the charts are simply absent, no error, because most
|
|
3664
3664
|
deployments will never supply one. Be clear-eyed about the load: results are cached against
|
|
3665
3665
|
the filter state, but this is one query per column per filter change, and a grid with eight
|
|
3666
3666
|
faceted columns asks eight questions every time a filter moves.</p>
|
|
@@ -3669,13 +3669,13 @@ const providerFor = (id) => ({
|
|
|
3669
3669
|
toggles, <kbd>Shift</kbd> with arrows extends a range on ordered columns, <kbd>Escape</kbd>
|
|
3670
3670
|
clears. Selected buckets carry an outline as well as a colour. Beyond per-bucket labels the
|
|
3671
3671
|
chart carries a sentence describing the distribution's shape, because twenty bucket readings
|
|
3672
|
-
do not add up to "most of the mass is at the low end"
|
|
3672
|
+
do not add up to "most of the mass is at the low end", and that shape is the entire
|
|
3673
3673
|
value of the chart.</p>
|
|
3674
3674
|
</div>
|
|
3675
3675
|
|
|
3676
3676
|
<h2 id="timeline-guide">Time scrubber</h2>
|
|
3677
3677
|
<p class="lead-in">
|
|
3678
|
-
Move the grid back through recent data changes
|
|
3678
|
+
Move the grid back through recent data changes: what did this look like a minute ago,
|
|
3679
3679
|
before that number moved.
|
|
3680
3680
|
</p>
|
|
3681
3681
|
<div class="example">
|
|
@@ -3691,17 +3691,17 @@ grid.timeline.detach();</code></pre>
|
|
|
3691
3691
|
</div>
|
|
3692
3692
|
<div class="why">
|
|
3693
3693
|
<p><strong>Attaching puts a control on the grid.</strong> A slider along the bottom with two
|
|
3694
|
-
readings beside it
|
|
3694
|
+
readings beside it: how long ago, and the clock time. Relative answers the question actually
|
|
3695
3695
|
being asked; absolute is what someone reads out to the person next to them. It moves the grid
|
|
3696
3696
|
while the handle is dragged rather than on release, and it turns accent-coloured the moment
|
|
3697
3697
|
you are off live, because a grid quietly showing stale data is the failure this control can
|
|
3698
3698
|
cause. It removes itself on <code>detach()</code>.</p>
|
|
3699
3699
|
<p><strong>It reads the data, not your actions.</strong> Undo history records what the
|
|
3700
|
-
<em>user</em> did
|
|
3700
|
+
<em>user</em> did (sorts, filters, edits) which is rarely the question. This reads the
|
|
3701
3701
|
change log: a bounded, timestamped, deliberately unmerged record of everything that arrived,
|
|
3702
3702
|
so the intermediate states are all still there to move between.</p>
|
|
3703
3703
|
<p><strong>Nothing is scrubbable before <code>attach()</code>.</strong> What a value used to
|
|
3704
|
-
be is not recoverable after the fact
|
|
3704
|
+
be is not recoverable after the fact (no other part of the grid remembers it) so recording
|
|
3705
3705
|
has to be switched on before there is a past to move through. It is off by default because
|
|
3706
3706
|
reading a row per key on every change is real cost on a hot feed, and paying it for a
|
|
3707
3707
|
scrubber nobody opened would be the wrong default.</p>
|
|
@@ -3711,7 +3711,7 @@ grid.timeline.detach();</code></pre>
|
|
|
3711
3711
|
<p><strong>Value changes reverse; row additions and removals do not.</strong> An add would
|
|
3712
3712
|
need a removal and a remove would need re-insertion at its old position, and neither is
|
|
3713
3713
|
recoverable from what the log holds. A window containing them scrubs over the value changes
|
|
3714
|
-
and leaves the row set alone
|
|
3714
|
+
and leaves the row set alone: stated plainly because the alternative is a scrubber that
|
|
3715
3715
|
silently half-works.</p>
|
|
3716
3716
|
<p><strong>What moved is marked.</strong> Seeking compares each affected row before and after
|
|
3717
3717
|
and marks the cells whose value changed, in <code>--lattice-timeline-changed</code>. Without
|
|
@@ -3719,13 +3719,13 @@ grid.timeline.detach();</code></pre>
|
|
|
3719
3719
|
number you are hunting for goes past unseen.</p>
|
|
3720
3720
|
<p><strong>The mark is held, not flashed.</strong> It stays until the next seek clears it.
|
|
3721
3721
|
Every other transient signal in the grid fades on a timer, and this one deliberately does not
|
|
3722
|
-
|
|
3722
|
+
, a scrub is someone hunting for what changed, and a highlight they can miss while reading the
|
|
3723
3723
|
other end of the row helps nobody. <code>timeline:seeking</code> fires before any change is
|
|
3724
3724
|
applied, so a five-step drag clears once and marks once rather than strobing per entry.</p>
|
|
3725
3725
|
<p><strong>It compares column values, not raw fields.</strong> A computed column has no field
|
|
3726
3726
|
of its own; diffing the source row would leave it silently unmarked while its number visibly
|
|
3727
3727
|
moved. Reading through the column instead costs a little more and marks what the viewer can
|
|
3728
|
-
actually see change
|
|
3728
|
+
actually see change, which is the only definition of "changed" that matters here.</p>
|
|
3729
3729
|
<p><strong>The window is bounded by rows, not only by changes.</strong> A cap on entries
|
|
3730
3730
|
alone does not bound memory, because an entry is not a fixed size: one carrying a single
|
|
3731
3731
|
changed cell and one carrying a fifty-thousand-row batch both count as one. So the log holds
|
|
@@ -3734,7 +3734,7 @@ grid.timeline.detach();</code></pre>
|
|
|
3734
3734
|
here than it looks: the log is what keeps superseded row objects alive after the source has
|
|
3735
3735
|
swapped in their replacements, so on a feed delivering five thousand rows a batch an
|
|
3736
3736
|
entry-only cap retains twenty million of them. The one exception is a single change larger
|
|
3737
|
-
than the whole cap, which is kept
|
|
3737
|
+
than the whole cap, which is kept: emptying the log would be worse than being briefly over,
|
|
3738
3738
|
and it would drop the newest change rather than the oldest. Watch <code>held</code> against
|
|
3739
3739
|
<code>heldLimit</code> in <code>grid.updates.stats()</code>; <code>rows</code> is a lifetime
|
|
3740
3740
|
total and says nothing about memory.</p>
|
|
@@ -3766,14 +3766,14 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
|
|
|
3766
3766
|
<thead><tr><th>Keys</th><th>Does</th></tr></thead>
|
|
3767
3767
|
<tbody>
|
|
3768
3768
|
<tr><td class="sig">Escape</td><td class="desc">Leave, restoring the grid exactly as it was.</td></tr>
|
|
3769
|
-
<tr><td class="sig">Ctrl/Cmd + = / -</td><td class="desc">Enlarge or reduce live
|
|
3769
|
+
<tr><td class="sig">Ctrl/Cmd + = / -</td><td class="desc">Enlarge or reduce live, a laptop on a call and a projector at the back of a room are different problems.</td></tr>
|
|
3770
3770
|
<tr><td class="sig">Ctrl/Cmd + 0</td><td class="desc">Back to the default enlargement.</td></tr>
|
|
3771
3771
|
</tbody>
|
|
3772
3772
|
</table>
|
|
3773
3773
|
</div>
|
|
3774
3774
|
<div class="why">
|
|
3775
3775
|
<p><strong>The scale multiplies your density, it does not replace it.</strong> A grid built at
|
|
3776
|
-
<code>spacious</code> presented at 1.5x is still recognisably that grid, half as big again
|
|
3776
|
+
<code>spacious</code> presented at 1.5x is still recognisably that grid, half as big again ,
|
|
3777
3777
|
which is what makes a presentation look like the product rather than like a different one.
|
|
3778
3778
|
Virtualisation follows the enlargement, so rows are positioned at the size they are drawn.</p>
|
|
3779
3779
|
<p><strong>Full-screen is the maximiser, not a second implementation.</strong> A grid the user
|
|
@@ -3782,7 +3782,7 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
|
|
|
3782
3782
|
element the host had already hidden is not revealed on exit.</p>
|
|
3783
3783
|
<p><strong>Events</strong> are <code>presentation:started</code>,
|
|
3784
3784
|
<code>presentation:ended</code>, <code>presentation:scale</code> and
|
|
3785
|
-
<code>presentation:changed</code
|
|
3785
|
+
<code>presentation:changed</code>: colon-separated like every other grid event rather than
|
|
3786
3786
|
the camelCase the original brief used, so a host subscribing to them does not have to
|
|
3787
3787
|
remember which family a name belongs to.</p>
|
|
3788
3788
|
</div>
|
|
@@ -3791,7 +3791,7 @@ grid.presentation.stop(); <span class="cmt">// or Escape</span
|
|
|
3791
3791
|
<pre><code>grid.presentation.start({ views: ['escalations', 'at-risk', 'margin-watch'] });
|
|
3792
3792
|
grid.presentation.step(1); <span class="cmt">// or an arrow key, space, Page Down</span>
|
|
3793
3793
|
grid.presentation.goTo(0); <span class="cmt">// or Home / End</span>
|
|
3794
|
-
grid.presentation.reset(); <span class="cmt">// or R
|
|
3794
|
+
grid.presentation.reset(); <span class="cmt">// or R: back to the view as saved</span></code></pre>
|
|
3795
3795
|
</div>
|
|
3796
3796
|
<div class="table-wrap">
|
|
3797
3797
|
<table>
|
|
@@ -3810,14 +3810,14 @@ grid.presentation.reset(); <span class="cmt">// or R — back to the vi
|
|
|
3810
3810
|
ordinary <code>views.apply</code>, as a single undo entry. Nothing about presenting changes
|
|
3811
3811
|
what a view means.</p>
|
|
3812
3812
|
<p><strong>The stepping keys only bind when there is a sequence</strong>, and never while
|
|
3813
|
-
something is being typed into. Without a deck those keys belong to the grid
|
|
3814
|
-
with no slides still expects Page Down to scroll
|
|
3813
|
+
something is being typed into. Without a deck those keys belong to the grid, a presenter
|
|
3814
|
+
with no slides still expects Page Down to scroll, and a quick filter answering a question
|
|
3815
3815
|
from the room must not advance the deck on the space bar.</p>
|
|
3816
3816
|
<p><strong>Stepping past either end sits there.</strong> It does not wrap: a presenter who
|
|
3817
3817
|
sees the first slide again thinks the deck has restarted.</p>
|
|
3818
3818
|
<p><strong>Transitions are a cross-fade, not continuous row motion.</strong> Rows are pooled
|
|
3819
3819
|
and virtualised, so an element holding a row before a view change may hold a different row
|
|
3820
|
-
after it
|
|
3820
|
+
after it: only rows visible in <em>both</em> states could be animated between positions, and
|
|
3821
3821
|
half a movement draws the eye to whichever rows happened to survive rather than to the change
|
|
3822
3822
|
itself. <code>prefers-reduced-motion</code> removes it; a projected fade is far larger than
|
|
3823
3823
|
one on a laptop, so someone who asked for less motion meant it.</p>
|
|
@@ -3842,7 +3842,7 @@ grid.presentation.start({ chrome: ['statusBar'] });</code></pre>
|
|
|
3842
3842
|
the other rows are gone. It is opacity alone, so a dimmed sparkline keeps its colours instead
|
|
3843
3843
|
of flattening to grey.</p>
|
|
3844
3844
|
<p><strong>A spotlight does not survive a view change.</strong> It belongs to the point being
|
|
3845
|
-
made, not to the deck
|
|
3845
|
+
made, not to the deck: carried forward, it leaves the audience looking at a lit row that no
|
|
3846
3846
|
longer means anything.</p>
|
|
3847
3847
|
<p><strong>Redaction travels in views and undo.</strong> It is part of grid state, so a saved
|
|
3848
3848
|
view carries its own masking and a view that redacts salary redacts it every time it is
|
|
@@ -3869,14 +3869,14 @@ grid.on('presentation:captured', (e) => {
|
|
|
3869
3869
|
<p><strong>Mounting the bar elsewhere needs the grid's class.</strong> Every rule that styles
|
|
3870
3870
|
the prompt bar is scoped under <code>.lattice</code>, and every colour token is declared
|
|
3871
3871
|
there, so a bar mounted into your own chrome through <code>ai.element</code> arrives
|
|
3872
|
-
unstyled. Add <code>class="lattice"</code> to the container
|
|
3873
|
-
<code>data-theme</code> the grid carries, if you have set one
|
|
3872
|
+
unstyled. Add <code>class="lattice"</code> to the container, and the same
|
|
3873
|
+
<code>data-theme</code> the grid carries, if you have set one, and it picks up the theme.</p>
|
|
3874
3874
|
</div>
|
|
3875
3875
|
|
|
3876
3876
|
<div class="why">
|
|
3877
3877
|
<p><strong>It photographs the browser's own rendering.</strong> The grid is cloned, every
|
|
3878
3878
|
computed style is inlined onto the clone, and the result is wrapped in an SVG
|
|
3879
|
-
<code>foreignObject</code> and drawn to a canvas
|
|
3879
|
+
<code>foreignObject</code> and drawn to a canvas, so the picture is what the browser drew,
|
|
3880
3880
|
not a second renderer's guess at it. That matters here more than usual: every decoration,
|
|
3881
3881
|
sparkline and pill the cell layer produces comes out right without being reimplemented.</p>
|
|
3882
3882
|
<p><strong>Virtualisation makes it cheap.</strong> Only the rows on screen exist in the DOM,
|
|
@@ -3884,7 +3884,7 @@ grid.on('presentation:captured', (e) => {
|
|
|
3884
3884
|
A full-screen capture at <code>scale: 2</code> takes around a second.</p>
|
|
3885
3885
|
<p><strong>Cross-origin images are refused before the work starts.</strong> They taint the
|
|
3886
3886
|
canvas, and a tainted canvas fails at the very last step with a <code>SecurityError</code>
|
|
3887
|
-
that names nothing
|
|
3887
|
+
that names nothing, so the check runs first and the error names the offending URL. Serve the
|
|
3888
3888
|
image same-origin, inline it as a <code>data:</code> URL, or hide the column.</p>
|
|
3889
3889
|
<p><strong>Two further limits</strong>, both inherent to the technique: web fonts need
|
|
3890
3890
|
embedding to appear (Lattice's default <code>system-ui</code> stack is unaffected), and CSS
|
|
@@ -3909,7 +3909,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
3909
3909
|
content coordinates and redrawn with the scroll offset subtracted, so a circle drawn round a
|
|
3910
3910
|
cell travels with that cell rather than hanging over whatever scrolled underneath it.</p>
|
|
3911
3911
|
<p><strong>They are transient.</strong> Marks annotate a moment, so they are cleared when the
|
|
3912
|
-
presentation ends. A capture taken while they are on screen includes them
|
|
3912
|
+
presentation ends. A capture taken while they are on screen includes them, the canvas bitmap
|
|
3913
3913
|
is carried into the still deliberately, because <code>cloneNode</code> copies a canvas element
|
|
3914
3914
|
and not one pixel of what was drawn on it.</p>
|
|
3915
3915
|
<p><strong>No tool shortcuts are bound.</strong> The keys a presenter would want are already
|
|
@@ -3920,7 +3920,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
3920
3920
|
<h2 id="accessibility-guide">Accessibility</h2>
|
|
3921
3921
|
<p class="lead-in">
|
|
3922
3922
|
The grid is built to WCAG 2.2 level AA. What follows is what it does, what it does not do yet,
|
|
3923
|
-
and the keyboard map in full
|
|
3923
|
+
and the keyboard map in full: stated plainly, because a conformance claim that overstates
|
|
3924
3924
|
is worth less than one that admits its edges.
|
|
3925
3925
|
</p>
|
|
3926
3926
|
|
|
@@ -3986,37 +3986,37 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
3986
3986
|
<h3>Colour and contrast</h3>
|
|
3987
3987
|
<p>No information is carried by hue alone. The <code>high-contrast</code> theme runs text at 21:1
|
|
3988
3988
|
and borders at 6.1:1. In Windows High Contrast Mode the grid translates its state into borders
|
|
3989
|
-
and system colours rather than fighting the palette
|
|
3989
|
+
and system colours rather than fighting the palette: see
|
|
3990
3990
|
<a href="#theming">theming</a> for what that means in detail.</p>
|
|
3991
3991
|
|
|
3992
3992
|
<h3>How this is checked</h3>
|
|
3993
3993
|
<p>The grid carries its own accessibility rules, and they run on every build against each
|
|
3994
|
-
configuration that differs structurally
|
|
3995
|
-
and with a tool panel
|
|
3994
|
+
configuration that differs structurally: flat, grouped, tree, pinned, editing, paginated
|
|
3995
|
+
and with a tool panel: rather than against one sample grid. The same rules are available
|
|
3996
3996
|
live from the devtools panel, where they can also read colour and measure targets.</p>
|
|
3997
3997
|
<p>Be clear about what that proves. These are our own rules covering what a data grid gets wrong,
|
|
3998
3998
|
not a general-purpose engine, and automated checking of any kind catches a minority of real
|
|
3999
|
-
problems. They are a regression net
|
|
3999
|
+
problems. They are a regression net (they stop a fix being undone silently) and
|
|
4000
4000
|
not evidence of conformance.</p>
|
|
4001
4001
|
|
|
4002
4002
|
<h3>Bigger targets for touch</h3>
|
|
4003
4003
|
<p class="lead-in">
|
|
4004
4004
|
The grid meets the minimum target size on its own. That minimum is a conformance floor, not a
|
|
4005
|
-
comfortable size for a finger
|
|
4005
|
+
comfortable size for a finger: both mobile platforms recommend nearer 44 pixels.
|
|
4006
4006
|
</p>
|
|
4007
4007
|
<div class="example">
|
|
4008
4008
|
<pre><code>targetSize: 'large'</code></pre>
|
|
4009
4009
|
</div>
|
|
4010
4010
|
<p>This raises the hit areas and leaves the type where it is, which is the distinction that
|
|
4011
4011
|
matters: a touch user wants a larger target, and a low-vision user wants larger text. Density
|
|
4012
|
-
is the control for the second, and the two combine
|
|
4012
|
+
is the control for the second, and the two combine, a compact grid with large targets is
|
|
4013
4013
|
a reasonable thing to want on a tablet.</p>
|
|
4014
4014
|
<p>It applies by itself under a coarse pointer, since the person holding one is both who the
|
|
4015
4015
|
criterion is for and the least likely to go looking for a setting. Pass
|
|
4016
4016
|
<code>targetSize: 'default'</code> to opt out of that.</p>
|
|
4017
4017
|
<div class="why">
|
|
4018
4018
|
<p>Density alone does not do this. It scales the header, the rows and the type, and leaves the
|
|
4019
|
-
affordances inside them exactly as they were
|
|
4019
|
+
affordances inside them exactly as they were: measured at every preset, the menu button
|
|
4020
4020
|
stays 24 pixels, the filter 16 and the resize grip 10. A spacious grid has the room going spare
|
|
4021
4021
|
and controls no larger than a compact one, which is the gap this fills.</p>
|
|
4022
4022
|
</div>
|
|
@@ -4026,7 +4026,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
4026
4026
|
<ul>
|
|
4027
4027
|
<li><strong>Two-dimensional scrolling.</strong> A grid scrolls horizontally at narrow widths.
|
|
4028
4028
|
WCAG 1.4.10 Reflow explicitly permits this for data tables, so it is conforming rather than a
|
|
4029
|
-
gap
|
|
4029
|
+
gap, but it is worth knowing before you meet it.</li>
|
|
4030
4030
|
<li><strong>Pinned columns do not release at narrow widths.</strong> At around 320 pixels, two
|
|
4031
4031
|
pinned columns of ordinary width can leave under 60 pixels for the scrolling middle. The grid
|
|
4032
4032
|
stays operable and nothing is lost, but a layout that pins columns is worth reviewing if you
|
|
@@ -4034,7 +4034,7 @@ grid.annotate.use(null); <span class="cmt">// hand the grid
|
|
|
4034
4034
|
<li><strong>The filter icon is a small target, deliberately.</strong> It is 16 × 16,
|
|
4035
4035
|
below the 24-pixel minimum of WCAG 2.5.8, and conforms under that criterion's
|
|
4036
4036
|
<em>equivalent</em> allowance: filtering is also a column-menu item, and the menu button
|
|
4037
|
-
meets the size on its own. Worth knowing if you are pointing at it on a touch screen
|
|
4037
|
+
meets the size on its own. Worth knowing if you are pointing at it on a touch screen ,
|
|
4038
4038
|
the menu is the larger route to the same thing.</li>
|
|
4039
4039
|
</ul>
|
|
4040
4040
|
|
|
@@ -4067,9 +4067,9 @@ grid.set('density', 'compact'); <span class="cmt">// live</span></code></pre
|
|
|
4067
4067
|
<p><strong>Virtualisation follows the token, not a separate number.</strong> Row positions
|
|
4068
4068
|
are computed from the resolved <code>--lattice-row-height</code>, so a host that overrides
|
|
4069
4069
|
that token by hand gets the virtualisation to agree. An explicit <code>rowHeight</code> in
|
|
4070
|
-
config outranks both
|
|
4070
|
+
config outranks both, a host that names a number means it.</p>
|
|
4071
4071
|
<p><strong>Height alone will not reproduce a modern app listing.</strong> Those designs pair
|
|
4072
|
-
generous rows with two-line cells
|
|
4072
|
+
generous rows with two-line cells (a bold title over a grey sub-label) and an avatar or
|
|
4073
4073
|
thumbnail. <code>spacious</code> gives the room, an <a href="#image-guide">image column</a>
|
|
4074
4074
|
gives the picture, and <a href="#twoline-guide">twoline</a> gives the second line.</p>
|
|
4075
4075
|
</div>
|
|
@@ -4102,7 +4102,7 @@ grid.set('density', 'compact'); <span class="cmt">// live</span></code></pre
|
|
|
4102
4102
|
including a <code>data:</code> URL claiming to be anything other than an image. Relative URLs
|
|
4103
4103
|
pass, since they cannot name a scheme. A grid drawing URLs that arrived in a data feed is
|
|
4104
4104
|
exactly where a bad one gets through. Note this differs from the <code>link</code> renderer,
|
|
4105
|
-
which refuses <code>data:</code> outright
|
|
4105
|
+
which refuses <code>data:</code> outright: correct for an anchor, wrong for a picture.</p>
|
|
4106
4106
|
<p><strong>A missing or broken image never moves the column.</strong> The box is sized from
|
|
4107
4107
|
the row height whether or not the picture loads, and a failed load leaves it empty rather
|
|
4108
4108
|
than showing the browser's broken-image glyph, which is a different size in every engine.</p>
|
|
@@ -4149,7 +4149,7 @@ props: { secondary: 'user', format: (v) => v ? `User: ${v}` : '' }</code></pr
|
|
|
4149
4149
|
<h2 id="redaction-guide">Redacting a column</h2>
|
|
4150
4150
|
<p class="lead-in">
|
|
4151
4151
|
For presenting and screen sharing: obscure the values in a column while everything that
|
|
4152
|
-
makes the grid readable
|
|
4152
|
+
makes the grid readable (the row count, the sort, the filters, the column layout) stays
|
|
4153
4153
|
exactly as it was. Right-click a column heading and choose <strong>Redact column</strong>.
|
|
4154
4154
|
</p>
|
|
4155
4155
|
<div class="example">
|
|
@@ -4162,12 +4162,12 @@ grid.redaction.clear(); <span class="cmt">// when the call ends</span><
|
|
|
4162
4162
|
<p><strong>This is not a security control, and the difference matters.</strong> The values
|
|
4163
4163
|
are still in the model, still in the DOM, still on the clipboard and still in every export.
|
|
4164
4164
|
Anyone looking at the page can read them from devtools, or by turning off a single CSS rule.
|
|
4165
|
-
What redaction defeats is a camera and a screen recorder
|
|
4165
|
+
What redaction defeats is a camera and a screen recorder, which is the threat a presenter
|
|
4166
4166
|
actually has, and the only one it claims to answer. For a value that must never reach the
|
|
4167
4167
|
browser, use <code>permissions</code> with <code>writeOnly</code>: there the value is not
|
|
4168
4168
|
sent, so there is nothing to reveal.</p>
|
|
4169
4169
|
<p><strong>Headings stay readable, totals do not.</strong> The column heading is left alone
|
|
4170
|
-
deliberately
|
|
4170
|
+
deliberately, a redacted column still has to be identifiable, by the presenter who wants to
|
|
4171
4171
|
turn it back on and by the audience who need to know what they are not being shown. The
|
|
4172
4172
|
pinned totals row <em>is</em> redacted, because the sum of a column is not a hint at its
|
|
4173
4173
|
values: filter to one row and the total is the value.</p>
|
|
@@ -4181,7 +4181,7 @@ grid.redaction.clear(); <span class="cmt">// when the call ends</span><
|
|
|
4181
4181
|
<h2 id="clipboard">Clipboard</h2>
|
|
4182
4182
|
<p class="lead-in">
|
|
4183
4183
|
Copy produces the tab-separated form Excel, Numbers and Sheets all read, so a range pastes as
|
|
4184
|
-
cells rather than as one lump of text. Values go through each column's clipboard hook
|
|
4184
|
+
cells rather than as one lump of text. Values go through each column's clipboard hook: a
|
|
4185
4185
|
lookup copies its label, a date copies an unambiguous form.
|
|
4186
4186
|
</p>
|
|
4187
4187
|
<div class="example">
|
|
@@ -4203,7 +4203,7 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
|
|
|
4203
4203
|
<p class="lead-in">
|
|
4204
4204
|
Press <kbd>?</kbd> in the grid to see this list in the product. The overlay is generated from
|
|
4205
4205
|
the same bindings the grid implements, so it cannot drift from them, and it shows
|
|
4206
|
-
<kbd>Cmd</kbd> rather than <kbd>Ctrl</kbd> on a Mac
|
|
4206
|
+
<kbd>Cmd</kbd> rather than <kbd>Ctrl</kbd> on a Mac, the grid reads either, so that is what
|
|
4207
4207
|
you will actually press. <kbd>Escape</kbd> closes it and focus returns where it was. Set
|
|
4208
4208
|
<code>shortcuts: false</code> if you want <kbd>?</kbd> for something else.
|
|
4209
4209
|
</p>
|
|
@@ -4225,7 +4225,7 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
|
|
|
4225
4225
|
</table>
|
|
4226
4226
|
</div>
|
|
4227
4227
|
<div class="why">
|
|
4228
|
-
<p>None of these fire while you are typing into an input
|
|
4228
|
+
<p>None of these fire while you are typing into an input, a filter box, an open editor, the
|
|
4229
4229
|
view-name field. The grid checks where the keystroke came from before claiming it, which
|
|
4230
4230
|
sounds obvious and is the sort of thing that is usually wrong.</p>
|
|
4231
4231
|
</div>
|
|
@@ -4251,17 +4251,17 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
|
|
|
4251
4251
|
who has built a filter has already learned this, and two vocabularies for one idea is how a
|
|
4252
4252
|
product ends up explaining itself twice.</p>
|
|
4253
4253
|
<p><strong>First match wins, by default.</strong> "Red if overdue, amber if due this week"
|
|
4254
|
-
reads top to bottom and stops
|
|
4254
|
+
reads top to bottom and stops, the spreadsheet convention, and the one people expect.
|
|
4255
4255
|
<code>stopIfTrue: false</code> lets rules combine: weight from one, colour from another.</p>
|
|
4256
4256
|
<p><strong>A blank cell satisfies no comparison.</strong> <code>Number(null)</code> is zero, so
|
|
4257
4257
|
a naive implementation sweeps every empty cell into "less than 100" and formats half a column
|
|
4258
4258
|
that has no data in it.</p>
|
|
4259
|
-
<p><strong>Text still compares numerically.</strong> A rules panel produces strings
|
|
4260
|
-
the user typed into yields <code>"100"</code>, not <code>100</code
|
|
4259
|
+
<p><strong>Text still compares numerically.</strong> A rules panel produces strings, the box
|
|
4260
|
+
the user typed into yields <code>"100"</code>, not <code>100</code>, and comparing those as
|
|
4261
4261
|
text puts <code>"9"</code> above <code>"100"</code>.</p>
|
|
4262
4262
|
<p><strong>A scale's bounds are given, not derived.</strong> Deriving them means scanning the
|
|
4263
4263
|
column per cell, and a scale that rescaled as rows were filtered would change a cell's colour
|
|
4264
|
-
without its value changing
|
|
4264
|
+
without its value changing, the opposite of what the colour is for.</p>
|
|
4265
4265
|
</div>
|
|
4266
4266
|
|
|
4267
4267
|
<h2 id="formatting-guide">Formatting an end user can change</h2>
|
|
@@ -4283,8 +4283,8 @@ grid.formatting.clear('margin');</code></pre>
|
|
|
4283
4283
|
</div>
|
|
4284
4284
|
<div class="why">
|
|
4285
4285
|
<p><strong>Two scopes, one ordered list.</strong> A rule sits on a column id or on
|
|
4286
|
-
<code>'*'</code> for every column. Evaluation joins them
|
|
4287
|
-
own
|
|
4286
|
+
<code>'*'</code> for every column. Evaluation joins them: grid-wide first, then the column's
|
|
4287
|
+
own, so a column rule can override a grid-wide one, and <code>stopIfTrue</code> means the
|
|
4288
4288
|
same thing across the join as it does within either half.</p>
|
|
4289
4289
|
<p><strong>Saved views and undo came free.</strong> The rules are a section of
|
|
4290
4290
|
<code>GridState</code>, and both saved views and the undo timeline are built on that. Nothing
|
|
@@ -4303,12 +4303,12 @@ grid.formatting.clear('margin');</code></pre>
|
|
|
4303
4303
|
<pre><code>createGrid(el, { toolPanel: { side: 'right', panels: ['columns', 'filters', 'formatting'] } });</code></pre>
|
|
4304
4304
|
</div>
|
|
4305
4305
|
<div class="why">
|
|
4306
|
-
<p>The panel is a form over that array and nothing more
|
|
4306
|
+
<p>The panel is a form over that array and nothing more: every control is one call into
|
|
4307
4307
|
<code>grid.formatting</code>, which is what makes each gesture undoable without the panel
|
|
4308
4308
|
knowing undo exists. It exposes ordering because ordering is meaning: dragging a rule up can
|
|
4309
4309
|
change which of two colours a cell takes.</p>
|
|
4310
4310
|
<p><strong>Not yet in the panel:</strong> icon sets and data bars. Both are column decorations
|
|
4311
|
-
rather than cell styles
|
|
4311
|
+
rather than cell styles, the <code>bar</code> and <code>icon</code> decorations already
|
|
4312
4312
|
render them, and driving those from the panel needs a runtime column-decoration API that
|
|
4313
4313
|
persists and undoes alongside the rules. Building it as a second bar implementation inside
|
|
4314
4314
|
the rule engine was the alternative, and the wrong one.</p>
|
|
@@ -4324,7 +4324,7 @@ grid.formatting.clear('margin');</code></pre>
|
|
|
4324
4324
|
grid.filters.quick('crc', { mode: 'fuzzy' }); <span class="cmt">// characters in order</span>
|
|
4325
4325
|
grid.filters.quick('^CIR-[12]', { mode: 'regex' });
|
|
4326
4326
|
|
|
4327
|
-
grid.filters.quick('acme'); <span class="cmt">// mode persists
|
|
4327
|
+
grid.filters.quick('acme'); <span class="cmt">// mode persists: still 'regex' here</span>
|
|
4328
4328
|
grid.filters.quickState(); <span class="cmt">// { text, mode }</span></code></pre>
|
|
4329
4329
|
</div>
|
|
4330
4330
|
<div class="why">
|
|
@@ -4365,7 +4365,7 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
|
|
|
4365
4365
|
</div>
|
|
4366
4366
|
<div class="why">
|
|
4367
4367
|
<p><strong>A cell is a few hundred pixels seen for a second</strong>, so there are no axes, no
|
|
4368
|
-
gridlines and no legend. The chart carries one idea
|
|
4368
|
+
gridlines and no legend. The chart carries one idea (a shape, a share, a comparison) and the
|
|
4369
4369
|
number beside it carries the precision. Hide the number with <code>label: false</code> when the
|
|
4370
4370
|
column next to it already says the same thing.</p>
|
|
4371
4371
|
<p><strong>Pin <code>min</code> and <code>max</code> when columns are meant to compare.</strong>
|
|
@@ -4373,12 +4373,12 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
|
|
|
4373
4373
|
by an order of magnitude draw identically. Pinning the scale is what makes the column readable
|
|
4374
4374
|
down its length rather than only across it.</p>
|
|
4375
4375
|
<p><strong>A gap is not a zero.</strong> Entries that are not numbers break the line and omit
|
|
4376
|
-
the bar, rather than being drawn at the baseline
|
|
4376
|
+
the bar, rather than being drawn at the baseline: joining across a missing reading would draw
|
|
4377
4377
|
a trend nobody measured, and drawing it at zero invents a dip.</p>
|
|
4378
4378
|
<p><strong>Cost.</strong> Each chart is one SVG built once, with the paths' <code>d</code>
|
|
4379
4379
|
attributes the only thing a repaint writes, and a bar chart is two paths rather than one
|
|
4380
4380
|
element per bar. Rows recycle as you scroll, so this is what keeps a chart column the same
|
|
4381
|
-
price as a text one. Nothing measures the DOM
|
|
4381
|
+
price as a text one. Nothing measures the DOM, the drawing happens in a fixed coordinate
|
|
4382
4382
|
space that CSS scales.</p>
|
|
4383
4383
|
<p><strong>Accessibility.</strong> The chart is <code>aria-hidden</code> and the cell carries a
|
|
4384
4384
|
summary: "12 points, 9 to 20, ending 18". A path cannot be read aloud, and a series announced
|
|
@@ -4409,14 +4409,14 @@ grid.filters.quickState(); <span class="cmt">// { text, mode }</span></co
|
|
|
4409
4409
|
<em>user</em> typed into a cell. Handing it to the JavaScript engine would let anyone who can
|
|
4410
4410
|
edit a cell read your cookies, call your API with your credentials, or post the grid's contents
|
|
4411
4411
|
anywhere. It is a hand-written tokeniser and recursive-descent parser, and the only callable
|
|
4412
|
-
things are the built-in functions
|
|
4412
|
+
things are the built-in functions: <code>constructor</code>, <code>globalThis</code> and
|
|
4413
4413
|
<code>constructor.constructor("return 1")()</code> all simply fail to resolve.</p>
|
|
4414
4414
|
<p><strong>The result is stored, not the expression.</strong> A formula is a way of
|
|
4415
4415
|
<em>entering</em> a value, exactly like <code>1,200</code> or <code>(50)</code> or
|
|
4416
4416
|
<code>12%</code>. It commits as one undo step, fires one <code>cell:changed</code>, and passes
|
|
4417
4417
|
through the column's own validation.</p>
|
|
4418
4418
|
<p><strong>The result is stored, not the formula.</strong> The expression is evaluated once, at
|
|
4419
|
-
the moment you commit it, and what lands in the cell is a value like any other
|
|
4419
|
+
the moment you commit it, and what lands in the cell is a value like any other, so it does not
|
|
4420
4420
|
recalculate when a cell it referred to changes later. For a value that must stay in step with
|
|
4421
4421
|
its inputs, use a computed column, which is re-evaluated whenever its dependencies move.</p>
|
|
4422
4422
|
</div>
|
|
@@ -4436,14 +4436,14 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
4436
4436
|
</div>
|
|
4437
4437
|
<p class="lead-in">
|
|
4438
4438
|
A formula that cannot be read rejects the edit and the cell keeps its old value, the same as any
|
|
4439
|
-
other unparseable text. Failures are returned rather than thrown
|
|
4439
|
+
other unparseable text. Failures are returned rather than thrown: this runs on the commit path,
|
|
4440
4440
|
where an exception would abandon the commit half-done.
|
|
4441
4441
|
</p>
|
|
4442
4442
|
<div class="why">
|
|
4443
4443
|
<p><strong>Bare arithmetic is deliberately not a formula.</strong> <code>2-1</code> is a
|
|
4444
4444
|
plausible product code and <code>1/2</code> a plausible date. A reader that evaluated either on
|
|
4445
4445
|
a guess would have to guess wrong sometimes, and the wrong answer is not a visible error but a
|
|
4446
|
-
plausible number
|
|
4446
|
+
plausible number: <code>2*3</code> stored as <strong>23</strong> looks like data. Arithmetic
|
|
4447
4447
|
without a leading <code>=</code> is refused outright, so the cell keeps what it had rather than
|
|
4448
4448
|
taking a number nobody typed.</p>
|
|
4449
4449
|
</div>
|
|
@@ -4474,7 +4474,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
4474
4474
|
<div class="why">
|
|
4475
4475
|
<p><strong>Handed the defaults, rather than replacing them.</strong> A builder that had to
|
|
4476
4476
|
return every item in order to append one would be written once as a copy of the built-ins and
|
|
4477
|
-
would then drift from them
|
|
4477
|
+
would then drift from them, the copy keeps the menu it was forked from, and stops gaining
|
|
4478
4478
|
whatever the grid adds later. Spreading <code>defaults</code> costs one line and never goes
|
|
4479
4479
|
stale.</p>
|
|
4480
4480
|
<p>Return the array you want shown: add, remove, reorder, or replace outright. An empty array
|
|
@@ -4485,7 +4485,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
4485
4485
|
<p class="lead-in">
|
|
4486
4486
|
<code>columnMenu</code> takes the same function form, for both routes into a column's menu:
|
|
4487
4487
|
the header's 3-dot button and a right-click on the heading. Its <code>params</code> is
|
|
4488
|
-
<code>{ colId, column, grid }</code>, and the same rules apply
|
|
4488
|
+
<code>{ colId, column, grid }</code>, and the same rules apply: spread the defaults, return
|
|
4489
4489
|
an empty array to suppress, return nothing to leave them alone.
|
|
4490
4490
|
</p>
|
|
4491
4491
|
<div class="example">
|
|
@@ -4528,7 +4528,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
4528
4528
|
</div>
|
|
4529
4529
|
<p class="lead-in">
|
|
4530
4530
|
<code>title</code> and <code>icon</code> may each be a function, re-read on every repaint, for
|
|
4531
|
-
a control whose meaning changes
|
|
4531
|
+
a control whose meaning changes: that is how maximise becomes restore. <code>enabled</code> is
|
|
4532
4532
|
a predicate rather than a flag, so a button that cannot do anything greys itself out instead of
|
|
4533
4533
|
doing nothing when clicked.
|
|
4534
4534
|
</p>
|
|
@@ -4554,7 +4554,7 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
|
|
|
4554
4554
|
</div>
|
|
4555
4555
|
<div class="why">
|
|
4556
4556
|
<p><strong>An explicit <code>actions</code> array replaces the default rather than extending
|
|
4557
|
-
it.</strong> That is what makes it useful
|
|
4557
|
+
it.</strong> That is what makes it useful (you choose the set and the order) but it also
|
|
4558
4558
|
means a config written against an earlier version keeps exactly the buttons it named and
|
|
4559
4559
|
silently misses anything added since. If you want whatever the current version offers, leave
|
|
4560
4560
|
the key out.</p>
|
|
@@ -4589,19 +4589,19 @@ createGrid(el, { maximise: <span class="kw">false</span> });</code></pre>
|
|
|
4589
4589
|
<div class="why">
|
|
4590
4590
|
<p><strong>The element is moved, not just restyled.</strong> A <code>position: fixed</code>
|
|
4591
4591
|
element is positioned against the nearest ancestor carrying a <code>transform</code>,
|
|
4592
|
-
<code>filter</code>, <code>contain</code> or <code>will-change</code
|
|
4593
|
-
card, any animated panel, any sticky app shell
|
|
4592
|
+
<code>filter</code>, <code>contain</code> or <code>will-change</code>, which is to say any
|
|
4593
|
+
card, any animated panel, any sticky app shell, and against the viewport only when there is
|
|
4594
4594
|
no such ancestor. A class alone would therefore fill the window on one page and land in a
|
|
4595
4595
|
300px box on the next, and would still be clipped by an <code>overflow: hidden</code> or
|
|
4596
4596
|
buried by a stacking context. Reparenting to <code><body></code> removes every ancestor
|
|
4597
4597
|
that could do any of that.</p>
|
|
4598
4598
|
<p>Coming back is a hidden placeholder left in the element's place, not a remembered parent
|
|
4599
|
-
and index
|
|
4599
|
+
and index, an index goes stale the moment your application inserts a sibling while the grid
|
|
4600
4600
|
is away, and then silently reinserts in the wrong slot. The placeholder also holds the vacated
|
|
4601
4601
|
space open at the size the grid had, so the page behind neither reflows nor loses its scroll
|
|
4602
4602
|
position while you are looking at the grid.</p>
|
|
4603
4603
|
<p>The geometry is written as inline styles and every displaced property is handed back
|
|
4604
|
-
exactly as it was found, because the element being restyled is <em>yours</em
|
|
4604
|
+
exactly as it was found, because the element being restyled is <em>yours</em>: most often one
|
|
4605
4605
|
with an inline <code>height</code> on it, which is the ordinary way a grid gets sized and
|
|
4606
4606
|
which nothing but an inline style can beat. While maximised, the element carries
|
|
4607
4607
|
<code>.lat-maximised</code> and <code><body></code> carries
|
|
@@ -4630,7 +4630,7 @@ grid.highlight.clear();</code></pre>
|
|
|
4630
4630
|
</p>
|
|
4631
4631
|
<h2 id="views-guide">Saved views</h2>
|
|
4632
4632
|
<p class="lead-in">
|
|
4633
|
-
A view is a named grid state
|
|
4633
|
+
A view is a named grid state: sort, filters, grouping, column order, widths, visibility.
|
|
4634
4634
|
There are two kinds and the picker keeps them apart.
|
|
4635
4635
|
</p>
|
|
4636
4636
|
<div class="example">
|
|
@@ -4651,14 +4651,14 @@ grid.highlight.clear();</code></pre>
|
|
|
4651
4651
|
</div>
|
|
4652
4652
|
<p class="lead-in">
|
|
4653
4653
|
Views in <code>saved</code> are <strong>defined views</strong>: part of the application,
|
|
4654
|
-
listed under their own heading, and neither renamable nor deletable
|
|
4654
|
+
listed under their own heading, and neither renamable nor deletable: refused by the model as
|
|
4655
4655
|
well as hidden in the interface. A view flagged <code>isDefault</code> is applied on load.
|
|
4656
4656
|
Everything the user saves sits below, with rename, share, make-default and delete.
|
|
4657
4657
|
</p>
|
|
4658
4658
|
<div class="why">
|
|
4659
4659
|
<p><strong>Applying a view is a destination, not a patch.</strong> A view's state names only
|
|
4660
4660
|
the sections it cares about, so applying one resets to the grid's starting state first. Without
|
|
4661
|
-
that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns
|
|
4661
|
+
that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns ,
|
|
4662
4662
|
the same view giving a different grid depending on what preceded it, which is the one thing a
|
|
4663
4663
|
named view must not do.</p>
|
|
4664
4664
|
</div>
|
|
@@ -4673,7 +4673,7 @@ grid.highlight.clear();</code></pre>
|
|
|
4673
4673
|
<table>
|
|
4674
4674
|
<thead><tr><th>What changed</th><th>What a saved view does</th></tr></thead>
|
|
4675
4675
|
<tbody>
|
|
4676
|
-
<tr><td class="name">A column was added</td><td class="desc"><strong>It appears</strong>, in the state its definition declares, placed after every column the view names. A view is not a whitelist
|
|
4676
|
+
<tr><td class="name">A column was added</td><td class="desc"><strong>It appears</strong>, in the state its definition declares, placed after every column the view names. A view is not a whitelist, it says nothing about columns it has never seen, and silence is not an instruction to hide.</td></tr>
|
|
4677
4677
|
<tr><td class="name">A column was added, and should not appear yet</td><td class="desc">Declare it <code>layout: { hidden: true }</code>. The view does not mention it, so nothing overrides that, and it stays hidden until the user shows it.</td></tr>
|
|
4678
4678
|
<tr><td class="name">A column was removed</td><td class="desc">The entries naming it are skipped and reported; the rest of the view applies. A sort or grouping on the missing column is dropped rather than left pointing at nothing.</td></tr>
|
|
4679
4679
|
<tr><td class="name">A column was renamed</td><td class="desc">That is a removal and an addition. The old id is skipped, the new column appears at the end, and any width or pinning the user had set is lost with the old id.</td></tr>
|
|
@@ -4682,8 +4682,8 @@ grid.highlight.clear();</code></pre>
|
|
|
4682
4682
|
</div>
|
|
4683
4683
|
|
|
4684
4684
|
<div class="why">
|
|
4685
|
-
<p><strong>Applying a view never throws and never refuses.</strong> It returns a report
|
|
4686
|
-
<code>{ applied, skipped }</code
|
|
4685
|
+
<p><strong>Applying a view never throws and never refuses.</strong> It returns a report ,
|
|
4686
|
+
<code>{ applied, skipped }</code>: naming each thing it could not use and why. Refusing the
|
|
4687
4687
|
whole view because one column has gone would lose a layout the user built deliberately, and
|
|
4688
4688
|
throwing during a page load would lose the page. So a view degrades to as much of itself as
|
|
4689
4689
|
still makes sense, and the host decides whether the user needs telling.</p>
|
|
@@ -4701,7 +4701,7 @@ grid.highlight.clear();</code></pre>
|
|
|
4701
4701
|
<p class="lead-in">
|
|
4702
4702
|
The consequence worth planning for is the first one: <strong>a column added in a new release
|
|
4703
4703
|
is visible to everyone, including users with a saved view.</strong> That is usually what you
|
|
4704
|
-
want
|
|
4704
|
+
want (a new field nobody can see is a field nobody uses) but if a release adds several at
|
|
4705
4705
|
once, every saved view gains them all at the right-hand end. Ship them hidden if that is not
|
|
4706
4706
|
the introduction you want.
|
|
4707
4707
|
</p>
|
|
@@ -4733,16 +4733,16 @@ grid.on('view:default', e => api.patch(`/views/${e.view.id}`, { isDefault: tr
|
|
|
4733
4733
|
|
|
4734
4734
|
<div class="why">
|
|
4735
4735
|
<p><strong>The other half of the same seam.</strong> <code>views.storage</code> above is where
|
|
4736
|
-
a developer plugs in their own backend
|
|
4736
|
+
a developer plugs in their own backend, a real server, reached over the network. Not every
|
|
4737
4737
|
grid has one to plug in, and a picker offering "Save" that quietly does nothing until a backend
|
|
4738
4738
|
exists is worse than not offering it. <code>views.local: true</code> is the no-backend answer:
|
|
4739
4739
|
saved views live in this browser's own <code>localStorage</code>, under a default key shared by
|
|
4740
|
-
every grid on the origin unless you pass one of your own
|
|
4741
|
-
<code>views: { local: { key: 'orders-grid-views' } }</code
|
|
4740
|
+
every grid on the origin unless you pass one of your own ,
|
|
4741
|
+
<code>views: { local: { key: 'orders-grid-views' } }</code>: to keep two grids' views apart.
|
|
4742
4742
|
Given alongside an explicit <code>storage</code>, the explicit adapter always wins and
|
|
4743
|
-
<code>local</code> is silently
|
|
4743
|
+
<code>local</code> is silently (well, not silently: it warns once) ignored, so a page cannot
|
|
4744
4744
|
end up writing to both without meaning to. The adapter itself is exported as
|
|
4745
|
-
<code>createLocalViewStorage(opts)</code>, for anyone who wants it directly
|
|
4745
|
+
<code>createLocalViewStorage(opts)</code>, for anyone who wants it directly, a custom key
|
|
4746
4746
|
without the shorthand, or a different <code>Storage</code>-shaped backing such as
|
|
4747
4747
|
<code>sessionStorage</code> for views scoped to one tab rather than persisted across visits.</p>
|
|
4748
4748
|
</div>
|
|
@@ -4750,7 +4750,7 @@ grid.on('view:default', e => api.patch(`/views/${e.view.id}`, { isDefault: tr
|
|
|
4750
4750
|
<h2 id="history-guide">Undo</h2>
|
|
4751
4751
|
<p class="lead-in">
|
|
4752
4752
|
Undo covers the whole grid, not only edits. Sorts, filters, column moves, grouping, an applied
|
|
4753
|
-
view and a restore all record an entry
|
|
4753
|
+
view and a restore all record an entry, and each carries a label written for a button.
|
|
4754
4754
|
</p>
|
|
4755
4755
|
<div class="example">
|
|
4756
4756
|
<p class="example__label">Naming the action</p>
|
|
@@ -4759,7 +4759,7 @@ grid.history.undo();
|
|
|
4759
4759
|
grid.history.list(); <span class="cmt">// the timeline, newest first</span></code></pre>
|
|
4760
4760
|
</div>
|
|
4761
4761
|
<div class="why">
|
|
4762
|
-
<p>A button that says only "Undo" makes the user press it to find out what it does
|
|
4762
|
+
<p>A button that says only "Undo" makes the user press it to find out what it does: and
|
|
4763
4763
|
pressing it is the thing they were unsure about. "Undo sort by Region" is decided before the
|
|
4764
4764
|
click rather than after.</p>
|
|
4765
4765
|
</div>
|
|
@@ -4779,16 +4779,16 @@ grid.history.list(); <span class="cmt">// the timeline, newest first</sp
|
|
|
4779
4779
|
|
|
4780
4780
|
<h2 id="permissions-guide">Column permissions</h2>
|
|
4781
4781
|
<p class="lead-in">
|
|
4782
|
-
Four levels, resolved per column from configuration or a callback. They are not a ladder
|
|
4782
|
+
Four levels, resolved per column from configuration or a callback. They are not a ladder ,
|
|
4783
4783
|
reading and writing are independent, so they are the four corners of a 2×2.
|
|
4784
4784
|
</p>
|
|
4785
4785
|
<div class="table-wrap">
|
|
4786
4786
|
<table>
|
|
4787
4787
|
<thead><tr><th>Level</th><th>Visible</th><th>Readable</th><th>Editable</th><th>For</th></tr></thead>
|
|
4788
4788
|
<tbody>
|
|
4789
|
-
<tr><td class="sig">hidden</td><td
|
|
4790
|
-
<tr><td class="sig">read</td><td>yes</td><td>yes</td><td
|
|
4791
|
-
<tr><td class="sig">writeOnly</td><td>yes</td><td
|
|
4789
|
+
<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 and the filter model.</td></tr>
|
|
4790
|
+
<tr><td class="sig">read</td><td>yes</td><td>yes</td><td>, </td><td class="desc">No editor opens; paste, fill and clear skip it.</td></tr>
|
|
4791
|
+
<tr><td class="sig">writeOnly</td><td>yes</td><td>, </td><td>yes</td><td class="desc">A secret. The cell shows a mask, the editor opens empty.</td></tr>
|
|
4792
4792
|
<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>
|
|
4793
4793
|
</tbody>
|
|
4794
4794
|
</table>
|
|
@@ -4811,12 +4811,12 @@ grid.permissions.setContext({ role: 'clerk' }); <span class="cmt">// re-r
|
|
|
4811
4811
|
<div class="note note--warn">
|
|
4812
4812
|
<p><strong>For three of the four this is a usability control, not a security boundary.</strong>
|
|
4813
4813
|
Anything the grid can render it has already loaded, and devtools reaches it. Hiding a column
|
|
4814
|
-
removes it from the interface, not from the process
|
|
4814
|
+
removes it from the interface, not from the process, which is worth a great deal for the way
|
|
4815
4815
|
data actually leaks, which is an export mailed onward or a shared view carrying a column a
|
|
4816
4816
|
colleague should not see. It is worth nothing against someone determined.</p>
|
|
4817
4817
|
<p><code>writeOnly</code> is the exception, and the reason it exists. Nothing in the grid needs
|
|
4818
4818
|
the value, so your server can send <code>null</code> for that field and the column still
|
|
4819
|
-
works
|
|
4819
|
+
works: at which point the secret is genuinely not on the page. Enforce everything else
|
|
4820
4820
|
server-side; <code>permittedColumns</code> and <code>permittedExport</code> are pure and
|
|
4821
4821
|
dependency-free so the same policy object runs in Node against a request that arrived over
|
|
4822
4822
|
the wire.</p>
|
|
@@ -4850,17 +4850,17 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
|
|
|
4850
4850
|
</div>
|
|
4851
4851
|
<div class="why">
|
|
4852
4852
|
<p><strong>The row is gone from the data and still in the snapshot.</strong> That is the only
|
|
4853
|
-
place it exists, and it is what these rows are built from
|
|
4853
|
+
place it exists, and it is what these rows are built from, so they carry the values they had
|
|
4854
4854
|
when the snapshot was taken, not any current ones.</p>
|
|
4855
4855
|
<p><strong>One option answers both questions:</strong> whether to show it, and whether it
|
|
4856
4856
|
counts. <code>'pinned'</code> puts it beneath the rows, struck through and dimmed, outside the
|
|
4857
|
-
row set
|
|
4857
|
+
row set, so <code>rows.count()</code>, exports and selection all pass over it.
|
|
4858
4858
|
<code>'data'</code> appends it to the set instead, and all three include it. Omitted, nothing
|
|
4859
4859
|
is shown and the grid is the one you already had.</p>
|
|
4860
4860
|
<p><strong>Neither sorts or filters it among the live rows.</strong> A removed row's values
|
|
4861
4861
|
are yesterday's; ordering them among today's presents two data sets as one, and lets a filter
|
|
4862
4862
|
written for current values decide the fate of historical ones. Neither permits an edit either
|
|
4863
|
-
|
|
4863
|
+
, a write aimed at a removed row is refused and returns <code>0</code>, rather than being
|
|
4864
4864
|
counted as applied against a record that is not there.</p>
|
|
4865
4865
|
</div>
|
|
4866
4866
|
|
|
@@ -4884,15 +4884,15 @@ grid.diff.before('CIR-100042', 'capacity');</code></pre>
|
|
|
4884
4884
|
<p class="lead-in">
|
|
4885
4885
|
That mounts a prompt bar. The user types "EMEA circuits over 500 gigs, biggest first"; the
|
|
4886
4886
|
grid composes a message including its schema; your callback returns the model's reply; the
|
|
4887
|
-
grid validates it and shows what it would do in plain English
|
|
4888
|
-
<em>"Filter Region is EMEA and Capacity is more than 500, sort Capacity descending"</em>
|
|
4887
|
+
grid validates it and shows what it would do in plain English ,
|
|
4888
|
+
<em>"Filter Region is EMEA and Capacity is more than 500, sort Capacity descending"</em> ,
|
|
4889
4889
|
with Apply and Discard.
|
|
4890
4890
|
</p>
|
|
4891
4891
|
<div class="why">
|
|
4892
|
-
<p><strong>Nothing is executed on trust.</strong> The vocabulary is seven actions
|
|
4892
|
+
<p><strong>Nothing is executed on trust.</strong> The vocabulary is seven actions ,
|
|
4893
4893
|
<code>setFilters</code>, <code>setSort</code>, <code>groupBy</code>,
|
|
4894
4894
|
<code>showColumns</code>, <code>hideColumns</code>, <code>setQuick</code>,
|
|
4895
|
-
<code>clear</code
|
|
4895
|
+
<code>clear</code>, and a reply naming a column that does not exist is rejected with a
|
|
4896
4896
|
reason while the valid actions in the same reply are kept. A model cannot be talked into an
|
|
4897
4897
|
operation the vocabulary does not contain, because there is nothing else to call.</p>
|
|
4898
4898
|
</div>
|
|
@@ -4910,7 +4910,7 @@ grid.export.clipboard({ headers: true, rows: 'range' });
|
|
|
4910
4910
|
grid.export.print();</code></pre>
|
|
4911
4911
|
</div>
|
|
4912
4912
|
<p class="lead-in">
|
|
4913
|
-
Exports follow what the user is looking at
|
|
4913
|
+
Exports follow what the user is looking at, the current filter, sort and column order,
|
|
4914
4914
|
formatted values, and only the columns they may read.
|
|
4915
4915
|
</p>
|
|
4916
4916
|
|
|
@@ -4938,7 +4938,7 @@ grid.state.apply(savedView.state);
|
|
|
4938
4938
|
<h2 id="webcomponent-guide">Web component</h2>
|
|
4939
4939
|
<p class="lead-in">
|
|
4940
4940
|
<code><lattice-grid></code> is the grid as a custom element, shipped as a self-contained
|
|
4941
|
-
module bundle. It exists for pages without a bundler
|
|
4941
|
+
module bundle. It exists for pages without a bundler, a Rails, Django or Laravel template
|
|
4942
4942
|
that wants a grid without adopting a front-end build.
|
|
4943
4943
|
</p>
|
|
4944
4944
|
<div class="example">
|
|
@@ -4951,7 +4951,7 @@ grid.state.apply(savedView.state);
|
|
|
4951
4951
|
rows='[{"id":"A","city":"Leeds"},{"id":"B","city":"Cardiff"}]'></lattice-grid></code></pre>
|
|
4952
4952
|
</div>
|
|
4953
4953
|
<div class="why">
|
|
4954
|
-
<p><strong>Load this or the main bundle, not both.</strong> The module is self-contained
|
|
4954
|
+
<p><strong>Load this or the main bundle, not both.</strong> The module is self-contained: it
|
|
4955
4955
|
carries the grid with it, so a page that also loads <code>lattice-grid.esm.js</code>
|
|
4956
4956
|
downloads and evaluates the grid twice.</p>
|
|
4957
4957
|
</div>
|
|
@@ -4971,8 +4971,8 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
4971
4971
|
<div class="why">
|
|
4972
4972
|
<p><strong>Assign before appending where you can.</strong> The element builds its grid once,
|
|
4973
4973
|
at the end of the task in which it connects, so everything set in that task arrives as one
|
|
4974
|
-
configuration rather than as a series of updates. Setting properties later still works
|
|
4975
|
-
go through the live configuration path
|
|
4974
|
+
configuration rather than as a series of updates. Setting properties later still works: they
|
|
4975
|
+
go through the live configuration path, but the grid is built empty first and repainted
|
|
4976
4976
|
after, which is a visible flash on a large set.</p>
|
|
4977
4977
|
<p>Reading <code>.grid</code> forces the build immediately, so the property is never briefly
|
|
4978
4978
|
null.</p>
|
|
@@ -4995,7 +4995,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
4995
4995
|
<p><strong>Light DOM, deliberately.</strong> The element renders into itself rather than a
|
|
4996
4996
|
shadow root, because the grid's generated decoration rules are injected into
|
|
4997
4997
|
<code>document.head</code> and the theme stylesheet is a <code><link></code> the page
|
|
4998
|
-
owns
|
|
4998
|
+
owns: neither crosses a shadow boundary. A shadowed grid would be structurally correct and
|
|
4999
4999
|
completely unstyled, and subtly so: the <code>--lattice-*</code> tokens <em>do</em> inherit
|
|
5000
5000
|
through a shadow root, so the colours would arrive while every pill, bar and heat cell stayed
|
|
5001
5001
|
bare. Light DOM keeps every documented theming route working unchanged.</p>
|
|
@@ -5013,7 +5013,7 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
5013
5013
|
<tr><td class="name">ready</td><td class="type">{}</td><td class="desc">First render is done. Fires on a future turn, so you can subscribe on the line after <code>createGrid</code>.</td></tr>
|
|
5014
5014
|
<tr><td class="name">cell:changed</td><td class="type">{ row, key, colId, value, oldValue, undo }</td><td class="desc">Persisting an edit. <code>undo</code> distinguishes a rollback from a fresh change.</td></tr>
|
|
5015
5015
|
<tr><td class="name">selection:changed</td><td class="type">{ keys }</td><td class="desc">Enabling a bulk action.</td></tr>
|
|
5016
|
-
<tr><td class="name">range:changed</td><td class="type">{ ranges }</td><td class="desc">A status bar showing the sum of what is selected
|
|
5016
|
+
<tr><td class="name">range:changed</td><td class="type">{ ranges }</td><td class="desc">A status bar showing the sum of what is selected: see <code>selection.summary()</code>.</td></tr>
|
|
5017
5017
|
<tr><td class="name">sort:changed / filter:changed</td><td class="type">{ sort } / { filters }</td><td class="desc">Reflecting the view in the URL.</td></tr>
|
|
5018
5018
|
<tr><td class="name">history:changed</td><td class="type">{ canUndo, canRedo, undo, redo }</td><td class="desc">Driving your own undo button. Emitted <em>after</em> the entry is pushed, so the label is right.</td></tr>
|
|
5019
5019
|
<tr><td class="name">history:applied</td><td class="type">{ direction, step }</td><td class="desc">An action was undone or redone, with which and what. <code>history:changed</code> also fires when a new action is pushed, so it cannot distinguish the two.</td></tr>
|
|
@@ -5032,8 +5032,8 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
5032
5032
|
watermark; that is the whole of what it does.
|
|
5033
5033
|
</p>
|
|
5034
5034
|
<p class="lead-in">
|
|
5035
|
-
Free to develop against, licensed to deploy. A grid on <strong>localhost</strong
|
|
5036
|
-
loopback host
|
|
5035
|
+
Free to develop against, licensed to deploy. A grid on <strong>localhost</strong>, or any
|
|
5036
|
+
loopback host: needs no key at all. On any other domain an unlicensed grid still renders
|
|
5037
5037
|
everything and carries a small trial watermark linking to latticegrid.dev.
|
|
5038
5038
|
</p>
|
|
5039
5039
|
<div class="table-wrap">
|
|
@@ -5052,24 +5052,24 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
5052
5052
|
grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'</span></code></pre>
|
|
5053
5053
|
</div>
|
|
5054
5054
|
<p class="lead-in">
|
|
5055
|
-
Call it once, before creating a grid. Setting a key later still works
|
|
5056
|
-
off and <code>licence:changed</code> fires
|
|
5055
|
+
Call it once, before creating a grid. Setting a key later still works, the watermark comes
|
|
5056
|
+
off and <code>licence:changed</code> fires, but the first frames of the grid will carry it.
|
|
5057
5057
|
</p>
|
|
5058
5058
|
<p class="lead-in">
|
|
5059
5059
|
Keys come from <a href="https://www.latticegrid.dev">latticegrid.dev</a> and are issued per
|
|
5060
5060
|
deployment rather than per developer or per seat: name the domains the grid will run on and
|
|
5061
5061
|
one key covers every developer, every build and every user on them. A key names the domains
|
|
5062
|
-
it covers as you would expect
|
|
5062
|
+
it covers as you would expect: <code>*.acme.com</code> matches <code>app.acme.com</code>,
|
|
5063
5063
|
<code>a.b.acme.com</code> and <code>acme.com</code> itself.
|
|
5064
5064
|
</p>
|
|
5065
5065
|
<p class="lead-in">
|
|
5066
5066
|
Checking a key needs no network. There is no licence server, no call home, and nothing that
|
|
5067
|
-
can fail at three in the morning
|
|
5067
|
+
can fail at three in the morning, a key carries its own answer and the grid reads it
|
|
5068
5068
|
locally, so a grid on an air-gapped network behaves exactly like one on the open internet.
|
|
5069
5069
|
</p>
|
|
5070
5070
|
<div class="why">
|
|
5071
5071
|
<p><strong>Nothing ever refuses to render, and nothing is ever withheld.</strong> An expired
|
|
5072
|
-
key, a wrong domain, a key that will not read
|
|
5072
|
+
key, a wrong domain, a key that will not read: all of them log one warning and show the
|
|
5073
5073
|
watermark. Every feature keeps working. The failure to avoid is a customer's production
|
|
5074
5074
|
screen going blank because a licence lapsed over a weekend, and a grid that quietly drops a
|
|
5075
5075
|
feature is the same failure wearing a disguise.</p>
|
|
@@ -5127,7 +5127,7 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'trial'<
|
|
|
5127
5127
|
<footer>
|
|
5128
5128
|
<p>
|
|
5129
5129
|
Lattice Grid 1.7.1 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
5130
|
-
Written against the shipped source. Where this guide and the code disagree, the code wins
|
|
5130
|
+
Written against the shipped source. Where this guide and the code disagree, the code wins ,
|
|
5131
5131
|
please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
5132
5132
|
</p>
|
|
5133
5133
|
</footer>
|