73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
import { sanitizeCsvValue, type CsvValue } from "./csv";
|
|
|
|
export const GOOGLE_SHEETS_NEW_URL = "https://sheets.new";
|
|
|
|
export async function copyTableToClipboard(
|
|
headers: string[],
|
|
rows: CsvValue[][],
|
|
): Promise<void> {
|
|
if (typeof navigator === "undefined" || !navigator.clipboard?.write) {
|
|
throw new Error("Clipboard API not available in this browser.");
|
|
}
|
|
|
|
const safeRows = rows.map((row) =>
|
|
row.map((value) => sanitizeCsvValue(value ?? "")),
|
|
);
|
|
|
|
const tsv = buildTsv(headers, safeRows);
|
|
const html = buildHtmlTable(headers, safeRows);
|
|
|
|
// Wrap blobs in Promise.resolve for Safari <17.4 compatibility — older
|
|
// Safari requires Promise<Blob> values; modern browsers accept either.
|
|
await navigator.clipboard.write([
|
|
new ClipboardItem({
|
|
"text/plain": Promise.resolve(new Blob([tsv], { type: "text/plain" })),
|
|
"text/html": Promise.resolve(new Blob([html], { type: "text/html" })),
|
|
}),
|
|
]);
|
|
}
|
|
|
|
function buildTsv(
|
|
headers: string[],
|
|
rows: (string | number | boolean)[][],
|
|
): string {
|
|
const lines = [headers.map(tsvCell).join("\t")];
|
|
for (const row of rows) {
|
|
lines.push(row.map(tsvCell).join("\t"));
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function tsvCell(value: string | number | boolean): string {
|
|
if (typeof value !== "string") return String(value);
|
|
return value.replace(/[\t\r\n]+/g, " ");
|
|
}
|
|
|
|
function buildHtmlTable(
|
|
headers: string[],
|
|
rows: (string | number | boolean)[][],
|
|
): string {
|
|
const thead = `<thead><tr>${headers.map((h) => `<th>${escapeHtml(h)}</th>`).join("")}</tr></thead>`;
|
|
const tbody = `<tbody>${rows
|
|
.map(
|
|
(row) =>
|
|
`<tr>${row.map((cell) => `<td>${escapeHtmlCell(cell)}</td>`).join("")}</tr>`,
|
|
)
|
|
.join("")}</tbody>`;
|
|
return `<table>${thead}${tbody}</table>`;
|
|
}
|
|
|
|
function escapeHtmlCell(value: string | number | boolean): string {
|
|
if (typeof value === "number" || typeof value === "boolean")
|
|
return String(value);
|
|
return escapeHtml(value);
|
|
}
|
|
|
|
function escapeHtml(value: string): string {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|