csvExport.js
1 // RFC 4180 CSV field escape: wrap in quotes when the value contains a 2 // quote, comma, or newline; escape inner quotes by doubling them. 3 function escapeCell(v) { 4 if (v == null) return ""; 5 const s = typeof v === "string" ? v : (typeof v === "object" ? JSON.stringify(v) : String(v)); 6 if (/[",\n\r]/.test(s)) { 7 return `"${s.replace(/"/g, '""')}"`; 8 } 9 return s; 10 } 11 12 /** 13 * Build a CSV string from an array of row objects. 14 * 15 * @param {object[]} rows — data rows 16 * @param {Array<{key: string, header?: string, get?: (row) => any}>} columns 17 * `get` takes precedence over `row[key]`; `header` falls back to `key`. 18 * @returns {string} CSV with CRLF line endings (Excel-friendly). 19 */ 20 export function toCsv(rows, columns) { 21 const headerLine = columns.map((c) => escapeCell(c.header || c.key)).join(","); 22 const dataLines = rows.map((r) => 23 columns.map((c) => escapeCell(c.get ? c.get(r) : r[c.key])).join(",") 24 ); 25 // Excel on Windows reads CRLF more reliably than LF, and Excel-on-Mac 26 // handles either — CRLF is the safe default. 27 return [headerLine, ...dataLines].join("\r\n"); 28 } 29 30 /** 31 * Trigger a browser download of the given CSV content. `filename` gets a 32 * `.csv` suffix if not already present. 33 */ 34 export function downloadCsv(filename, csv) { 35 const name = filename.endsWith(".csv") ? filename : `${filename}.csv`; 36 // `` BOM so Excel on Windows auto-detects UTF-8. Harmless for 37 // other tooling — pandas / Numbers / Sheets strip the BOM on read. 38 const blob = new Blob(["", csv], { type: "text/csv;charset=utf-8;" }); 39 const href = URL.createObjectURL(blob); 40 const a = document.createElement("a"); 41 a.href = href; 42 a.download = name; 43 document.body.appendChild(a); 44 a.click(); 45 document.body.removeChild(a); 46 // Give the browser a tick before revoking so the download actually fires. 47 setTimeout(() => URL.revokeObjectURL(href), 0); 48 }