@@ -21,6 +21,7 @@ import {
2121 filterRows ,
2222 prepareSheet ,
2323 sortRows ,
24+ summarizeRange ,
2425 toTsv ,
2526} from "../lib/csv-grid.js" ;
2627import type { CsvColumn , CsvSheet , CsvSort } from "../lib/csv-grid.js" ;
@@ -49,6 +50,8 @@ export type CsvTableLabels = {
4950 readonly blank : string ;
5051 readonly untitledColumn : ( position : number ) => string ;
5152 readonly selectAll : string ;
53+ readonly nameBox : string ;
54+ readonly cellContents : string ;
5255 readonly count : ( rows : number , columns : number ) => string ;
5356 readonly filteredCount : (
5457 matching : number ,
@@ -59,6 +62,9 @@ export type CsvTableLabels = {
5962 readonly empty : string ;
6063 readonly noMatches : string ;
6164 readonly selected : ( rows : number , columns : number ) => string ;
65+ readonly average : ( value : number ) => string ;
66+ readonly cellCount : ( cells : number ) => string ;
67+ readonly sum : ( value : number ) => string ;
6268 readonly copied : string ;
6369 readonly copyFailed : string ;
6470} ;
@@ -79,6 +85,8 @@ const LABELS: CsvTableLabels = {
7985 blank : "(blank)" ,
8086 untitledColumn : ( position ) => `Column ${ position . toLocaleString ( ) } ` ,
8187 selectAll : "Select all" ,
88+ nameBox : "Selected cell" ,
89+ cellContents : "Cell contents" ,
8290 count : ( rows , columns ) =>
8391 `${ counted ( rows , "row" , "rows" ) } · ${ counted ( columns , "column" , "columns" ) } ` ,
8492 filteredCount : ( matching , total , columns ) =>
@@ -88,6 +96,9 @@ const LABELS: CsvTableLabels = {
8896 noMatches : "No rows match." ,
8997 selected : ( rows , columns ) =>
9098 `${ counted ( rows , "row" , "rows" ) } × ${ counted ( columns , "column" , "columns" ) } selected` ,
99+ average : ( value ) => `Average: ${ value . toLocaleString ( ) } ` ,
100+ cellCount : ( cells ) => `Count: ${ cells . toLocaleString ( ) } ` ,
101+ sum : ( value ) => `Sum: ${ value . toLocaleString ( ) } ` ,
91102 copied : "Copied" ,
92103 copyFailed : "Couldn't copy. Your browser blocked the clipboard." ,
93104} ;
@@ -305,6 +316,8 @@ type Range = {
305316 readonly view : readonly number [ ] ;
306317 readonly anchor : Cell ;
307318 readonly focus : Cell ;
319+ /** Picked by row numbers or column letters, and addressed as such: `3:5`, `B:D`. */
320+ readonly whole ?: "rows" | "columns" ;
308321} ;
309322
310323const clamp = ( value : number , min : number , max : number ) =>
@@ -339,7 +352,8 @@ const keepTextUnselected = (event: MouseEvent) => {
339352 *
340353 * Rows are numbered as a spreadsheet numbers them: the header is row 1 and a
341354 * sort renumbers, while a filter hides rows and leaves gaps, so a number is
342- * the row's place in the sorted, unfiltered sheet.
355+ * the row's place in the sorted, unfiltered sheet and an address names the
356+ * same rectangle the person sees.
343357 */
344358function CsvGrid ( {
345359 parsed,
@@ -430,6 +444,14 @@ function CsvGrid({
430444 [ sheet , view ] ,
431445 ) ;
432446
447+ const summary = useMemo (
448+ ( ) =>
449+ rowFrom === - 1
450+ ? null
451+ : summarizeRange ( sheet , view , [ rowFrom , rowTo ] , [ columnFrom , columnTo ] ) ,
452+ [ sheet , view , rowFrom , rowTo , columnFrom , columnTo ] ,
453+ ) ;
454+
433455 const reportRef = useRef ( onSelectionChange ) ;
434456 reportRef . current = onSelectionChange ;
435457 const reported = useRef < Range | null > ( null ) ;
@@ -532,6 +554,7 @@ function CsvGrid({
532554 column : 0 ,
533555 } ,
534556 focus : { row, column : lastColumn } ,
557+ whole : "rows" ,
535558 } ) ) ;
536559 } ,
537560 [ view , lastColumn ] ,
@@ -548,6 +571,7 @@ function CsvGrid({
548571 extend && current ?. view === view ? current . anchor . column : column ,
549572 } ,
550573 focus : { row : view . length - 1 , column } ,
574+ whole : "columns" ,
551575 } ) ) ;
552576 }
553577
@@ -664,6 +688,26 @@ function CsvGrid({
664688
665689 const rowLabel = ( position : number ) =>
666690 position === - 1 ? 1 : ( rank [ view [ position ] ?? 0 ] ?? 0 ) + 2 ;
691+ const address = ( cell : Cell ) =>
692+ `${ columnLetter ( cell . column ) } ${ rowLabel ( cell . row ) } ` ;
693+ const nameBox =
694+ selection === null
695+ ? at . column >= 0
696+ ? address ( at )
697+ : ""
698+ : selection . whole === "rows"
699+ ? `${ rowLabel ( rowFrom ) } :${ rowLabel ( rowTo ) } `
700+ : selection . whole === "columns"
701+ ? `${ columnLetter ( columnFrom ) } :${ columnLetter ( columnTo ) } `
702+ : rowFrom === rowTo && columnFrom === columnTo
703+ ? address ( { row : rowFrom , column : columnFrom } )
704+ : `${ address ( { row : rowFrom , column : columnFrom } ) } :${ address ( { row : rowTo , column : columnTo } ) } ` ;
705+ const contents =
706+ at . column < 0
707+ ? ""
708+ : at . row === - 1
709+ ? ( sheet . headers [ at . column ] ?? "" )
710+ : ( sheet . rows [ view [ at . row ] ?? - 1 ] ?. [ at . column ] ?? "" ) ;
667711
668712 const notes = [
669713 view . length === sheet . rows . length
@@ -678,14 +722,36 @@ function CsvGrid({
678722 ? [ wording . columnsOf ( columnCount , parsed . headers . length ) ]
679723 : [ ] ) ,
680724 ] ;
725+ const totals =
726+ summary === null
727+ ? [ ]
728+ : [
729+ ...( summary . numbers > 0
730+ ? [ wording . average ( summary . sum / summary . numbers ) ]
731+ : [ ] ) ,
732+ wording . cellCount ( summary . count ) ,
733+ ...( summary . numbers > 0 ? [ wording . sum ( summary . sum ) ] : [ ] ) ,
734+ ] ;
681735
682736 return (
683737 < div className = "flex h-full min-h-0 min-w-0 flex-col gap-2" >
684738 < div className = "flex min-h-0 min-w-0 flex-1 flex-col rounded-md border border-border bg-background text-xs" >
685- < div
686- ref = { scroller }
687- className = "min-h-0 flex-[1_1_32rem] overflow-auto rounded-t-md"
688- >
739+ < div className = "flex items-stretch overflow-hidden rounded-t-md border-b border-border" >
740+ < input
741+ readOnly
742+ value = { nameBox }
743+ aria-label = { wording . nameBox }
744+ className = "h-8 w-24 shrink-0 border-r border-border bg-background px-2 tabular-nums focus-visible:-outline-offset-2"
745+ />
746+ < textarea
747+ readOnly
748+ rows = { 1 }
749+ value = { contents }
750+ aria-label = { wording . cellContents }
751+ className = "field-sizing-content max-h-20 min-h-8 min-w-0 flex-1 resize-none bg-background px-2 py-2 leading-4 break-words focus-visible:-outline-offset-2"
752+ />
753+ </ div >
754+ < div ref = { scroller } className = "min-h-0 flex-[1_1_32rem] overflow-auto" >
689755 < div className = "relative isolate w-max min-w-full" >
690756 < table
691757 role = "grid"
@@ -819,9 +885,19 @@ function CsvGrid({
819885 < p role = "status" className = { cn ( copyFailed && "text-destructive" ) } >
820886 { copyFailed ? wording . copyFailed : copied ? wording . copied : "" }
821887 </ p >
822- { selection === null ? null : (
823- < p className = "ms-auto tabular-nums" >
824- { wording . selected ( rowTo - rowFrom + 1 , columnTo - columnFrom + 1 ) }
888+ { summary === null ? null : (
889+ < p className = "ms-auto flex flex-wrap gap-x-4 tabular-nums" >
890+ < span className = "sr-only" >
891+ { wording . selected (
892+ rowTo - rowFrom + 1 ,
893+ columnTo - columnFrom + 1 ,
894+ ) }
895+ </ span >
896+ { totals . map ( ( total ) => (
897+ < span key = { total } data-total >
898+ { total }
899+ </ span >
900+ ) ) }
825901 </ p >
826902 ) }
827903 </ div >
0 commit comments