tosijs-ui 1.9.7 → 1.9.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.8
4
+
5
+ **Grouped tables can now say how much of a group is showing.** The piece missing from 1.9.7's
6
+ row grouping: a cell that reports on its group ("showing 2 of 7") or offers a **show-all**
7
+ toggle needs to compare what is rendered against what exists — and that is the one comparison
8
+ a cell renderer cannot make for itself. The filter has already run by then, so a consumer
9
+ sees the survivors with nothing to measure them against. The table sees both sides.
10
+
11
+ ```js
12
+ const id = table.groupIdFor(row)
13
+ const { visible, total } = table.rowGroupCounts.get(id) ?? { visible: 0, total: 0 }
14
+ cell.textContent = visible < total ? `showing ${visible} of ${total}` : `${total} lines`
15
+
16
+ // the toggle is just a set of ids handed back to the table
17
+ expanded.has(id) ? expanded.delete(id) : expanded.add(id)
18
+ table.visibleGroupedRowIds = [...expanded]
19
+ ```
20
+
21
+ - **`table.rowGroupCounts`** — a `Map` from group id to `{ visible, total }`, rendered rows
22
+ against rows before filtering. Recomputed each render and readable while cells render.
23
+ - **`table.groupIdFor(row)`** — a row's group id, or `null` when ungrouped. This matters when
24
+ the grouping was **inferred** from `nonRepeatingGroupedRowCells`: you never wrote that
25
+ function, so its ids are otherwise unreproducible and the map would be keyed by strings you
26
+ could not construct.
27
+
28
+ Groups the filter removed **entirely** stay in the map with `visible: 0`, so "nothing here
29
+ matched" stays distinguishable from "no such group" — the first being exactly when a show-all
30
+ toggle is worth offering. `rowGroupCounts` is always a `Map` (empty when ungrouped), so
31
+ `.get()` needs no null check. Pinned rows sit outside grouping and are not counted.
32
+
33
+ Additive only; ungrouped tables are unaffected.
34
+
3
35
  ## 1.9.7
4
36
 
5
37
  **`<tosi-table>` can group rows.** The motivating shape is invoice lines: rows that belong
@@ -1,6 +1,6 @@
1
1
  import { Component as WebComponent, ElementCreator } from 'tosijs';
2
2
  import { SortCallback } from './make-sorter.js';
3
- import { RowGroupIdFn } from './row-grouping.js';
3
+ import { RowGroupIdFn, GroupCount } from './row-grouping.js';
4
4
  import { ValueRendererType } from './value-renderer.js';
5
5
  export interface ColumnOptions {
6
6
  name?: string;
@@ -201,7 +201,28 @@ export declare class TosiTable extends WebComponent {
201
201
  set nonRepeatingGroupedRowCells(props: string[] | null);
202
202
  /** The grouping function in force, or null when the table is ungrouped. */
203
203
  private get groupIdFn();
204
+ /**
205
+ * This row's group id, or null when the table is ungrouped.
206
+ *
207
+ * Public because the grouping may be INFERRED from `nonRepeatingGroupedRowCells`, in which
208
+ * case the consumer never wrote the function and cannot reproduce its ids — which would
209
+ * leave `rowGroupCounts` keyed by strings they have no way to construct.
210
+ */
211
+ groupIdFor(row: any): string | null;
212
+ /**
213
+ * Per-group `{ visible, total }` counts — rendered rows against rows before filtering.
214
+ *
215
+ * For cells that report or control their own group: "showing 2 of 7", or a toggle that
216
+ * adds the group to `visibleGroupedRowIds`. The table is the only thing that sees both
217
+ * sides of the filter, so this is the piece a custom cell renderer cannot derive itself.
218
+ *
219
+ * Recomputed each render, and always a Map — empty when ungrouped — so callers can `.get()`
220
+ * without a null check. Groups filtered away entirely are present with `visible: 0`;
221
+ * pinned rows sit outside grouping and are not counted.
222
+ */
223
+ get rowGroupCounts(): Map<string, GroupCount>;
204
224
  private _grouping;
225
+ private _rowGroupCounts;
205
226
  get sort(): SortCallback | undefined;
206
227
  set sort(sortFunc: SortCallback | undefined);
207
228
  get columns(): ColumnOptions[];
@@ -587,6 +587,36 @@ For the same decision in JavaScript — inside a `dataCell` binding or a `rowRen
587
587
  callback — **`table.isFirstInGroup(row)`** answers it directly. It is always `true` when the
588
588
  table is ungrouped.
589
589
 
590
+ ### `rowGroupCounts` — how much of each group is showing
591
+
592
+ A cell that reports on its group ("showing 2 of 7") or offers a **show-all** toggle needs to
593
+ know how many rows the group has, not just how many are on screen. That comparison is the
594
+ one thing a cell renderer cannot make for itself: the filter has already run, so a consumer
595
+ sees the survivors and has nothing to measure them against.
596
+
597
+ **`table.rowGroupCounts`** is a `Map` from group id to `{ visible, total }` — rendered rows
598
+ against rows before filtering — recomputed each render and available while cells render.
599
+ **`table.groupIdFor(row)`** gives a row's group id, which matters when the grouping was
600
+ *inferred* from `nonRepeatingGroupedRowCells`: you never wrote that function, so its ids are
601
+ otherwise unreproducible.
602
+
603
+ ```typescript
604
+ // inside a dataCell binding, or rowRendered
605
+ const id = table.groupIdFor(row)
606
+ const { visible, total } = table.rowGroupCounts.get(id) ?? { visible: 0, total: 0 }
607
+ cell.textContent = visible < total ? `showing ${visible} of ${total}` : `${total} lines`
608
+
609
+ // …and the toggle is just a set of ids handed back to the table
610
+ expanded.has(id) ? expanded.delete(id) : expanded.add(id)
611
+ table.visibleGroupedRowIds = [...expanded]
612
+ ```
613
+
614
+ Groups the filter removed **entirely** are still in the map, with `visible: 0` — otherwise
615
+ "nothing in this group matched" would be indistinguishable from "no such group", and the
616
+ first is precisely when a show-all toggle is worth offering. The map is always a `Map`
617
+ (empty when ungrouped), so `.get()` needs no null check. Pinned rows sit outside grouping
618
+ and are not counted.
619
+
590
620
  ### Example
591
621
 
592
622
  Grouping the emoji table by category and subcategory. It is grouped by *inference* — only
@@ -679,6 +709,26 @@ test('stripes are per group and survive virtualisation', () => {
679
709
  }
680
710
  })
681
711
 
712
+ test('rowGroupCounts is keyed by groupIdFor, and totals the whole dataset', () => {
713
+ // The integration that matters: the grouping here is INFERRED, so `groupIdFor` is the
714
+ // only way to produce a key, and a mismatch between the two would make the map useless
715
+ // while both halves still looked fine on their own.
716
+ const counts = table.rowGroupCounts
717
+ expect(counts.size).toBe(groupIndex.size)
718
+ for (const row of rows) {
719
+ expect(counts.has(table.groupIdFor(row))).toBe(true)
720
+ }
721
+ // No filter is set on this example, so every group is fully visible…
722
+ let summed = 0
723
+ for (const { visible, total } of counts.values()) {
724
+ expect(visible).toBe(total)
725
+ expect(total).toBeGreaterThan(0)
726
+ summed += total
727
+ }
728
+ // …and the totals account for every row, rather than just the stamped ones.
729
+ expect(summed).toBe(rows.length)
730
+ })
731
+
682
732
  test('category and subcategory are shown once per group, and nothing else is hidden', () => {
683
733
  const visible = (cell) => getComputedStyle(cell).visibility !== 'hidden'
684
734
  let shown = 0
@@ -763,7 +813,7 @@ As well as any column names you want localized.
763
813
  import { Component as WebComponent, elements, vars, varDefault, tosiValue, getListItem, getListBinding, tosi, } from 'tosijs';
764
814
  import { trackDrag } from './track-drag.js';
765
815
  import { naturalSorter } from './natural-compare.js';
766
- import { resolveRowGroupId, withForcedGroups, clusterByGroup, groupRenderMeta, } from './row-grouping.js';
816
+ import { resolveRowGroupId, withForcedGroups, clusterByGroup, groupRenderMeta, groupCounts, } from './row-grouping.js';
767
817
  import { icons } from './icons.js';
768
818
  import { valueRenderer, } from './value-renderer.js';
769
819
  import { popMenu } from './menu.js';
@@ -1136,6 +1186,31 @@ export class TosiTable extends WebComponent {
1136
1186
  get groupIdFn() {
1137
1187
  return resolveRowGroupId(this._rowGroupId, this._nonRepeatingGroupedRowCells);
1138
1188
  }
1189
+ /**
1190
+ * This row's group id, or null when the table is ungrouped.
1191
+ *
1192
+ * Public because the grouping may be INFERRED from `nonRepeatingGroupedRowCells`, in which
1193
+ * case the consumer never wrote the function and cannot reproduce its ids — which would
1194
+ * leave `rowGroupCounts` keyed by strings they have no way to construct.
1195
+ */
1196
+ groupIdFor(row) {
1197
+ const groupId = this.groupIdFn;
1198
+ return groupId ? groupId(row) : null;
1199
+ }
1200
+ /**
1201
+ * Per-group `{ visible, total }` counts — rendered rows against rows before filtering.
1202
+ *
1203
+ * For cells that report or control their own group: "showing 2 of 7", or a toggle that
1204
+ * adds the group to `visibleGroupedRowIds`. The table is the only thing that sees both
1205
+ * sides of the filter, so this is the piece a custom cell renderer cannot derive itself.
1206
+ *
1207
+ * Recomputed each render, and always a Map — empty when ungrouped — so callers can `.get()`
1208
+ * without a null check. Groups filtered away entirely are present with `visible: 0`;
1209
+ * pinned rows sit outside grouping and are not counted.
1210
+ */
1211
+ get rowGroupCounts() {
1212
+ return this._rowGroupCounts;
1213
+ }
1139
1214
  /*
1140
1215
  Grouping facts for the CURRENT visible rows, recomputed each render.
1141
1216
 
@@ -1144,6 +1219,7 @@ export class TosiTable extends WebComponent {
1144
1219
  ungrouped, which is also what makes every grouping code path below a single null check.
1145
1220
  */
1146
1221
  _grouping = null;
1222
+ _rowGroupCounts = new Map();
1147
1223
  get sort() {
1148
1224
  if (this._sort) {
1149
1225
  return this._sort;
@@ -2080,9 +2156,13 @@ export class TosiTable extends WebComponent {
2080
2156
  if (groupId) {
2081
2157
  visibleData = clusterByGroup(visibleData, groupId);
2082
2158
  this._grouping = groupRenderMeta(visibleData, groupId);
2159
+ // Counted from `scope` (pre-filter) against the final rendered set, and assigned
2160
+ // BEFORE the rows below are stamped, so a cell renderer can read it as it renders.
2161
+ this._rowGroupCounts = groupCounts(scope, visibleData, groupId);
2083
2162
  }
2084
2163
  else {
2085
2164
  this._grouping = null;
2165
+ this._rowGroupCounts = new Map();
2086
2166
  }
2087
2167
  this.rowData.pinnedTopData = pinnedTopData;
2088
2168
  this.rowData.pinnedBottomData = pinnedBottomData;