@toclocoinc/lattice-grid 1.14.0 → 1.16.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.
@@ -437,7 +437,7 @@
437
437
  <div class="shell">
438
438
  <aside class="rail">
439
439
  <p class="rail__brand">Lattice Grid</p>
440
- <p class="rail__sub">Developer guide · v1.14.0</p>
440
+ <p class="rail__sub">Developer guide · v1.16.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -884,6 +884,14 @@ createGrid(element, { direction: 'rtl' }); <span class="cmt">// or say so out
884
884
  <code>.events</code> namespaces: sitting on top of a real Lattice grid underneath.
885
885
  Swap the import and, for the surface below, the calling code does not change.
886
886
  </p>
887
+ <p class="lead-in">
888
+ The wrapper shares the core the page already loads rather than carrying its own,
889
+ so load <code>@toclocoinc/lattice-grid</code> alongside it: a bundler wires the shared
890
+ import automatically (it dedupes against the core your app already imports), and a
891
+ plain <code>&lt;script src&gt;</code> page loads the global build first. The pay-off is
892
+ that a licence set on that one core applies to these grids too, and the module is a
893
+ few kilobytes of translation rather than a second copy of the grid.
894
+ </p>
887
895
 
888
896
  <div class="example">
889
897
  <p class="example__label">A drop-in constructor</p>
@@ -1525,6 +1533,43 @@ cell: { decoration: 'dot' } <span class="cmt">// a leading
1525
1533
  property, not a search for hex codes.</p>
1526
1534
  </div>
1527
1535
 
1536
+ <div class="example">
1537
+ <p class="example__label">Icon sets: a threshold glyph per value band</p>
1538
+ <pre><code><span class="cmt">// A built-in set — traffic lights, arrows, rating marks — driven by value.</span>
1539
+ cell: { decoration: { type: 'icon', iconSet: 'arrows' } }
1540
+
1541
+ <span class="cmt">// Or your own bands. The highest `min` a value clears wins; a band with no</span>
1542
+ <span class="cmt">// `min` is the catch-all. `label` is what a screen reader announces.</span>
1543
+ cell: { decoration: { type: 'icon', bands: [
1544
+ { min: 0.9, icon: 'success', label: 'on target', variant: 'success' },
1545
+ { min: 0.5, icon: 'warning', label: 'at risk', variant: 'warning' },
1546
+ { icon: 'danger', label: 'off track', variant: 'danger' },
1547
+ ] } }</code></pre>
1548
+ </div>
1549
+ <div class="why">
1550
+ <p>An icon set is a restatement of the value, not a replacement for it: the value still
1551
+ renders beside the glyph, and the band's <code>label</code> is set as the glyph's
1552
+ <code>aria-label</code>, so a screen-reader user hears "on target 92%" rather than a bare
1553
+ number with the status lost. The glyphs are the grid's own inline sprites (§16), so an icon
1554
+ set adds no dependency and makes no request. Built-in sets: <code>trafficLights</code>,
1555
+ <code>arrows</code>, <code>trafficArrows</code>, <code>ratings</code>.</p>
1556
+ </div>
1557
+
1558
+ <div class="example">
1559
+ <p class="example__label">Turning a decoration on at runtime</p>
1560
+ <pre><code><span class="cmt">// Set, change or clear a column's decoration after the grid is built.</span>
1561
+ grid.columns.decorate('score', { type: 'bar', min: 0, max: 100 });
1562
+ grid.columns.decorate('trend', { type: 'icon', iconSet: 'arrows' });
1563
+ grid.columns.decorate('score', <span class="kw">null</span>); <span class="cmt">// back to plain text</span></code></pre>
1564
+ </div>
1565
+ <div class="why">
1566
+ <p>A decoration is presentation, so <code>columns.decorate</code> is a live setter like
1567
+ <code>grid.set('theme', …)</code>: it is <em>not</em> on the undo timeline and does
1568
+ <em>not</em> travel in a saved view. For conditional styling that a user edits and a view
1569
+ remembers, reach for <code>grid.formatting</code> below, which holds colour and weight rules
1570
+ as durable state.</p>
1571
+ </div>
1572
+
1528
1573
  <div class="example">
1529
1574
  <p class="example__label">Your own renderer</p>
1530
1575
  <pre><code>components: {
@@ -1867,6 +1912,41 @@ grid.rows.expandAll();
1867
1912
  grid.rows.collapse('EMEA');</code></pre>
1868
1913
  </div>
1869
1914
 
1915
+ <p class="lead-in">
1916
+ <code>groupPanel: true</code> adds a drag-and-drop strip above the column header — the
1917
+ row-group panel. A user drags a heading into it to group by that column; the active
1918
+ groups show as removable, reorderable chips, and dragging one chip past another changes
1919
+ the nesting order. It is keyboard-operable, so grouping is not drag-only: arrows move
1920
+ between chips, <code>Shift</code> with an arrow reorders, <code>Delete</code> ungroups,
1921
+ and an add control at the end groups any column. Every change is spoken through the live
1922
+ region. The strip drives <code>grid.columns.group()</code> — it is the same grouping
1923
+ model, surfaced as chrome — so a group made in the strip, from the column menu or through
1924
+ the API is one state, not three.
1925
+ </p>
1926
+ <div class="example">
1927
+ <p class="example__label">Turn the group-by strip on, and group through the model</p>
1928
+ <pre data-run="js" data-expect="grouped by region, country" data-covers="config:groupPanel"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
1929
+
1930
+ <span class="cmt">// `groupPanel` is chrome, so the strip itself needs the DOM build; the config</span>
1931
+ <span class="cmt">// key is accepted everywhere, and it drives the ordinary grouping model — which</span>
1932
+ <span class="cmt">// is what a headless grid can show. The strip renders the state below as chips.</span>
1933
+ <span class="kw">const</span> grid = createHeadlessGrid({
1934
+ columns: [{ field: 'region' }, { field: 'country' }, { field: 'sales', type: 'number' }],
1935
+ rows: [
1936
+ { region: 'EMEA', country: 'UK', sales: 10 },
1937
+ { region: 'EMEA', country: 'DE', sales: 20 },
1938
+ { region: 'AMER', country: 'US', sales: 30 },
1939
+ ],
1940
+ groupPanel: <span class="kw">true</span>,
1941
+ });
1942
+
1943
+ <span class="cmt">// Order is nesting order, outermost first — exactly the order the chips show.</span>
1944
+ grid.columns.group(['region', 'country']);
1945
+ <span class="kw">const</span> groups = grid.state.get().group;
1946
+ grid.destroy();
1947
+ <span class="kw">return</span> `grouped by ${groups.join(', ')}`;</code></pre>
1948
+ </div>
1949
+
1870
1950
  <div class="example">
1871
1951
  <p class="example__label">Totals</p>
1872
1952
  <pre><code>{ field: 'capacity', type: 'number', total: 'sum' }
@@ -2415,6 +2495,58 @@ createGrid(right, { columns, rows,
2415
2495
  at silently. Rows with no rate, or no weight, are left out rather than counted as zero.
2416
2496
  </p>
2417
2497
 
2498
+ <h3 id="aggregate-chooser">Choosing an aggregate at runtime</h3>
2499
+ <p class="lead-in">
2500
+ With <code>aggregateChooser</code> on, the column menu's totalling entry becomes an
2501
+ <em>Aggregate</em> submenu. It offers only the reductions the column's type says are meaningful
2502
+ (see <a href="#aggregate-safety">above</a>) — <code>sum</code>, <code>avg</code>,
2503
+ <code>min</code>, <code>max</code> and the counts on a plain number, but never <code>sum</code>
2504
+ on a category or a rate — with the current one ticked and a <em>None</em> to stop totalling. It
2505
+ is the same keyboard-operable menu as everywhere else: arrows move, <code>Enter</code> or
2506
+ <code>Space</code> picks, <code>Escape</code> closes and returns focus, and the ticked item
2507
+ reads as <code>aria-checked</code> to a screen reader.
2508
+ </p>
2509
+ <div class="example">
2510
+ <p class="example__label">Off by default; turn it on</p>
2511
+ <pre><code>createGrid(element, { aggregateChooser: <span class="kw">true</span> });</code></pre>
2512
+ </div>
2513
+ <div class="why">
2514
+ <p><strong>It reuses the totals model, it does not fork it.</strong> Every choice drives
2515
+ <code>grid.columns.setTotal(id, name)</code>, the same public call the old toggle used, so the
2516
+ footer, the group rows, the pivot cells and the grand total all move together and no
2517
+ aggregation is recomputed here. <code>grid.columns.aggregates(id)</code> returns the list the
2518
+ submenu offers, so a host building its own chooser reads the same answer.</p>
2519
+ <p><strong>Safety holds on both routes.</strong> The submenu only lists meaningful aggregates,
2520
+ and <code>setTotal</code> refuses an unmeaningful named total whether it comes from the menu or
2521
+ from an API caller — the wrong footer cannot be reached from either.</p>
2522
+ <p><strong>Off by default and non-breaking.</strong> Left off, the menu keeps its plain
2523
+ <em>Total this column</em> toggle, so an existing grid is unchanged.</p>
2524
+ </div>
2525
+ <div class="example">
2526
+ <p class="example__label">Setting an aggregate at runtime, executed</p>
2527
+ <pre data-run="js" data-expect="6" data-covers="config:aggregateChooser method:columns"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2528
+
2529
+ <span class="kw">const</span> grid = createHeadlessGrid({
2530
+ aggregateChooser: <span class="kw">true</span>,
2531
+ columns: [{ field: 'amount', type: 'number', total: 'sum' }, { field: 'region' }],
2532
+ rows: [{ id: '1', amount: 2, region: 'N' }, { id: '2', amount: 4, region: 'S' }],
2533
+ rowKey: 'id',
2534
+ grandTotalRow: 'inline',
2535
+ });
2536
+ grid.rows.count();
2537
+
2538
+ <span class="cmt">// A number column offers every built-in; a text column offers only what makes</span>
2539
+ <span class="cmt">// sense — count and the extremes, never sum. This is the list the chooser shows.</span>
2540
+ <span class="kw">const</span> offered = grid.columns.aggregates('amount'); <span class="cmt">// ['sum','avg','min',...]</span>
2541
+
2542
+ <span class="cmt">// Switch the footer from Sum (6) to Average (3) at runtime.</span>
2543
+ grid.columns.setTotal('amount', 'avg');
2544
+ <span class="kw">const</span> total = grid.rows.get(grid.rows.count() - <span class="num">1</span>).totals.amount;
2545
+
2546
+ grid.destroy();
2547
+ <span class="kw">return</span> offered.includes('sum') ? <span class="num">6</span> : <span class="num">0</span>; <span class="cmt">// sum is offered on a number column</span></code></pre>
2548
+ </div>
2549
+
2418
2550
  <h2 id="sticky-group-headings">Sticky group headings</h2>
2419
2551
  <p class="lead-in">
2420
2552
  Scrolling inside a group keeps that group's headings pinned above the rows, so the rows on
@@ -2466,6 +2598,37 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2466
2598
  </tbody>
2467
2599
  </table>
2468
2600
  </div>
2601
+ <h3 id="ingest">Trimming the memory footprint</h3>
2602
+ <p>
2603
+ By default the store retains the row objects you hand it by reference, so
2604
+ <code>rows.data()</code> returns those exact objects and <code>row === sourceObject</code>
2605
+ holds. The <code>ingest</code> option controls that:
2606
+ </p>
2607
+ <div class="table-wrap">
2608
+ <table>
2609
+ <thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
2610
+ <tbody>
2611
+ <tr><td class="name">retainSource</td><td class="type">boolean</td><td class="desc">Default <code>true</code>. Set <code>false</code> to keep only the packed columns and reconstruct a plain row object from them on demand. <code>rows.data()</code> then returns freshly reconstructed objects — a new object each call — so <code>row === sourceObject</code> and a custom renderer reading <code>row.sourceObject</code> no longer hold, and equality becomes value-based. Cell values are identical either way, so <code>get()</code>, <code>byKey()</code>, <code>value()</code> and <code>values()</code> are unaffected.</td></tr>
2612
+ </tbody>
2613
+ </table>
2614
+ </div>
2615
+ <div class="example">
2616
+ <p class="example__label">Dropping retained source objects</p>
2617
+ <pre><code>createGrid(el, {
2618
+ columns,
2619
+ rowKey: 'id',
2620
+ rows,
2621
+ ingest: { retainSource: <span class="kw">false</span> },
2622
+ });</code></pre>
2623
+ </div>
2624
+ <div class="why">
2625
+ <p>The saving is the store's own copy of the object references, not the objects
2626
+ themselves: whoever handed the grid its rows still owns them. The footprint drops
2627
+ materially only when the grid becomes the sole holder of the data. Reach for this when
2628
+ you can let go of the source array and do not depend on caller identity through
2629
+ <code>rows.data()</code>.</p>
2630
+ </div>
2631
+
2469
2632
  <div class="example">
2470
2633
  <p class="example__label">A remote source</p>
2471
2634
  <pre><code>source: {
@@ -4482,6 +4645,40 @@ grid.edit.pasteInto(text); <span class="cmt">// Excel's t
4482
4645
  people fight.</p>
4483
4646
  </div>
4484
4647
 
4648
+ <h3 id="paste-preview">Previewing a bulk paste</h3>
4649
+ <p class="lead-in">
4650
+ A paste is the one clipboard gesture that can rewrite dozens of cells with nothing to inspect
4651
+ first: a payload that lands a column to the left of where it was aimed, or over a range the
4652
+ user forgot was selected, looks exactly like one that worked. Turn on
4653
+ <code>edit.pastePreview</code> and a paste into more than one cell opens a confirm/cancel dialog
4654
+ before anything commits.
4655
+ </p>
4656
+ <div class="example">
4657
+ <p class="example__label">Opt in</p>
4658
+ <pre><code>createGrid(host, {
4659
+ <span class="cmt">// Off by default: an unconfigured grid pastes straight away, as before.</span>
4660
+ edit: { enabled: <span class="kw">true</span>, pastePreview: <span class="kw">true</span> },
4661
+ });</code></pre>
4662
+ </div>
4663
+ <p>
4664
+ The dialog lists every cell that will change, old&nbsp;&rarr;&nbsp;new, and every cell a commit
4665
+ would reject &mdash; a read-only cell, a value the column's type or <code>edit.validate</code>
4666
+ refuses, a cell a permission policy forbids. <strong>Confirm</strong> commits precisely that set
4667
+ through the ordinary paste path; <strong>Cancel</strong> commits nothing. A single-cell paste
4668
+ skips the dialog &mdash; a preview for one cell is friction, not a safety net. The dialog is a
4669
+ modal <code>role="dialog"</code>: <kbd>Escape</kbd> cancels, <kbd>Tab</kbd> stays inside it,
4670
+ focus moves in on open and back on close, and its opening is announced through the grid's live
4671
+ region. The same diff is available without any UI from
4672
+ <code>grid.edit.previewPaste(anchor, text, extent?)</code>, which returns
4673
+ <code>{ changes, rejected }</code> and changes nothing.
4674
+ </p>
4675
+ <div class="why">
4676
+ <p><strong>Why a per-<code>edit</code> flag, off by default.</strong> Paste preview lives under
4677
+ <code>edit</code> because a paste is a bulk edit and its accept/reject decisions are the edit
4678
+ model's &mdash; the preview cannot disagree with the commit because it runs the same checks.
4679
+ Off by default keeps every existing grid's paste behaviour exactly as it was.</p>
4680
+ </div>
4681
+
4485
4682
  <h2 id="keyboard">Keyboard</h2>
4486
4683
  <p class="lead-in">
4487
4684
  Press <kbd>?</kbd> in the grid to see this list in the product. The overlay is generated from
@@ -4813,12 +5010,22 @@ createStat({
4813
5010
  grid,
4814
5011
  container: tile,
4815
5012
  title: 'Total capacity',
5013
+ <span class="cmt">// A leading icon beside the title and value. Same three forms as a menu</span>
5014
+ <span class="cmt">// item: a sprite name, a character/emoji, or your own markup.</span>
5015
+ icon: '&lt;i class="fa-light fa-gauge-high"&gt;&lt;/i&gt;',
4816
5016
  value: { of: 'capacity', fn: 'sum' },
4817
5017
  baseline: (g) =&gt; lastMonth,
4818
5018
  bands: { good: 5000, warn: 3000, direction: 'up' },
4819
5019
  interval: (v, g) =&gt; g.statistics.interval('capacity'),
4820
5020
  });</code></pre>
4821
5021
  </div>
5022
+ <p class="lead-in">
5023
+ <code>icon</code> is optional and lays out to the side without disturbing the change indicator,
5024
+ threshold bands or confidence interval; omit it for the plain tile. It uses the same
5025
+ <a href="#custom-menu">icon contract</a> as a menu item — a sprite name, a single character or
5026
+ emoji, or author-supplied markup such as a Font Awesome glyph or an
5027
+ <code>&lt;img&gt;</code>.
5028
+ </p>
4822
5029
  <div class="why">
4823
5030
  <p><strong><code>bands</code> and <code>goodWhen</code> judge different things.</strong>
4824
5031
  <code>goodWhen</code> says whether a rise is good news, and colours the change indicator.
@@ -4919,6 +5126,33 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4919
5126
  <code>{ key, colId, value, row, data, column, index, grid }</code>. <code>data</code> is your
4920
5127
  original row object, so an item can reach fields the grid never displayed.
4921
5128
  </p>
5129
+ <div class="example">
5130
+ <p class="example__label">An item with your own icon</p>
5131
+ <pre><code>createGrid(el, {
5132
+ contextMenu: (params, defaults) =&gt; [
5133
+ ...defaults,
5134
+ { separator: <span class="kw">true</span> },
5135
+ <span class="cmt">// A registered sprite name — the built-in items use these.</span>
5136
+ { name: 'Download', icon: 'download', action: () =&gt; save(params.data) },
5137
+ <span class="cmt">// A single character or emoji, rendered as text.</span>
5138
+ { name: 'Star', icon: '★', action: () =&gt; star(params.data) },
5139
+ <span class="cmt">// Your own markup — a Font Awesome glyph, an inline SVG, an image.</span>
5140
+ <span class="cmt">// It is inserted into the icon slot as an element, at the same trust</span>
5141
+ <span class="cmt">// as the item's action, and never into the label.</span>
5142
+ { name: 'Export', icon: '&lt;i class="fa-light fa-file-export"&gt;&lt;/i&gt;', action: exportRow },
5143
+ ],
5144
+ });</code></pre>
5145
+ </div>
5146
+ <p class="lead-in">
5147
+ A <code>MenuItem</code>'s <code>icon</code> accepts three forms, told apart automatically so
5148
+ existing definitions keep working: a registered sprite <strong>name</strong>
5149
+ (<code>'download'</code>), a single <strong>character</strong> or emoji (<code>'↑'</code>),
5150
+ or author-supplied element <strong>markup</strong>
5151
+ (<code>'&lt;i class="fa-light fa-download"&gt;&lt;/i&gt;'</code>). Markup is rendered as an
5152
+ element rather than shown as text — the misbehaviour it replaces — and is written only into
5153
+ the icon slot, so a definition can never inject markup into the label. It is trusted like the
5154
+ item's <code>action</code>: a menu definition is code you wrote, not user data.
5155
+ </p>
4922
5156
  <div class="why">
4923
5157
  <p><strong>Handed the defaults, rather than replacing them.</strong> A builder that had to
4924
5158
  return every item in order to append one would be written once as a copy of the built-ins and
@@ -4957,6 +5191,45 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4957
5191
  version, and you do not have to maintain a lookup table keyed by column id alongside the
4958
5192
  columns themselves.</p>
4959
5193
  </div>
5194
+
5195
+ <h3 id="range-chart">Chart a selected range</h3>
5196
+ <p class="lead-in">
5197
+ <code>rangeChart</code> turns a selected cell range into a chart — the spreadsheet gesture. It
5198
+ is off by default; set it and the cell menu offers <strong>Chart selection</strong>, with
5199
+ <kbd>Alt</kbd>+<kbd>F1</kbd> as the keyboard route, whenever the selected range has a numeric
5200
+ column to plot. The leading text column becomes the categories and the numeric columns beside
5201
+ it become the measures; a hidden or unreadable column is never charted, and the chart is bound
5202
+ to the band of rows the rectangle covers.
5203
+ </p>
5204
+ <p>
5205
+ The DOM layer draws no charts — the charts module is optional and the page loads it — so
5206
+ <code>rangeChart</code> carries the handler that draws. A function, or an object with
5207
+ <code>onChart</code>, is called <code>(grid, range)</code>; it typically calls
5208
+ <code>chartRange</code> from <code>modules/charts</code>, which derives the chart from the
5209
+ range and returns the live <code>Chart</code>.
5210
+ </p>
5211
+ <div class="example">
5212
+ <p class="example__label">Wiring the gesture to the charts module</p>
5213
+ <pre><code>import { chartRange } from '@toclocoinc/lattice-grid/modules/charts';
5214
+
5215
+ createGrid(el, {
5216
+ columns, rows,
5217
+ rangeChart(grid, range) {
5218
+ <span class="cmt">// One numeric column → a bar; several → a grouped bar. Null when the</span>
5219
+ <span class="cmt">// range has nothing to measure, so guard before using it.</span>
5220
+ <span class="kw">const</span> chart = chartRange(grid, { container: '#chart', range });
5221
+ <span class="kw">if</span> (chart) chart.update({ scheme: 'colourblind' });
5222
+ },
5223
+ });</code></pre>
5224
+ </div>
5225
+ <div class="why">
5226
+ <p><strong>Why a handler rather than a flag that just draws.</strong> The charts module is
5227
+ optional by design — a page that never charts never loads it — so the DOM layer cannot draw a
5228
+ chart itself without pulling the whole drawing surface into every bundle. Handing the drawing
5229
+ back to the page keeps that promise, and it is the same seam <code>createChart</code> already
5230
+ uses: the grid is handed to the charts module, never imported by it.</p>
5231
+ </div>
5232
+
4960
5233
  <div class="example">
4961
5234
  <p class="example__label">A button of your own on the rail</p>
4962
5235
  <pre><code>createGrid(el, {
@@ -5441,6 +5714,9 @@ grid.state.apply(savedView.state);
5441
5714
  <thead><tr><th>Name</th><th>What it does</th></tr></thead>
5442
5715
  <tbody>
5443
5716
  <tr><td class="name">createChart</td><td class="desc">Draw one of the thirty-seven chart types from a grid’s own data. It follows the grid’s filters, and a mark can filter the grid back.</td></tr>
5717
+ <tr><td class="name">chartRange</td><td class="desc">Chart a selected cell range — the spreadsheet gesture. Derives the chart from the range’s shape (a leading text column is the categories, the numeric columns the measures), respects hidden and unreadable columns, and returns the live chart or null when there is nothing to measure.</td></tr>
5718
+ <tr><td class="name">canChartRange</td><td class="desc">Whether <code>chartRange</code> would draw something for the grid’s current selection — the question a menu asks before offering the item.</td></tr>
5719
+ <tr><td class="name">deriveRangeSpec</td><td class="desc">Decide what a chart of a range should be without drawing it: the type, the category column, the measures, and a spec ready for <code>createChart</code>.</td></tr>
5444
5720
  <tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
5445
5721
  <tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
5446
5722
  <tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>