diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2070c541ab..08d8aac442 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) diff --git a/site/src/pages/docs/extensions/export.mdx b/site/src/pages/docs/extensions/export.mdx index fa0ca21fe1..e33439c496 100644 --- a/site/src/pages/docs/extensions/export.mdx +++ b/site/src/pages/docs/extensions/export.mdx @@ -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 @@ -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: @@ -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']` diff --git a/src/extensions/export/bootstrap-table-export.js b/src/extensions/export/bootstrap-table-export.js index a8560bb58b..09ef07093e 100644 --- a/src/extensions/export/bootstrap-table-export.js +++ b/src/extensions/export/bootstrap-table-export.js @@ -1,6 +1,6 @@ /** * @author zhixin wen - * extensions: https://github.com/hhurz/tableExport.jquery.plugin + * Built-in export implementation (replacing deprecated tableExport.jquery.plugin) */ const Utils = $.fn.bootstrapTable.utils @@ -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') + + 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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + + const lines = ['', `<${tableName}>`] + + rows.forEach(r => { + lines.push(' ') + r.forEach((v, i) => { + const key = headerRow[i] || `col${i + 1}` + lines.push(` <${key}>${esc(v)}`) + }) + lines.push(' ') + }) + + lines.push(``) + 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')};` + 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() } @@ -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 + } - 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) {