@toclocoinc/lattice-grid 1.35.0 → 1.36.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 +1 -1
- package/docs/API.html +153 -1
- package/docs/api-detail.html +87 -2
- package/lattice-grid.d.ts +88 -2
- package/lattice-grid.esm.min.js +557 -98
- package/lattice-grid.min.cjs +556 -98
- package/lattice-grid.min.js +556 -98
- package/modules/angular.esm.min.js +12 -3
- package/modules/angular.min.cjs +12 -3
- package/modules/angular.min.js +12 -3
- package/modules/chart-alluvial.esm.min.js +1 -1
- package/modules/chart-arc.esm.min.js +1 -1
- package/modules/chart-bubblemap.esm.min.js +1 -1
- package/modules/chart-bump.esm.min.js +1 -1
- package/modules/chart-calendar.esm.min.js +1 -1
- package/modules/chart-decomposition.esm.min.js +1 -1
- package/modules/chart-diverging.esm.min.js +1 -1
- package/modules/chart-dumbbell.esm.min.js +1 -1
- package/modules/chart-fan.esm.min.js +1 -1
- package/modules/chart-hexbin.esm.min.js +1 -1
- package/modules/chart-hexmap.esm.min.js +1 -1
- package/modules/chart-icicle.esm.min.js +1 -1
- package/modules/chart-parallel.esm.min.js +1 -1
- package/modules/chart-ridgeline.esm.min.js +1 -1
- package/modules/chart-roc.esm.min.js +1 -1
- package/modules/chart-slope.esm.min.js +1 -1
- package/modules/chart-splom.esm.min.js +1 -1
- package/modules/chart-waffle.esm.min.js +1 -1
- package/modules/charts.esm.min.js +4 -4
- package/modules/charts.min.cjs +4 -4
- package/modules/charts.min.js +4 -4
- package/modules/data-router.esm.min.js +30 -11
- package/modules/data-router.min.cjs +30 -11
- package/modules/data-router.min.js +30 -11
- package/modules/devtools.esm.min.js +2 -2
- package/modules/devtools.min.cjs +2 -2
- package/modules/devtools.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +4 -4
- package/modules/dhtmlx-compat.min.cjs +4 -4
- package/modules/dhtmlx-compat.min.js +4 -4
- package/modules/gantt.esm.min.js +367 -16
- package/modules/gantt.min.cjs +367 -16
- package/modules/gantt.min.js +367 -16
- package/modules/htmx.esm.min.js +555 -98
- package/modules/htmx.min.cjs +555 -98
- package/modules/htmx.min.js +555 -98
- package/modules/kanban.esm.min.js +110 -10
- package/modules/kanban.min.cjs +110 -10
- package/modules/kanban.min.js +110 -10
- package/modules/kpi.esm.min.js +4 -4
- package/modules/kpi.min.cjs +4 -4
- package/modules/kpi.min.js +4 -4
- package/modules/mock-socket.esm.min.js +2 -2
- package/modules/mock-socket.min.cjs +2 -2
- package/modules/mock-socket.min.js +2 -2
- package/modules/react.esm.min.js +12 -3
- package/modules/react.min.cjs +12 -3
- package/modules/react.min.js +12 -3
- package/modules/svelte.esm.min.js +12 -3
- package/modules/svelte.min.cjs +12 -3
- package/modules/svelte.min.js +12 -3
- package/modules/vue.esm.min.js +12 -3
- package/modules/vue.min.cjs +12 -3
- package/modules/vue.min.js +12 -3
- package/modules/webcomponent.esm.min.js +556 -98
- package/modules/webcomponent.min.cjs +556 -98
- package/modules/webcomponent.min.js +556 -98
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
dependencies, no build step required. Optional adapters for React, Vue, Svelte
|
|
5
5
|
and Web Components ship alongside it.
|
|
6
6
|
|
|
7
|
-
Version 1.
|
|
7
|
+
Version 1.36.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/docs/API.html
CHANGED
|
@@ -2478,6 +2478,49 @@ grid.licence.state(); <span class="cmt">// 'licensed' | 'localhost' | 'tria
|
|
|
2478
2478
|
</table>
|
|
2479
2479
|
</div>
|
|
2480
2480
|
|
|
2481
|
+
<h3 id="url-source">Load from a URL: a JSON or NDJSON file</h3>
|
|
2482
|
+
<p class="section-note">
|
|
2483
|
+
<code>createUrlSource(url, opts)</code> points the grid straight at a file. A <strong>JSON</strong>
|
|
2484
|
+
file (a top-level array, or a nested array picked out with <code>rowsPath</code> or <code>map</code>)
|
|
2485
|
+
is read whole and handed over as rows. An <strong>NDJSON / JSONL</strong> file — one JSON value
|
|
2486
|
+
per line — is <strong>streamed in incrementally</strong>: the first rows render while the rest is
|
|
2487
|
+
still arriving, and a large file never sits in memory as one string. It is built on the
|
|
2488
|
+
<code>stream</code> source, so it inherits the frame-coalesced render, the <code>stream:chunk</code> /
|
|
2489
|
+
<code>stream:end</code> progress events and promotion to memory once a small file has fully landed.
|
|
2490
|
+
</p>
|
|
2491
|
+
<p>Format is resolved in order: an explicit <code>format: 'json' | 'ndjson'</code> wins; else the URL
|
|
2492
|
+
extension (<code>.ndjson</code>/<code>.jsonl</code> vs <code>.json</code>); else the
|
|
2493
|
+
<code>Content-Type</code> (<code>application/x-ndjson</code> vs <code>application/json</code>); else a
|
|
2494
|
+
sniff of the first bytes, or a clear error asking for an explicit <code>format</code>. Pass
|
|
2495
|
+
<code>fetch</code> to supply auth or a proxy (default is the global <code>fetch</code>),
|
|
2496
|
+
<code>headers</code> to merge request headers, <code>batchSize</code> to tune NDJSON chunking
|
|
2497
|
+
(default 500), <code>lenient: true</code> to skip a malformed NDJSON line with a warning rather
|
|
2498
|
+
than failing, and <code>poll</code> (ms) to re-fetch on an interval, replacing the rows each pass.
|
|
2499
|
+
Every failure — a non-2xx status, a network error, a bad body, a malformed line — surfaces
|
|
2500
|
+
as <code>source:error</code> and leaves the grid usable, never an uncaught throw. The fetch is aborted
|
|
2501
|
+
when the grid is destroyed. Zero new dependencies: <code>fetch</code>,
|
|
2502
|
+
<code>response.body.getReader()</code> and <code>TextDecoder</code>.</p>
|
|
2503
|
+
<p class="section-note">A JSON file loaded through a mock transport so the example runs headless with no
|
|
2504
|
+
network; in an app, omit <code>fetch</code> and the global one is used. Run on every build.</p>
|
|
2505
|
+
<pre data-run="js" data-expect="3" data-covers="export:createUrlSource"><code><span class="kw">const</span> { createHeadlessGrid, createUrlSource } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
2506
|
+
|
|
2507
|
+
<span class="cmt">// A mock transport, so this runs with no network. In your app, drop `fetch`.</span>
|
|
2508
|
+
<span class="kw">const</span> file = JSON.stringify([{ id: 1, city: 'Oslo' }, { id: 2, city: 'Lima' }, { id: 3, city: 'Cairo' }]);
|
|
2509
|
+
<span class="kw">const</span> fetchImpl = <span class="kw">async</span> () => <span class="kw">new</span> Response(file, { headers: { 'content-type': 'application/json' } });
|
|
2510
|
+
|
|
2511
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
2512
|
+
rowKey: 'id',
|
|
2513
|
+
columns: [{ field: 'id' }, { field: 'city' }],
|
|
2514
|
+
source: createUrlSource('https://example.test/cities.json', { fetch: fetchImpl }),
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
<span class="kw">await</span> <span class="kw">new</span> Promise((r) => setTimeout(r, 50)); <span class="cmt">// let the file load</span>
|
|
2518
|
+
<span class="kw">const</span> count = grid.rows.count();
|
|
2519
|
+
grid.destroy();
|
|
2520
|
+
<span class="kw">return</span> count; <span class="cmt">// 3</span></code></pre>
|
|
2521
|
+
<p>For NDJSON, point it at a <code>.ndjson</code> or <code>.jsonl</code> URL —
|
|
2522
|
+
<code>createUrlSource('/events.ndjson')</code> — and the rows stream in as they parse.</p>
|
|
2523
|
+
|
|
2481
2524
|
<h3 id="derived-source">Derived sources: a grid built from another grid</h3>
|
|
2482
2525
|
<p class="section-note">
|
|
2483
2526
|
Most dashboards put a summary panel beside a table, the top five sales people, the
|
|
@@ -3639,6 +3682,35 @@ off(); <span class="cmt">// on() returns i
|
|
|
3639
3682
|
<tr><td class="name">presence:left</td><td class="desc">A peer disconnected.</td></tr>
|
|
3640
3683
|
<tr><td class="name">presence:failed</td><td class="desc">A presence transport error. Presence is lossy by design; this is informational.</td></tr>
|
|
3641
3684
|
<tr><td class="name">presence:lockRefused</td><td class="desc">An edit was refused because a peer holds the cell.</td></tr>
|
|
3685
|
+
<tr><td class="name">export:request</td><td class="type">{ request }</td><td class="desc">A remote export was requested. Past-tense notification.</td></tr>
|
|
3686
|
+
<tr><td class="name">export:done</td><td class="type">{ request, remote }</td><td class="desc">A remote export completed. Past-tense notification.</td></tr>
|
|
3687
|
+
<tr><td class="name">shortcuts:opened</td><td class="type">{}</td><td class="desc">The keyboard-shortcuts help overlay opened. Past-tense notification.</td></tr>
|
|
3688
|
+
<tr><td class="name">shortcuts:closed</td><td class="type">{}</td><td class="desc">The keyboard-shortcuts help overlay closed. Past-tense notification.</td></tr>
|
|
3689
|
+
<tr><td class="name">print:before</td><td class="type">{ rows }</td><td class="desc">Print mode is about to snapshot. Past-tense notification, not cancellable.</td></tr>
|
|
3690
|
+
<tr><td class="name">print:after</td><td class="type">{ rows }</td><td class="desc">Print mode restored the grid. Fires even if the print was cancelled by the browser.</td></tr>
|
|
3691
|
+
<tr><td colspan="3" class="desc"><strong>Cancellable before-events (BACKLOG-0000943).</strong> Each gates a user-initiated mutation. The handler receives a <code>BeforeEvent</code> carrying the action context plus <code>preventDefault(reason?)</code>, <code>defaultPrevented</code> and <code>reason</code>. Calling <code>preventDefault()</code> (or returning <code>false</code>) cancels the action; the handler may be <code>async</code>, and the mutation is held until every before-handler settles. On a veto the paired <code><action>:cancelled</code> fires with the reason. Host/API writes and remote/router-applied deltas (origin !== <code>'user'</code>) do not fire these. The <code>origin</code> field distinguishes a genuine user gesture from a module-driven re-entry, which is how a host deduplicates.</td></tr>
|
|
3692
|
+
<tr><td class="name">beforeEdit</td><td class="type">{ row, key, mode, changes, origin }</td><td class="desc">A validated cell/row commit is about to apply. <code>changes</code> is <code>[{ colId, oldValue, newValue }]</code>. Validation (<code>edit.validate</code>) is separate and runs first.</td></tr>
|
|
3693
|
+
<tr><td class="name">beforeSort</td><td class="type">{ sort, origin }</td><td class="desc">A sort is about to be set.</td></tr>
|
|
3694
|
+
<tr><td class="name">beforeFilter</td><td class="type">{ filters?, quick?, kind, origin }</td><td class="desc">A structured (<code>kind: 'structured'</code>) or quick (<code>kind: 'quick'</code>) filter is about to be set.</td></tr>
|
|
3695
|
+
<tr><td class="name">beforeColumnMove</td><td class="type">{ column, to, origin }</td><td class="desc">A column reorder is about to apply.</td></tr>
|
|
3696
|
+
<tr><td class="name">beforeColumnResize</td><td class="type">{ column, width, origin }</td><td class="desc">A column width change is about to apply.</td></tr>
|
|
3697
|
+
<tr><td class="name">beforeColumnHide</td><td class="type">{ columns, origin }</td><td class="desc">One or more columns are about to be hidden.</td></tr>
|
|
3698
|
+
<tr><td class="name">beforeSelect</td><td class="type">{ keys, previous, origin }</td><td class="desc">A user selection change is about to apply. A veto snaps back to the last announced selection.</td></tr>
|
|
3699
|
+
<tr><td class="name">beforeRowAdd</td><td class="type">{ row, origin }</td><td class="desc">An optimistic row append is about to apply.</td></tr>
|
|
3700
|
+
<tr><td class="name">beforeDelete</td><td class="type">{ key, rows, origin }</td><td class="desc">An optimistic row delete is about to apply — the canonical confirm-before-delete hook.</td></tr>
|
|
3701
|
+
<tr><td class="name">beforeRowMove</td><td class="type">{ key, from, to, origin }</td><td class="desc">A row reorder is about to apply.</td></tr>
|
|
3702
|
+
<tr><td class="name">beforeGroup</td><td class="type">{ key, expanded, origin }</td><td class="desc">A group/tree expand or collapse is about to apply.</td></tr>
|
|
3703
|
+
<tr><td class="name">edit:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeEdit</code> was vetoed. <code>reason</code> is <code>'stale'</code> when a live delta moved the cell during an async gate.</td></tr>
|
|
3704
|
+
<tr><td class="name">sort:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeSort</code> was vetoed.</td></tr>
|
|
3705
|
+
<tr><td class="name">filter:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeFilter</code> was vetoed.</td></tr>
|
|
3706
|
+
<tr><td class="name">columnMove:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeColumnMove</code> was vetoed.</td></tr>
|
|
3707
|
+
<tr><td class="name">columnResize:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeColumnResize</code> was vetoed.</td></tr>
|
|
3708
|
+
<tr><td class="name">columnHide:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeColumnHide</code> was vetoed.</td></tr>
|
|
3709
|
+
<tr><td class="name">selection:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeSelect</code> was vetoed; the selection snapped back.</td></tr>
|
|
3710
|
+
<tr><td class="name">rowAdd:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeRowAdd</code> was vetoed.</td></tr>
|
|
3711
|
+
<tr><td class="name">delete:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeDelete</code> was vetoed. <code>reason</code> is <code>'stale'</code> when the row was already gone.</td></tr>
|
|
3712
|
+
<tr><td class="name">rowMove:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeRowMove</code> was vetoed. <code>reason</code> is <code>'stale'</code> when the row had moved.</td></tr>
|
|
3713
|
+
<tr><td class="name">group:cancelled</td><td class="type">{ ...context, reason }</td><td class="desc">A <code>beforeGroup</code> was vetoed.</td></tr>
|
|
3642
3714
|
</tbody>
|
|
3643
3715
|
</table>
|
|
3644
3716
|
</div>
|
|
@@ -3672,6 +3744,67 @@ grid.destroy();
|
|
|
3672
3744
|
handlerName('annotation:changed'), <span class="cmt">// the adapter prop the frameworks expose</span>
|
|
3673
3745
|
].join(' | ');</code></pre>
|
|
3674
3746
|
|
|
3747
|
+
<h3 id="before-events-example">Cancellable before-events: guarded editing and confirm-before-delete, executed</h3>
|
|
3748
|
+
<p class="section-note">Every user-initiated mutation has a cancellable <code>before</code> event
|
|
3749
|
+
(BACKLOG-0000943). A handler cancels the pending action with
|
|
3750
|
+
<code>preventDefault(reason?)</code> and may be <code>async</code> — the mutation is held until it
|
|
3751
|
+
settles, which is what makes a confirm dialog or a server check a genuine gate. On a veto the paired
|
|
3752
|
+
<code><action>:cancelled</code> fires with the reason. Host/API writes and remote deltas do not
|
|
3753
|
+
fire them. Run headless on every build.</p>
|
|
3754
|
+
<pre data-run="js" data-expect="0|Ann|sort,edit locked" data-covers="event:beforeEdit event:beforeSort event:beforeFilter event:beforeColumnMove event:beforeColumnResize event:beforeColumnHide event:beforeSelect event:beforeRowAdd event:beforeDelete event:beforeRowMove event:beforeGroup event:edit:cancelled event:sort:cancelled event:filter:cancelled event:columnMove:cancelled event:columnResize:cancelled event:columnHide:cancelled event:selection:cancelled event:rowAdd:cancelled event:delete:cancelled event:rowMove:cancelled event:group:cancelled event:print:before event:print:after event:export:request event:export:done event:shortcuts:opened event:shortcuts:closed"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
3755
|
+
<span class="kw">const</span> grid = createHeadlessGrid({
|
|
3756
|
+
rowKey: 'id',
|
|
3757
|
+
columns: [{ field: 'id' }, { field: 'name', edit: { enabled: <span class="kw">true</span> } }, { field: 'v', type: 'number', edit: { enabled: <span class="kw">true</span> } }],
|
|
3758
|
+
rows: [{ id: 'a', name: 'Ann', v: 1 }, { id: 'b', name: 'Bo', v: 2 }],
|
|
3759
|
+
selection: 'multiple',
|
|
3760
|
+
});
|
|
3761
|
+
|
|
3762
|
+
<span class="kw">const</span> log = [];
|
|
3763
|
+
|
|
3764
|
+
<span class="cmt">// Guarded editing: veto a commit on a locked row, and hear the cancellation.</span>
|
|
3765
|
+
grid.on('beforeEdit', (e) => { <span class="kw">if</span> (e.key === 'a') e.preventDefault('locked'); });
|
|
3766
|
+
grid.on('edit:cancelled', (e) => log.push('edit ' + e.reason));
|
|
3767
|
+
|
|
3768
|
+
<span class="cmt">// Guard the query and layout surfaces.</span>
|
|
3769
|
+
grid.on('beforeSort', (e) => e.preventDefault('view-locked'));
|
|
3770
|
+
grid.on('sort:cancelled', () => log.push('sort'));
|
|
3771
|
+
grid.on('beforeFilter', (e) => e.preventDefault('no-filter'));
|
|
3772
|
+
grid.on('filter:cancelled', () => log.push('filter'));
|
|
3773
|
+
grid.on('beforeColumnMove', (e) => e.preventDefault('fixed'));
|
|
3774
|
+
grid.on('columnMove:cancelled', () => log.push('colmove'));
|
|
3775
|
+
grid.on('beforeColumnResize', (e) => e.preventDefault('fixed'));
|
|
3776
|
+
grid.on('columnResize:cancelled', () => log.push('colresize'));
|
|
3777
|
+
grid.on('beforeColumnHide', (e) => e.preventDefault('mandatory'));
|
|
3778
|
+
grid.on('columnHide:cancelled', () => log.push('colhide'));
|
|
3779
|
+
grid.on('beforeGroup', (e) => e.preventDefault('frozen'));
|
|
3780
|
+
grid.on('group:cancelled', () => log.push('group'));
|
|
3781
|
+
grid.on('beforeRowMove', (e) => e.preventDefault('ordered'));
|
|
3782
|
+
grid.on('rowMove:cancelled', () => log.push('rowmove'));
|
|
3783
|
+
grid.on('beforeRowAdd', (e) => e.preventDefault('quota'));
|
|
3784
|
+
grid.on('rowAdd:cancelled', () => log.push('rowadd'));
|
|
3785
|
+
grid.on('beforeSelect', () => {});
|
|
3786
|
+
grid.on('selection:cancelled', () => log.push('sel'));
|
|
3787
|
+
|
|
3788
|
+
<span class="cmt">// Confirm before delete: an async handler holds the delete until it settles.</span>
|
|
3789
|
+
grid.on('beforeDelete', <span class="kw">async</span> (e) => { <span class="kw">await</span> Promise.resolve(); e.preventDefault('user cancelled'); });
|
|
3790
|
+
grid.on('delete:cancelled', (e) => log.push('delete ' + e.reason));
|
|
3791
|
+
|
|
3792
|
+
<span class="cmt">// Past-tense notifications a host can also observe (not gates): print, remote</span>
|
|
3793
|
+
<span class="cmt">// export, and the keyboard-shortcuts overlay.</span>
|
|
3794
|
+
grid.on('print:before', () => log.push('print-before'));
|
|
3795
|
+
grid.on('print:after', () => log.push('print-after'));
|
|
3796
|
+
grid.on('export:request', () => {});
|
|
3797
|
+
grid.on('export:done', () => {});
|
|
3798
|
+
grid.on('shortcuts:opened', () => {});
|
|
3799
|
+
grid.on('shortcuts:closed', () => {});
|
|
3800
|
+
|
|
3801
|
+
<span class="cmt">// A vetoed sort (sync) and an edit blocked on the locked row.</span>
|
|
3802
|
+
grid.sort.set([{ col: 'v', dir: 'desc' }]);
|
|
3803
|
+
grid.edit.start('a', 'name');
|
|
3804
|
+
grid.edit.stop(<span class="kw">false</span>, { value: 'Nope' });
|
|
3805
|
+
|
|
3806
|
+
<span class="kw">return</span> grid.sort.get().length + '|' + grid.rows.byKey('a').data.name + '|' + log.join(',');</code></pre>
|
|
3807
|
+
|
|
3675
3808
|
<h2 id="rules">Conditional formatting</h2>
|
|
3676
3809
|
<p>Rules compile into the function <code>cell.style</code> already takes, so a compiled rule set
|
|
3677
3810
|
installs exactly like a hand-written style function.</p>
|
|
@@ -4200,6 +4333,13 @@ router.attachDefault(restGrid); <span class="cmt">// the "rest" s
|
|
|
4200
4333
|
const counts = router.load(snapshot); <span class="cmt">// keyed diff per grid: [{added,updated,removed}, ...]</span>
|
|
4201
4334
|
router.apply([{ op: 'upsert', row }, { op: 'delete', row }]); <span class="cmt">// in-place deltas by rowKey</span></code></pre>
|
|
4202
4335
|
<p><strong>A snapshot is a keyed diff, not a replace.</strong> <code>load</code> re-partitions the whole dataset and, per grid, adds the new rows, updates only the changed ones and removes the gone ones — so an unchanged row never repaints and selection, scroll and edit state survive. <strong>Deltas are applied in place</strong> by <code>rowKey</code>, last-writer-wins within a batch; an <code>upsert</code> whose partition property has changed <em>moves</em> the row (it leaves the route it no longer matches and joins the one it now does, never duplicated). A record matching no route is counted in <code>router.unrouted</code>, handed to <code>onUnrouted</code>, and — if an <code>attachDefault</code> grid exists — routed there, so nothing is ever silently lost. By default a record goes to the first route it matches; <code>overlap: true</code> fans it to every matching route.</p>
|
|
4336
|
+
<div class="note"><p><strong>Fanning one partition value to several viewers? Set <code>overlap: true</code>.</strong> The headline "route one feed to many viewers" — a grid <em>and</em> a KPI panel <em>and</em> a chart, all off one feed — needs <code>overlap: true</code>. With the default <code>overlap: false</code> a record stops at the <strong>first</strong> route it matches, so a second viewer attached to the <em>same</em> value silently receives nothing. Distinct values (one viewer per value) do not need it. When two routes claim the same value while <code>overlap</code> is false, the router emits a one-time dev warning naming the value.</p>
|
|
4337
|
+
<pre><code><span class="cmt">// One value, several viewers: a grid, a KPI panel and a chart all see 'deal'.</span>
|
|
4338
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'kind', rowKey: 'id', overlap: <span class="kw">true</span> });
|
|
4339
|
+
router.attach(dealsGrid, 'deal'); <span class="cmt">// the grid</span>
|
|
4340
|
+
router.subscribe('deal', kpiPanel); <span class="cmt">// a KPI tile off the same value</span>
|
|
4341
|
+
router.attach(dealsChart, 'deal'); <span class="cmt">// a chart off the same value</span>
|
|
4342
|
+
router.load(snapshot); <span class="cmt">// every viewer fills; without overlap:true only the grid would</span></code></pre></div>
|
|
4203
4343
|
<div class="table-wrap">
|
|
4204
4344
|
<table>
|
|
4205
4345
|
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
@@ -4218,7 +4358,7 @@ router.apply([{ op: 'upsert', row }, { op: 'delete', row }]); <span class="cmt">
|
|
|
4218
4358
|
<tr><td class="sig">flushStream()</td><td class="desc"><strong>v3:</strong> apply the buffered deltas now, coalesced into a single apply (a deterministic point, and for tests).</td></tr>
|
|
4219
4359
|
<tr><td class="sig">dropped</td><td class="desc"><strong>v3:</strong> how many stale/duplicate deltas the <code>seq</code> dedupe gate has dropped.</td></tr>
|
|
4220
4360
|
<tr><td class="sig">lastSeq() / checkpoint() / seenThrough(mark)</td><td class="desc"><strong>v3:</strong> the resume point — the highest applied <code>seq</code>, a per-record checkpoint to persist, and a way to prime it after a reconnect so an early replay is dropped.</td></tr>
|
|
4221
|
-
<tr><td class="sig">subscribe(predicate, handler, opts?)</td><td class="desc"><strong>v5:</strong> route a slice to any non-grid view. <code>handler(change)</code> receives the same keyed diff <code>{ add, update, remove }</code> a grid does — drive a KPI tile, detail pane, map or form. A peer to <code>attach</code>: same partitioning and the same <code>transform</code>/<code>filter</code>/<code>sort</code>/<code>rollup</code> options; grids and charts are unchanged.</td></tr>
|
|
4361
|
+
<tr><td class="sig">subscribe(predicate, handler, opts?)</td><td class="desc"><strong>v5:</strong> route a slice to any non-grid view. <code>handler(change)</code> receives the same keyed diff <code>{ add, update, remove }</code> a grid does — drive a KPI tile, detail pane, map or form. A peer to <code>attach</code>: same partitioning and the same <code>transform</code>/<code>filter</code>/<code>sort</code>/<code>rollup</code> options; grids and charts are unchanged. To drive a KPI/pane <em>and</em> a grid off the <strong>same</strong> partition value, create the router with <code>overlap: true</code> — with the default <code>overlap: false</code> only the first route on that value receives rows (the router warns once when it detects the clash).</td></tr>
|
|
4222
4362
|
<tr><td class="sig">alert(predicate, condition, handler, { filter?, debounce?, rowKey? })</td><td class="desc"><strong>v5:</strong> watch a slice and emit rather than render. <code>condition(rows)</code> is evaluated over the slice on every load and delta; when it first becomes truthy, <code>handler(signal, rows)</code> fires. Edge-triggered (once per crossing, re-arms on release), <code>debounce</code> coalesces a burst, and it never competes for a partition or touches a grid.</td></tr>
|
|
4223
4363
|
<tr><td class="sig">configure(spec)</td><td class="desc"><strong>v5:</strong> the whole routing graph as one data spec — <code>routes</code> (grid/<code>default</code>/<code>subscribe</code>/<code>alert</code> entries), <code>links</code>, <code>relate</code>, <code>buffer</code> — desugared to the imperative API. Composes with imperative calls and round-trips to identical behaviour. Also accepted as <code>createDataRouter({ config })</code>.</td></tr>
|
|
4224
4364
|
<tr><td class="sig">attach(grid, predicate, { writable, onWrite?, onConflict? })</td><td class="desc"><strong>v8 (BACKLOG-0000912):</strong> make a route <strong>writable</strong> — the router captures the grid's committed edits off its public edit surface (<code>grid.on('cell:changed')</code> → <code>grid.edit.setCells</code>) and routes them to <code>onWrite(change, { route, source })</code> (per-route here, or the router-global <code>onWrite</code>), reverting the cell on reject and re-entering an accepted write as a normal delta. <code>onConflict(change, { serverRow })</code> surfaces a last-write-wins conflict. A derived (<code>rollup</code>/<code>transform</code>) route cannot be writable — its edits are reverted and warned.</td></tr>
|
|
@@ -6097,6 +6237,18 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
|
|
|
6097
6237
|
</tbody>
|
|
6098
6238
|
</table>
|
|
6099
6239
|
</div>
|
|
6240
|
+
<h3 id="type-BeforeEvent">BeforeEvent</h3>
|
|
6241
|
+
<p class="section-note">A cancellable *before*-event (BACKLOG-0000943), delivered to `on('beforeX')` handlers before a user-initiated mutation is applied. A handler cancels the pending action by calling `preventDefault(reason?)`; the mutation is then abandoned and a past-tense `<action>:cancelled` event carries the reason. A handler may be `async` (or return a Promise): the grid awaits every before-handler before deciding, so a confirm dialog or a server check can gate the write. Any one handler preventing cancels the action. The action-specific fields (the edited cells, the target index, the affected rows) are spread alongside these, so a handler decides without reaching into grid internals. `origin` distinguishes a genuine user gesture from a host/module-driven or remote write, which is how a module whose move re-enters core is deduplicated by the host.</p>
|
|
6242
|
+
<div class="table-wrap">
|
|
6243
|
+
<table>
|
|
6244
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
6245
|
+
<tbody>
|
|
6246
|
+
<tr><td class="name">preventDefault</td><td class="type">(reason?: string): void</td><td class="desc">Cancel the pending action; the optional reason is surfaced on the cancellation event.</td></tr>
|
|
6247
|
+
<tr><td class="name">defaultPrevented</td><td class="type">boolean</td><td class="desc">True once any handler has called `preventDefault` or returned false.</td></tr>
|
|
6248
|
+
<tr><td class="name">reason</td><td class="type">string | null</td><td class="desc">The reason given to `preventDefault`, or null; `'stale'` when re-validation failed.</td></tr>
|
|
6249
|
+
</tbody>
|
|
6250
|
+
</table>
|
|
6251
|
+
</div>
|
|
6100
6252
|
<h3 id="type-BooleanFormat">BooleanFormat</h3>
|
|
6101
6253
|
<div class="table-wrap">
|
|
6102
6254
|
<table>
|
package/docs/api-detail.html
CHANGED
|
@@ -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.
|
|
440
|
+
<p class="rail__sub">Developer guide · v1.36.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -1869,6 +1869,38 @@ grid.rows.load(data);
|
|
|
1869
1869
|
grid.overlay.hide();</code></pre>
|
|
1870
1870
|
</div>
|
|
1871
1871
|
|
|
1872
|
+
<h3 id="url-source-guide">Straight from a URL, with <code>createUrlSource</code></h3>
|
|
1873
|
+
<p class="lead-in">
|
|
1874
|
+
When the data is a file at a URL you do not have to fetch it yourself.
|
|
1875
|
+
<code>createUrlSource(url, opts)</code> loads a JSON file or streams an NDJSON/JSONL file directly,
|
|
1876
|
+
and you pass it as the <code>source</code>. See the
|
|
1877
|
+
<a href="API.html#url-source">Sources reference</a> for every option.
|
|
1878
|
+
</p>
|
|
1879
|
+
<div class="example">
|
|
1880
|
+
<p class="example__label">A JSON file, and a streamed NDJSON file</p>
|
|
1881
|
+
<pre><code><span class="cmt">// A JSON file: a top-level array, or nested via rowsPath / map.</span>
|
|
1882
|
+
createGrid(el, { columns, rowKey: 'id', source: createUrlSource('/data/circuits.json') });
|
|
1883
|
+
|
|
1884
|
+
<span class="cmt">// An NDJSON file: rows stream in as they parse, first rows first.</span>
|
|
1885
|
+
createGrid(el, { columns, rowKey: 'id', source: createUrlSource('/data/events.ndjson', { batchSize: 500 }) });
|
|
1886
|
+
|
|
1887
|
+
<span class="cmt">// Auth, a nested array, and a 30s refresh:</span>
|
|
1888
|
+
createGrid(el, {
|
|
1889
|
+
columns, rowKey: 'id',
|
|
1890
|
+
source: createUrlSource('/api/rows', {
|
|
1891
|
+
headers: { Authorization: 'Bearer …' },
|
|
1892
|
+
rowsPath: 'result.items',
|
|
1893
|
+
poll: 30000,
|
|
1894
|
+
}),
|
|
1895
|
+
});</code></pre>
|
|
1896
|
+
</div>
|
|
1897
|
+
<p>
|
|
1898
|
+
The format is inferred from the extension, then the <code>Content-Type</code>, then a sniff —
|
|
1899
|
+
override it with <code>format: 'json' | 'ndjson'</code>. A non-2xx response, a network error or a
|
|
1900
|
+
malformed NDJSON line becomes a <code>source:error</code> event rather than an exception, and
|
|
1901
|
+
<code>lenient: true</code> skips a bad NDJSON line instead of failing the whole stream.
|
|
1902
|
+
</p>
|
|
1903
|
+
|
|
1872
1904
|
<h3>Incremental changes</h3>
|
|
1873
1905
|
<p class="lead-in">
|
|
1874
1906
|
<code>rows.load</code> replaces everything. When you have a delta, a websocket message, a
|
|
@@ -6238,7 +6270,7 @@ grid.state.apply(savedView.state);
|
|
|
6238
6270
|
<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>
|
|
6239
6271
|
<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>
|
|
6240
6272
|
<tr><td class="name">regressionPlots</td><td class="desc">Turn a fitted regression model into diagnostic chart specs ready for <code>createChart</code>: the fit line with its confidence band, residuals-vs-fitted, a QQ plot of the residuals, and a multicollinearity correlogram with the model’s VIF. The plots that need a per-row or per-coefficient quantity the grid has no column for (scale-location, residuals-vs-leverage, the coefficient forest) are returned as a null spec carrying the reason rather than dropped.</td></tr>
|
|
6241
|
-
<tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. v3 also adds per-route reshaping — <code>transform</code>/<code>filter</code>/<code>sort</code> and <code>rollup</code> ({ groupBy, aggregate }) summaries — a relationship graph (<code>relate(edges)</code>: multi-hop, several-into-one AND, and mutual edges) that scales v2's pairwise <code>link</code>, and stream hygiene: a <code>seq</code>/version orders and de-duplicates a feed (stale/duplicate deltas dropped, counted in <code>dropped</code>), <code>push</code> with a <code>batch</code>/<code>coalesce</code> buffers a high-frequency feed (<code>flushStream</code> for a deterministic point), and <code>lastSeq</code>/<code>checkpoint</code>/<code>seenThrough</code> resume precisely after a dropped socket. v4 adds time-travel: <code>buffer({ window, max })</code> records the ordered stream into a bounded ring over a moving base, so <code>scrubTo</code> reconstructs a past point, <code>replay</code> (with <code>pause</code>/<code>resume</code>) walks a range, and <code>live</code> returns to the head — every state pushed by the same keyed diff, <code>traveling</code>/<code>buffered</code> reporting the state. v6 adds cross-tab sync: <code>broadcast({ channel })</code> mirrors the ordered, de-duplicated deltas to other tabs/windows over a BroadcastChannel with no echo loop, a popped-out grid joining the same feed with no second socket and resyncing mid-stream via the reconnect path. v7 adds query-slice routing: <code>query(adapter, request)</code> sources the router from a DFQL/DuckDB (or any pushdown) adapter, partitioning one result across the routes; a route-level <code>where</code> is pushed down where the adapter's capabilities allow and the residual finished client-side, with <code>lastQueryPlan</code> reporting the split. v8 adds write-back: a <code>writable</code> route captures the grid's committed edits off its public edit surface and routes them to <code>onWrite(change, ctx)</code> (per-route or router-global), reverting on reject, re-entering an accepted write as a delta, and surfacing a last-write-wins <code>onConflict</code>; a derived route cannot be writable. v9 adds fan-in: <code>addSource(feed, { map, key })</code> returns a per-feed handle (<code>load</code>/<code>apply</code>/<code>push</code>/<code>remove</code>) whose rows are normalized and namespaced so many feeds merge into one keyed store without id collisions, <code>removeSource</code> dropping exactly a feed's rows and <code>sources</code> listing them. v10 adds observability: <code>metrics()</code> is a cheap snapshot of per-route/per-source counts and throughput plus the global unrouted/dropped/buffered/lag figures, <code>on('metrics')</code> drives a periodic emit (off unless a listener is registered), and <code>mountDevtools(el)</code> renders a live panel from the module's own DOM file. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
|
|
6273
|
+
<tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. By default routing is first-match-wins (<code>overlap: false</code>); to fan one partition value to several viewers at once (a grid <em>and</em> a KPI panel <em>and</em> a chart off one feed) create the router with <code>overlap: true</code> — with the default, a second viewer on the same value receives nothing and the router emits a one-time dev warning naming the clash. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. v3 also adds per-route reshaping — <code>transform</code>/<code>filter</code>/<code>sort</code> and <code>rollup</code> ({ groupBy, aggregate }) summaries — a relationship graph (<code>relate(edges)</code>: multi-hop, several-into-one AND, and mutual edges) that scales v2's pairwise <code>link</code>, and stream hygiene: a <code>seq</code>/version orders and de-duplicates a feed (stale/duplicate deltas dropped, counted in <code>dropped</code>), <code>push</code> with a <code>batch</code>/<code>coalesce</code> buffers a high-frequency feed (<code>flushStream</code> for a deterministic point), and <code>lastSeq</code>/<code>checkpoint</code>/<code>seenThrough</code> resume precisely after a dropped socket. v4 adds time-travel: <code>buffer({ window, max })</code> records the ordered stream into a bounded ring over a moving base, so <code>scrubTo</code> reconstructs a past point, <code>replay</code> (with <code>pause</code>/<code>resume</code>) walks a range, and <code>live</code> returns to the head — every state pushed by the same keyed diff, <code>traveling</code>/<code>buffered</code> reporting the state. v6 adds cross-tab sync: <code>broadcast({ channel })</code> mirrors the ordered, de-duplicated deltas to other tabs/windows over a BroadcastChannel with no echo loop, a popped-out grid joining the same feed with no second socket and resyncing mid-stream via the reconnect path. v7 adds query-slice routing: <code>query(adapter, request)</code> sources the router from a DFQL/DuckDB (or any pushdown) adapter, partitioning one result across the routes; a route-level <code>where</code> is pushed down where the adapter's capabilities allow and the residual finished client-side, with <code>lastQueryPlan</code> reporting the split. v8 adds write-back: a <code>writable</code> route captures the grid's committed edits off its public edit surface and routes them to <code>onWrite(change, ctx)</code> (per-route or router-global), reverting on reject, re-entering an accepted write as a delta, and surfacing a last-write-wins <code>onConflict</code>; a derived route cannot be writable. v9 adds fan-in: <code>addSource(feed, { map, key })</code> returns a per-feed handle (<code>load</code>/<code>apply</code>/<code>push</code>/<code>remove</code>) whose rows are normalized and namespaced so many feeds merge into one keyed store without id collisions, <code>removeSource</code> dropping exactly a feed's rows and <code>sources</code> listing them. v10 adds observability: <code>metrics()</code> is a cheap snapshot of per-route/per-source counts and throughput plus the global unrouted/dropped/buffered/lag figures, <code>on('metrics')</code> drives a periodic emit (off unless a listener is registered), and <code>mountDevtools(el)</code> renders a live panel from the module's own DOM file. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
|
|
6242
6274
|
<tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane×column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
|
|
6243
6275
|
<tr><td class="name">createKPI</td><td class="desc">A KPI / stat-tile view (module <code>kpi</code>) of a dataset as a panel of aggregate tiles — each tile a <code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>, <code>countDistinct</code> or a <code>custom</code> reducer over the routed rows, with an optional <code>filter</code> predicate, number <code>format</code> (<code>number</code>/<code>currency</code>/<code>percent</code>/<code>compact</code>), a <code>baseline</code> for a delta, semantic threshold bands (<code>thresholds</code> with two cut points and a direction, or an explicit <code>bands</code> list, giving a <code>good</code>/<code>warn</code>/<code>critical</code> status kept separate from any accent), and an optional <code>sparkline</code> series. It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, kpi)</code> and drive a KPI panel beside a grid, a kanban and a chart off one feed. Updates are incremental — a delta adjusts each tile's running accumulator by only the rows it carries (an add contributes, a remove reverses, an update reverses-then-contributes), the two bounded exceptions being a <code>min</code>/<code>max</code> whose current extreme is removed (a rescan of that tile's own value multiset) and a <code>custom</code> reducer (recomputed over the filtered store, an arbitrary function having no inverse). Each tile is a labelled <code><figure></code>, focusable and keyboard-activatable, its value announced, the sparkline respecting <code>prefers-reduced-motion</code>; a <code>tile:click</code> event (also from the keyboard) lets a host drill down or filter a routed grid. Pass <code>null</code> as the element for a headless panel that computes the same tile model without a DOM. Not a dashboard layout engine (that is the parked dashboard generator) and no charting beyond the minimal sparkline (that is the charts module).</td></tr>
|
|
6244
6276
|
<tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
|
|
@@ -6493,6 +6525,59 @@ el.grid.sort.set([{ col: 'charge', dir: 'desc' }]);</code></pre>
|
|
|
6493
6525
|
</tbody>
|
|
6494
6526
|
</table>
|
|
6495
6527
|
</div>
|
|
6528
|
+
<h4>Cancellable before-events (BACKLOG-0000943)</h4>
|
|
6529
|
+
<p>Every user-initiated mutation has a paired cancellable <code>before</code> event. The handler
|
|
6530
|
+
receives a <code>BeforeEvent</code> carrying the action context plus <code>preventDefault(reason?)</code>,
|
|
6531
|
+
<code>defaultPrevented</code> and <code>reason</code>. Calling <code>preventDefault()</code> — or
|
|
6532
|
+
returning <code>false</code>, the legacy kanban <code>onBeforeMove</code> idiom — cancels the action.
|
|
6533
|
+
A handler may be <code>async</code>; the mutation is held until every registered before-handler
|
|
6534
|
+
settles, so a confirm dialog or a server check genuinely gates the write. Any one handler
|
|
6535
|
+
preventing cancels it (veto wins), and a handler that throws is treated as a cancel and surfaced.
|
|
6536
|
+
On a veto the paired <code><action>:cancelled</code> event fires carrying the reason.</p>
|
|
6537
|
+
<p>These fire for <em>user</em> actions only. Host/API writes (for example
|
|
6538
|
+
<code>grid.edit.setCells</code>) and remote/router-applied deltas (<code>rows.apply</code>,
|
|
6539
|
+
origin !== <code>'user'</code>) do not fire them — remote truth is not a user gesture and
|
|
6540
|
+
does not self-veto. The <code>origin</code> field carried on each before-event lets a host
|
|
6541
|
+
deduplicate a module-initiated write (a kanban or Gantt move that re-enters core) from a genuine
|
|
6542
|
+
user gesture. If a handler was <code>async</code> and the underlying state moved during the await
|
|
6543
|
+
(a row removed, a value changed by a live delta), the gate re-validates and cancels with reason
|
|
6544
|
+
<code>'stale'</code> rather than applying against state that has moved. With no before-handler
|
|
6545
|
+
registered every mutation stays synchronous and behaves exactly as before.</p>
|
|
6546
|
+
<div class="table-wrap">
|
|
6547
|
+
<table>
|
|
6548
|
+
<thead><tr><th>Event</th><th>Fires when</th></tr></thead>
|
|
6549
|
+
<tbody>
|
|
6550
|
+
<tr><td class="name">beforeEdit</td><td class="desc">Before a validated cell/row commit applies. Carries row, key, mode, changes, origin. Validation (edit.validate) is separate and runs first. Paired with edit:cancelled.</td></tr>
|
|
6551
|
+
<tr><td class="name">beforeSort</td><td class="desc">Before a sort is set. Paired with sort:cancelled.</td></tr>
|
|
6552
|
+
<tr><td class="name">beforeFilter</td><td class="desc">Before a structured or quick filter is set (kind tells them apart). Paired with filter:cancelled.</td></tr>
|
|
6553
|
+
<tr><td class="name">beforeColumnMove</td><td class="desc">Before a column reorder applies, earlier than the post-mutation column change. Paired with columnMove:cancelled.</td></tr>
|
|
6554
|
+
<tr><td class="name">beforeColumnResize</td><td class="desc">Before a column width change applies. Paired with columnResize:cancelled.</td></tr>
|
|
6555
|
+
<tr><td class="name">beforeColumnHide</td><td class="desc">Before one or more columns are hidden. Paired with columnHide:cancelled.</td></tr>
|
|
6556
|
+
<tr><td class="name">beforeSelect</td><td class="desc">Before a user selection change applies; a veto snaps back to the last announced selection. Paired with selection:cancelled.</td></tr>
|
|
6557
|
+
<tr><td class="name">beforeRowAdd</td><td class="desc">Before an optimistic row append applies. Paired with rowAdd:cancelled.</td></tr>
|
|
6558
|
+
<tr><td class="name">beforeDelete</td><td class="desc">Before an optimistic row delete applies — the canonical confirm-before-delete hook. Paired with delete:cancelled.</td></tr>
|
|
6559
|
+
<tr><td class="name">beforeRowMove</td><td class="desc">Before a row reorder applies. Paired with rowMove:cancelled.</td></tr>
|
|
6560
|
+
<tr><td class="name">beforeGroup</td><td class="desc">Before a group/tree expand or collapse applies. Paired with group:cancelled.</td></tr>
|
|
6561
|
+
<tr><td class="name">edit:cancelled</td><td class="desc">A beforeEdit was vetoed; reason is 'stale' when a live delta moved the cell during an async gate.</td></tr>
|
|
6562
|
+
<tr><td class="name">sort:cancelled</td><td class="desc">A beforeSort was vetoed.</td></tr>
|
|
6563
|
+
<tr><td class="name">filter:cancelled</td><td class="desc">A beforeFilter was vetoed.</td></tr>
|
|
6564
|
+
<tr><td class="name">columnMove:cancelled</td><td class="desc">A beforeColumnMove was vetoed.</td></tr>
|
|
6565
|
+
<tr><td class="name">columnResize:cancelled</td><td class="desc">A beforeColumnResize was vetoed.</td></tr>
|
|
6566
|
+
<tr><td class="name">columnHide:cancelled</td><td class="desc">A beforeColumnHide was vetoed.</td></tr>
|
|
6567
|
+
<tr><td class="name">selection:cancelled</td><td class="desc">A beforeSelect was vetoed; the selection snapped back.</td></tr>
|
|
6568
|
+
<tr><td class="name">rowAdd:cancelled</td><td class="desc">A beforeRowAdd was vetoed.</td></tr>
|
|
6569
|
+
<tr><td class="name">delete:cancelled</td><td class="desc">A beforeDelete was vetoed; reason is 'stale' when the row was already gone.</td></tr>
|
|
6570
|
+
<tr><td class="name">rowMove:cancelled</td><td class="desc">A beforeRowMove was vetoed; reason is 'stale' when the row had moved.</td></tr>
|
|
6571
|
+
<tr><td class="name">group:cancelled</td><td class="desc">A beforeGroup was vetoed.</td></tr>
|
|
6572
|
+
<tr><td class="name">export:request</td><td class="desc">A remote export was requested. Past-tense notification.</td></tr>
|
|
6573
|
+
<tr><td class="name">export:done</td><td class="desc">A remote export completed. Past-tense notification.</td></tr>
|
|
6574
|
+
<tr><td class="name">shortcuts:opened</td><td class="desc">The keyboard-shortcuts help overlay opened. Past-tense notification.</td></tr>
|
|
6575
|
+
<tr><td class="name">shortcuts:closed</td><td class="desc">The keyboard-shortcuts help overlay closed. Past-tense notification.</td></tr>
|
|
6576
|
+
<tr><td class="name">print:before</td><td class="desc">Print mode is about to snapshot. Past-tense notification, not cancellable (BACKLOG-0000941).</td></tr>
|
|
6577
|
+
<tr><td class="name">print:after</td><td class="desc">Print mode restored the grid, even if the browser cancelled the print (BACKLOG-0000941).</td></tr>
|
|
6578
|
+
</tbody>
|
|
6579
|
+
</table>
|
|
6580
|
+
</div>
|
|
6496
6581
|
<h4>Presentation and formatting</h4>
|
|
6497
6582
|
<div class="table-wrap">
|
|
6498
6583
|
<table>
|
package/lattice-grid.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Lattice Grid 1.
|
|
2
|
+
* Lattice Grid 1.36.0, type declarations
|
|
3
3
|
* Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
|
|
4
4
|
* https://latticegrid.dev
|
|
5
5
|
*/
|
|
@@ -3442,7 +3442,21 @@ export type EventName =
|
|
|
3442
3442
|
/* Annotations */
|
|
3443
3443
|
| 'annotation:changed'
|
|
3444
3444
|
/* Export */
|
|
3445
|
-
| 'export:progress'
|
|
3445
|
+
| 'export:progress' | 'export:request' | 'export:done'
|
|
3446
|
+
/* Keyboard help overlay (past-tense notifications) */
|
|
3447
|
+
| 'shortcuts:opened' | 'shortcuts:closed'
|
|
3448
|
+
/* Print (past-tense notifications, BACKLOG-0000941) */
|
|
3449
|
+
| 'print:before' | 'print:after'
|
|
3450
|
+
/* Cancellable before-events (BACKLOG-0000943). Delivered through the async
|
|
3451
|
+
* before-dispatch path with a {@link BeforeEvent} carrying preventDefault. */
|
|
3452
|
+
| 'beforeEdit' | 'beforeSort' | 'beforeFilter'
|
|
3453
|
+
| 'beforeColumnMove' | 'beforeColumnResize' | 'beforeColumnHide'
|
|
3454
|
+
| 'beforeSelect' | 'beforeRowAdd' | 'beforeDelete' | 'beforeRowMove' | 'beforeGroup'
|
|
3455
|
+
/* Their cancellation notifications (past-tense, non-cancellable). */
|
|
3456
|
+
| 'edit:cancelled' | 'sort:cancelled' | 'filter:cancelled'
|
|
3457
|
+
| 'columnMove:cancelled' | 'columnResize:cancelled' | 'columnHide:cancelled'
|
|
3458
|
+
| 'selection:cancelled' | 'rowAdd:cancelled' | 'delete:cancelled'
|
|
3459
|
+
| 'rowMove:cancelled' | 'group:cancelled'
|
|
3446
3460
|
/* Every event at once, for logging and debugging. */
|
|
3447
3461
|
| '*';
|
|
3448
3462
|
|
|
@@ -3453,6 +3467,31 @@ export interface GridEvent {
|
|
|
3453
3467
|
[key: string]: unknown;
|
|
3454
3468
|
}
|
|
3455
3469
|
|
|
3470
|
+
/**
|
|
3471
|
+
* A cancellable *before*-event (BACKLOG-0000943), delivered to `on('beforeX')`
|
|
3472
|
+
* handlers before a user-initiated mutation is applied.
|
|
3473
|
+
*
|
|
3474
|
+
* A handler cancels the pending action by calling `preventDefault(reason?)`; the
|
|
3475
|
+
* mutation is then abandoned and a past-tense `<action>:cancelled` event carries
|
|
3476
|
+
* the reason. A handler may be `async` (or return a Promise): the grid awaits
|
|
3477
|
+
* every before-handler before deciding, so a confirm dialog or a server check
|
|
3478
|
+
* can gate the write. Any one handler preventing cancels the action.
|
|
3479
|
+
*
|
|
3480
|
+
* The action-specific fields (the edited cells, the target index, the affected
|
|
3481
|
+
* rows) are spread alongside these, so a handler decides without reaching into
|
|
3482
|
+
* grid internals. `origin` distinguishes a genuine user gesture from a
|
|
3483
|
+
* host/module-driven or remote write, which is how a module whose move re-enters
|
|
3484
|
+
* core is deduplicated by the host.
|
|
3485
|
+
*/
|
|
3486
|
+
export interface BeforeEvent extends GridEvent {
|
|
3487
|
+
/** Cancel the pending action; the optional reason is surfaced on the cancellation event. */
|
|
3488
|
+
preventDefault(reason?: string): void;
|
|
3489
|
+
/** True once any handler has called `preventDefault` or returned false. */
|
|
3490
|
+
defaultPrevented: boolean;
|
|
3491
|
+
/** The reason given to `preventDefault`, or null; `'stale'` when re-validation failed. */
|
|
3492
|
+
reason: string | null;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3456
3495
|
export type EventHandler = (e: GridEvent) => void;
|
|
3457
3496
|
export type Unsubscribe = () => void;
|
|
3458
3497
|
|
|
@@ -4997,6 +5036,53 @@ export function createPushdownSource(
|
|
|
4997
5036
|
}>;
|
|
4998
5037
|
};
|
|
4999
5038
|
|
|
5039
|
+
/**
|
|
5040
|
+
* Load a JSON or NDJSON file from a URL (BACKLOG-0000944).
|
|
5041
|
+
*
|
|
5042
|
+
* Returns a `StreamSourceConfig` for `createGrid(el, { source: createUrlSource(url, opts) })`.
|
|
5043
|
+
* A JSON file (a top-level array, or a nested array selected by `rowsPath`/`map`)
|
|
5044
|
+
* is read whole and handed over as rows; an NDJSON/JSONL file (one JSON value per
|
|
5045
|
+
* line) is streamed in incrementally in batches. Format is resolved from an
|
|
5046
|
+
* explicit `format`, else the URL extension, else the `Content-Type`, else a
|
|
5047
|
+
* sniff of the first bytes or a clear error. Errors — a non-2xx status, a network
|
|
5048
|
+
* failure, a bad body, a malformed line — surface as `source:error`, never as an
|
|
5049
|
+
* uncaught throw. Zero new dependencies: `fetch`, `response.body.getReader()` and
|
|
5050
|
+
* `TextDecoder`.
|
|
5051
|
+
*
|
|
5052
|
+
* @param url the file URL
|
|
5053
|
+
* @param opts loading options
|
|
5054
|
+
* @returns a stream source config
|
|
5055
|
+
*/
|
|
5056
|
+
export function createUrlSource(
|
|
5057
|
+
url: string,
|
|
5058
|
+
opts?: {
|
|
5059
|
+
/** Explicit format; wins over inference. */
|
|
5060
|
+
format?: 'json' | 'ndjson';
|
|
5061
|
+
/** Dot path to the array inside a wrapped JSON body (JSON only). */
|
|
5062
|
+
rowsPath?: string;
|
|
5063
|
+
/** Extract the array from the parsed JSON (JSON only); runs after `rowsPath`. */
|
|
5064
|
+
map?: (parsed: unknown) => unknown[];
|
|
5065
|
+
/** Transport override for auth, headers or a proxy; default `globalThis.fetch`. */
|
|
5066
|
+
fetch?: typeof fetch;
|
|
5067
|
+
/** Headers merged into the request. */
|
|
5068
|
+
headers?: Record<string, string>;
|
|
5069
|
+
/** Re-fetch on this interval in milliseconds; each pass replaces the rows. */
|
|
5070
|
+
poll?: number;
|
|
5071
|
+
/** NDJSON rows per emitted chunk, to avoid render thrash; default 500. */
|
|
5072
|
+
batchSize?: number;
|
|
5073
|
+
/** NDJSON: skip a malformed line with a warning rather than failing the stream. */
|
|
5074
|
+
lenient?: boolean;
|
|
5075
|
+
/** Start fetching on construction; default true. */
|
|
5076
|
+
autoStart?: boolean;
|
|
5077
|
+
/** Sliding-window bound passed through to the stream. */
|
|
5078
|
+
maxRows?: number;
|
|
5079
|
+
/** Render-coalescing window in ms, passed through to the stream. */
|
|
5080
|
+
coalesceMs?: number;
|
|
5081
|
+
/** Promote to a memory source below this row count, passed through to the stream. */
|
|
5082
|
+
promoteToMemoryBelow?: number;
|
|
5083
|
+
},
|
|
5084
|
+
): StreamSourceConfig;
|
|
5085
|
+
|
|
5000
5086
|
/**
|
|
5001
5087
|
* The pushdown map (BACKLOG-0000730 Part B): one published record per statistic
|
|
5002
5088
|
* giving whether the engine can express it, the DuckDB aggregate SQL it emits,
|