CSV & Clipboard Export

TableCrafter lets visitors pull a live table straight into their spreadsheet: download a UTF-8 CSV file or copy the rows to the clipboard as tab-separated values. Both paths respect active filters and hidden columns, so people export exactly what they see.

CSV Download Copy to Clipboard UTF-8 BOM Filter-aware RFC 4180

Overview

Export is off by default. You turn it on per table with the export shortcode attribute, which renders an export toolbar above the grid. The toolbar always contains a Copy to Clipboard button, plus either a single Export CSV button or a multi-format dropdown depending on configuration.

Two engines power the output:

Clipboard copy is always 100% client-side and never touches the server.

ℹ️

This page reflects TableCrafter (wp-data-tables) v3.5.6. The export handler lives in includes/class-tc-export-handler.php; the front-end logic is in assets/js/tablecrafter.js.

Enabling export

Add the export attribute to your shortcode. It accepts the usual truthy strings (true, 1, yes); anything else is treated as false.

// Minimal: turn on the export toolbar
[tablecrafter source="https://example.com/data.csv" export="true"]

// Export alongside search and filters
[tablecrafter source="https://example.com/inventory.json" search="true" filters="true" export="true"]

The shortcode writes data-export="true" onto the container, and the front-end reads it into config.exportable at init. When exportable is true, TableCrafter calls renderExportControls() and inserts the .tc-export-controls toolbar before the table.

Shortcode attributes

AttributeDefaultDescription
exportfalse OptionalShows the export toolbar (CSV/format buttons + Copy to Clipboard). Accepts true/1/yes.
source"" RequiredURL of the CSV, JSON, or Google Sheet feeding the table. Without it nothing renders to export.
include"" OptionalComma-separated columns to keep. Narrows what is displayed and therefore exported.
exclude"" OptionalComma-separated columns to drop from the table and exports.
filterstrue OptionalEnables column filters. Active filters carry into the export (see Filter-aware exports).
searchfalse OptionalEnables global search; matching rows define what gets exported.
⚠️

There is no export_formats or export_filename shortcode attribute in v3.5.6. The format list (csv, excel, pdf) and the base filename (table-export) come from JavaScript config defaults (config.advancedExport.formats and config.exportFilename), not from the shortcode. Override them via the JS API if you initialise tables programmatically.

The export toolbar

renderExportControls() builds the toolbar like this:

Format options in the dropdown route through handleExportFormat(): csv calls the client-side downloadCSV(), while excel and pdf call downloadEnhanced() (server-side). The fallback "Export CSV" button calls downloadEnhanced('csv'), which produces the BOM-prefixed server CSV.

Toolbar CSS classes

ClassElement
.tc-export-controlsToolbar wrapper around all export buttons.
.tc-export-csvSingle "Export CSV" button (green).
.tc-export-dropdown-wrapperWrapper for the multi-format dropdown.
.tc-export-main-btn"Export Data" trigger button for the dropdown.
.tc-export-dropdownThe dropdown panel (hidden until opened).
.tc-export-optionOne format row inside the dropdown (e.g. .tc-export-csv, .tc-export-pdf).
.tc-copy-clipboard"Copy to Clipboard" button.
.tc-copy-successJS-toggled state class added for ~2s after a successful copy.

How CSV download works

The browser CSV path is straightforward. exportToCSV() joins the visible column labels into a header row, escapes each cell with escapeCSVField(), and joins rows with \n. downloadCSV() then wraps that string in a Blob and triggers a download via a temporary anchor:

// Simplified from downloadCSV()
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
link.download = this.config.exportFilename + '.csv';  // e.g. table-export.csv
link.click();
URL.revokeObjectURL(url);

escapeCSVField() follows RFC 4180: any value containing a comma, newline, or double-quote is wrapped in quotes with internal quotes doubled ("""). Pure numeric values are left unquoted; other text is quoted.

The server-side CSV (used by downloadEnhanced('csv') and TC_Export_Handler::export_csv()) goes further: it prepends a UTF-8 BOM and writes rows with PHP's fputcsv() using an explicit empty escape argument to keep clean RFC 4180 quoting and avoid the PHP 8.4 deprecation.

Character encoding

Encoding is the difference between a clean export and one full of garbled accented characters in Excel.

💡

If recipients open exports in Excel on Windows and see mojibake (for example é instead of é), route them through a format that uses the server handler — the "Export CSV" fallback button or the XLSX option — so they get the BOM-prefixed file. The pure client-side dropdown CSV omits the BOM.

Copy to clipboard

The .tc-copy-clipboard button calls copyToClipboard(), which serialises the exportable rows as tab-separated values (TSV) — the format spreadsheets expect when you paste. Labels form the header line, then each row's fields are joined with \t and rows with \n.

It prefers the modern asynchronous Clipboard API and degrades gracefully:

if (navigator.clipboard && navigator.clipboard.writeText) {
  navigator.clipboard.writeText(text).then(onSuccess).catch(onError);
} else {
  onError('Clipboard API unavailable');  // falls back to a hidden textarea + execCommand('copy')
}

On success the button text swaps to "Copied!" and gains the .tc-copy-success class for two seconds before reverting. If the Clipboard API is missing or blocked (insecure context, permissions), the fallback creates an off-screen <textarea>, selects it, and runs document.execCommand('copy').

ℹ️

The async Clipboard API requires a secure context (HTTPS or localhost). On plain HTTP the code automatically uses the legacy execCommand path, so copy still works on most browsers.

Filter-aware & column-aware exports

Both CSV and clipboard pull their rows from getExportableData() and columns from getExportableColumns(), so exports honour what the visitor has narrowed the table down to:

Because filtering is applied before serialisation, a user who filters to "Region = West" and exports gets only the West rows — no extra server filtering needed.

Server-side export flow

For the server path, downloadEnhanced() POSTs a FormData payload to admin-ajax.php:

  1. Action tc_export_data with format, JSON data, JSON columns, filename, options, and a nonce from getExportNonce() (the tc_export_nonce action).
  2. ajax_export_data() verifies the nonce and a read capability, then routes the client-supplied rows through TC_Export_Handler::export_client_data()export_data().
  3. The generated file is written to wp-uploads/tablecrafter-exports/ (protected by an auto-generated .htaccess deny rule) and cached in a 5-minute transient.
  4. The response returns a one-time download_url (action tc_download_export, nonce tc_download_nonce); triggerDownload() clicks it to save the file.

Old temp files are reaped by cleanup_old_exports() (default max age one hour), and the per-download file is removed after serving.

⚠️

Server export requires a valid nonce and the read capability. Logged-out visitors are served via wp_ajax_nopriv_tc_export_data, but if the nonce can't be resolved the request is rejected. The instant client-side CSV and clipboard copy have no such requirement and always work in the browser.

The onExport callback

When you initialise a table through the JS API, supply config.onExport to hook into exports for analytics or post-processing. For client CSV it fires with the format, the exported rows, and the raw CSV string:

new TableCrafter(el, {
  exportable: true,
  exportFiltered: true,
  exportFilename: 'sales-q3',
  onExport(payload) {
    // { format: 'csv', data: [...], csvData: '...' }
    console.log('Exported', payload.format, payload.data.length, 'rows');
  }
});

Next steps

To produce formatted spreadsheets and report-ready documents from the same toolbar, continue with export-xlsx-pdf.html, and pair exports with on-page narrowing in filtering-and-search.html.