Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ restrictions:

* Please **do not** open issues or pull requests regarding the code in [`bootstrap-table-examples`](https://github.com/wenzhixin/bootstrap-table-examples) and [`extensions plugin dependence`](https://github.com/wenzhixin/bootstrap-table/tree/develop/src/extensions) (open them in their respective repositories), the dependence list:
* Table Editable: [x-editable](https://github.com/vitalets/x-editable)
* Table Export: [tableExport.jquery.plugin](https://github.com/hhurz/tableExport.jquery.plugin)
* Table Export: built-in exporter (no external plugin)
* Table Reorder-Columns: [jquery-ui](https://code.jquery.com/ui/) and [dragTable](https://github.com/akottr/dragtable/)
* Table Reorder-Rows: [tablednd](https://github.com/isocra/TableDnD)
* Table Resizable: [jquery-resizable-columns](https://github.com/dobtco/jquery-resizable-columns)
Expand Down
12 changes: 7 additions & 5 deletions site/src/pages/docs/extensions/export.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ group: extensions
toc: true
---

Use Plugin: [tableExport.jquery.plugin](https://github.com/hhurz/tableExport.jquery.plugin)

This is an important link to check out as some file types may require extra steps.
This extension now uses a built-in exporter (no external plugin required).

## Usage

Expand Down Expand Up @@ -66,7 +64,10 @@ This is an important link to check out as some file types may require extra step

- **Detail:**

Export [options](https://github.com/hhurz/tableExport.jquery.plugin#options) of `tableExport.jquery.plugin`
Options for the built-in exporter:
- `fileName`: String or function returning the base filename (without extension).
- `csvDelimiter`: CSV delimiter (default `,`).
- `tableName`: Table name used for SQL/XML exports (default `table`).

`exportOptions.fileName` can be a string or a function, for example:

Expand All @@ -86,7 +87,8 @@ This is an important link to check out as some file types may require extra step

- **Detail:**

Export types, support types: `['json', 'xml', 'png', 'csv', 'txt', 'sql', 'doc', 'excel', 'xlsx', 'pdf']`.
Built-in export types: `['json', 'xml', 'csv', 'txt', 'sql', 'excel']`.
Note: `png`, `xlsx`, `pdf`, `doc`, `powerpoint` are not supported by the built-in exporter.

- **Default:** `['json', 'xml', 'csv', 'txt', 'sql', 'excel']`

Expand Down
219 changes: 196 additions & 23 deletions src/extensions/export/bootstrap-table-export.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @author zhixin wen <wenzhixin2010@gmail.com>
* extensions: https://github.com/hhurz/tableExport.jquery.plugin
* Built-in export implementation (replacing deprecated tableExport.jquery.plugin)
*/

const Utils = $.fn.bootstrapTable.utils
Expand Down Expand Up @@ -179,10 +179,165 @@ $.BootstrapTable = class extends $.BootstrapTable {
const stateField = this.header.stateField
const isCardView = o.cardView

// Helper: escape CSV field
const csvEscape = (value, delimiter) => {
const d = delimiter || ','

if (value === null || value === undefined) return ''

const str = String(value)
const mustQuote = str.includes(d) || str.includes('"') || str.includes('\n') || str.includes('\r')

Comment on lines +183 to +190

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added helper code introduces trailing whitespace on blank lines (e.g., around this block). ESLint is configured with no-trailing-spaces: 'error' (eslint.config.js:85), so this will fail lint. Please remove the trailing spaces (or run the project formatter/linter autofix) in the new code.

Copilot uses AI. Check for mistakes.
if (!mustQuote) return str
return `"${str.replace(/"/g, '""')}"`
}

// Helper: build a 2D array of visible table content from DOM
const collectTableMatrix = ignoreColumnIdxs => {
const ignore = new Set((ignoreColumnIdxs ? ignoreColumnIdxs.map(Number) : []))
const headerRow = []
const headerTr = this.$el.find('thead tr').last()
const $ths = headerTr.children()

$ths.each((idx, th) => {
if ($(th).is(':visible') && !ignore.has(idx)) {
headerRow.push($(th).text().trim())
}
})

const rows = []
const $trs = this.$el.find('tbody > tr:visible')

$trs.each((_, tr) => {
// skip detail view rows or non-data rows
const $tr = $(tr)

if ($tr.hasClass('detail-view') || $tr.hasClass('no-records-found')) return

const row = []
$tr.children().each((idx, td) => {
if ($(td).is(':visible') && !ignore.has(idx)) {
row.push($(td).text().trim())
}
})

if (row.length) {
rows.push(row)
}
})

return { headerRow, rows }
}

// Helper: convert to content by type
const buildFile = (type, matrix, exportOptions) => {
const delimiter = exportOptions && exportOptions.csvDelimiter ? exportOptions.csvDelimiter : ','
const tableName = exportOptions && exportOptions.tableName || 'table'
const { headerRow, rows } = matrix

switch (type) {
case 'csv': {
const lines = []

if (headerRow.length) {
lines.push(headerRow.map(h => csvEscape(h, delimiter)).join(delimiter))
}

rows.forEach(r => lines.push(r.map(v => csvEscape(v, delimiter)).join(delimiter)))
return { mime: 'text/csv;charset=utf-8', ext: 'csv', content: lines.join('\r\n') }
}
case 'txt': {
const d = '\t'
const lines = []

if (headerRow.length) {
lines.push(headerRow.join(d))
}

rows.forEach(r => lines.push(r.join(d)))
return { mime: 'text/plain;charset=utf-8', ext: 'txt', content: lines.join('\r\n') }
}
case 'json': {
const objs = []

if (headerRow.length) {
rows.forEach(r => {
const obj = {}

headerRow.forEach((h, i) => {
obj[h || `col${i + 1}`] = r[i]
})

objs.push(obj)
})
} else {
rows.forEach(r => objs.push(r))
}

return { mime: 'application/json;charset=utf-8', ext: 'json', content: JSON.stringify(objs, null, 2) }
}
case 'xml': {
const esc = s => String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')

const lines = ['<?xml version="1.0" encoding="UTF-8"?>', `<${tableName}>`]

Comment on lines +287 to +288

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableName is interpolated directly as the XML root element name. If exportOptions.tableName contains spaces or other invalid XML name characters, the generated XML becomes invalid. Please sanitize/normalize tableName to a safe XML element name (and fall back to a default when it cannot be made valid).

Copilot uses AI. Check for mistakes.
rows.forEach(r => {
lines.push(' <row>')
r.forEach((v, i) => {
const key = headerRow[i] || `col${i + 1}`
lines.push(` <${key}>${esc(v)}</${key}>`)
})
Comment on lines +291 to +294

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XML element names are derived from header text (key = headerRow[i]). Header labels commonly contain spaces/special characters, which produces invalid XML tags like <First Name>. Please normalize header-derived keys into valid XML element names (e.g., replace invalid characters, ensure a valid starting character) and fall back to colN when needed.

Copilot uses AI. Check for mistakes.
lines.push(' </row>')
})

lines.push(`</${tableName}>`)
return { mime: 'application/xml;charset=utf-8', ext: 'xml', content: lines.join('\n') }
}
case 'sql': {
const cols = headerRow.map(h => (h || '').replace(/`/g, '``') || null).map((c, i) => c || `col${i + 1}`)

const values = rows.map(r => `(${r.map(v => {
if (v === null || v === undefined || v === '') return 'NULL'
const s = String(v).replace(/'/g, '\'\'' )
return `'${s}'`
}).join(', ')})`)

const sql = `INSERT INTO \`${tableName}\` (\`${cols.join('`, `')}\`) VALUES\n${values.join(',\n')};`
Comment on lines +304 to +310

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the SQL export, tableName is used inside backticks without escaping backticks in the name, and when rows is empty the generated statement becomes VALUES\n; (invalid SQL). Please escape backticks in tableName similarly to column names, and handle the no-rows case by returning an empty string/file or a comment instead of an invalid INSERT.

Suggested change
const values = rows.map(r => `(${r.map(v => {
if (v === null || v === undefined || v === '') return 'NULL'
const s = String(v).replace(/'/g, '\'\'' )
return `'${s}'`
}).join(', ')})`)
const sql = `INSERT INTO \`${tableName}\` (\`${cols.join('`, `')}\`) VALUES\n${values.join(',\n')};`
// If there are no rows, avoid generating an invalid INSERT statement.
if (!rows.length) {
return { mime: 'application/sql;charset=utf-8', ext: 'sql', content: '' }
}
const values = rows.map(r => `(${r.map(v => {
if (v === null || v === undefined || v === '') return 'NULL'
const s = String(v).replace(/'/g, '\'\'' )
return `'${s}'`
}).join(', ')})`)
const safeTableName = String(tableName).replace(/`/g, '``')
const sql = `INSERT INTO \`${safeTableName}\` (\`${cols.join('`, `')}\`) VALUES\n${values.join(',\n')};`

Copilot uses AI. Check for mistakes.
return { mime: 'application/sql;charset=utf-8', ext: 'sql', content: sql }
}
case 'excel': {
// Map to CSV for compatibility with legacy default 'excel' option
const csv = buildFile('csv', matrix, exportOptions)
return { mime: 'application/vnd.ms-excel', ext: 'xls', content: csv.content }
}
default:
return null
}
}

// Helper: trigger browser download
const saveAs = (content, mime, filename) => {
const blob = new Blob([content], { type: mime })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}

const doExport = callback => {
if (stateField) {
this.hideColumn(stateField)
}

if (isCardView) {
this.toggleView()
}
Expand Down Expand Up @@ -235,34 +390,52 @@ $.BootstrapTable = class extends $.BootstrapTable {
options.fileName = o.exportOptions.fileName()
}

this.$el.tableExport(Utils.extend({
onAfterSaveToFile: () => {
if (o.exportFooter) {
this.load(data)
}
// Build content and save without external plugin
const effective = Utils.extend({}, o.exportOptions, options)
const type = (effective.type || 'csv').toLowerCase()
const ignoreIdx = effective.ignoreColumn
const matrix = collectTableMatrix(ignoreIdx)
const built = buildFile(type, matrix, effective)

if (stateField) {
this.showColumn(stateField)
const onAfter = () => {
if (o.exportFooter) {
this.load(data)
}

if (stateField) {
this.showColumn(stateField)
}

if (isCardView) {
this.toggleView()
}

hiddenColumns.forEach(row => {
if (row.forceExport) {
this.hideColumn(row.field)
}
if (isCardView) {
this.toggleView()
})

this.columns.forEach(row => {
if (row.forceHide) {
this.showColumn(row.field)
}
})

hiddenColumns.forEach(row => {
if (row.forceExport) {
this.hideColumn(row.field)
}
})
if (callback) callback()
}

this.columns.forEach(row => {
if (row.forceHide) {
this.showColumn(row.field)
}
})
if (!built) {
// unsupported type without deprecated plugin; no-op but still restore state
onAfter()
return
}
Comment on lines +428 to +432

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the export type is unsupported, doExport silently no-ops and restores state, but the caller still fires the export-saved event, which is misleading because no file was produced. Please have doExport return a success/failure flag (or throw/trigger an error event) so unsupported types can avoid emitting export-saved and optionally notify the user.

Copilot uses AI. Check for mistakes.

if (callback) callback()
}
}, o.exportOptions, options))
const fileBase = effective.fileName || 'table-export'
const filename = `${fileBase}.${built.ext}`
saveAs(built.content, built.mime, filename)

onAfter()
}

if (o.exportDataType === 'all' && o.pagination) {
Expand Down