This Confluence user macro enhances all tables from $body using DOM parsing and replaces them with interactive Grid.js components.
It supports sorting, filtering, pagination, and CSV export with customizable delimiter (csvDelimiter param).
All tables are processed by default without the need for a separate toggle.
Icons like the export button are rendered using Iconify Iconify CDN for a clean UI.
Grid.js styles are loaded from CDN and tables are injected dynamically into the live Confluence page.
$body
<link id="gridjs-theme-light" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridjs/dist/theme/mermaid.min.css" />
<link id="gridjs-theme-dark" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridjs/dist/theme/mermaid-dark.min.css" disabled />
<script src="https://cdn.jsdelivr.net/npm/gridjs/dist/gridjs.umd.js"></script>
<script src="https://code.iconify.design/3/3.1.1/iconify.min.js"></script>
<style>
.gridjs-export-btn {
border: none;
background: none;
cursor: pointer;
font-size: 20px;
float: right;
color: var(--ds-text, #292a2e);
margin-top: 4px;
}
.gridjs-search input {
border-radius: 6px;
border: 1px solid var(--ds-border, #dfe1e6);
color: var(--ds-text, #292a2e);
background: var(--ds-surface, #ffffff);
}
.gridjs-search input:focus {
outline: none;
border-color: var(--ds-border-focused, #388bff);
box-shadow: 0 0 0 1px var(--ds-border-focused, #388bff);
}
html[data-color-mode="dark"] .gridjs-export-btn {
color: var(--ds-text, #b6c2cf);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const EXPORT_ICON = "ph:download";
const macroBody = `$body`;
const enableSorting = "$!parameters['enableSorting']" === "true";
const autoNumber = "$!parameters['addRowNumbers']" === "true";
const allowExport = "$!parameters['enableCSVExport']" === "true";
const showFilter = "$!parameters['showSearchFilter']" === "true";
const paginationLimit = parseInt("$!parameters['paginationLimit']") || 15;
const csvDelimiter = "$!parameters['cSVDelimiter']";
function applyGridTheme() {
const htmlMode = document.documentElement.getAttribute("data-color-mode");
document.getElementById("gridjs-theme-light").disabled = htmlMode === "dark";
document.getElementById("gridjs-theme-dark").disabled = htmlMode !== "dark";
}
applyGridTheme();
new MutationObserver(applyGridTheme).observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-color-mode"]
});
const parser = new DOMParser();
const doc = parser.parseFromString(macroBody, 'text/html');
const allTables = Array.from(doc.querySelectorAll('table'));
const liveTables = document.querySelectorAll('table[ac\\:local-id]');
allTables.forEach((table, idx) => {
const allTrs = Array.from(table.querySelectorAll('tr'));
const headerRow = allTrs.find(tr => tr.querySelector('th'));
const headerCells = Array.from(headerRow ? headerRow.querySelectorAll('th') : []).map(th => th.textContent.trim());
const dataRows = allTrs
.filter(tr => tr !== headerRow)
.map(tr => Array.from(tr.querySelectorAll('td')).map(td => td.textContent.trim()));
const numberedData = autoNumber ? dataRows.map((row, i) => [i + 1, ...row]) : dataRows;
const columns = [
...(autoNumber ? [{
name: '#',
width: '40px',
sort: false
}] : []),
...headerCells.map(col => ({
name: col,
sort: enableSorting
}))
];
const wrapper = document.createElement('div');
wrapper.style.width = '95%';
const liveTable = liveTables[idx];
if (!liveTable) return;
liveTable.replaceWith(wrapper);
const grid = new gridjs.Grid({
columns,
data: numberedData,
sort: enableSorting,
search: showFilter,
pagination: {
limit: paginationLimit
},
width: '100%'
});
grid.render(wrapper);
if (allowExport) {
setTimeout(() => {
const btn = document.createElement('button');
btn.className = 'gridjs-export-btn';
btn.innerHTML = '<span class="iconify" data-icon="' + EXPORT_ICON + '"></span>';
btn.title = 'Download CSV';
btn.addEventListener('click', () => {
const csv = convertToCSV(numberedData, csvDelimiter);
downloadBlob(csv, 'table-export-' + (idx + 1) + '.csv', 'text/csv;charset=utf-8;');
});
const searchWrap = wrapper.querySelector('.gridjs-search');
if (searchWrap) {
const spanWrap = document.createElement('span');
spanWrap.className = 'gridjs-search';
spanWrap.style.cssText = 'display:inline-block;width:100%;';
searchWrap.parentNode.replaceChild(spanWrap, searchWrap);
spanWrap.appendChild(searchWrap);
spanWrap.appendChild(btn);
} else {
wrapper.prepend(btn);
}
}, 100);
}
});
function convertToCSV(arr, delimiter) {
return arr.map(row =>
row.map(cell =>
'"' + String(cell || '').replace(/"/g, '""') + '"'
).join(delimiter)
).join('\n');
}
function downloadBlob(content, filename, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
});
</script>Add a configurable sticky banner to a Confluence page
Parse a table containing cost and currency columns, convert each row's value into a selected target currency, and append a footer row showing the total amount. Useful for tracking multi-currency expenses and reporting totals in a unified currency.
Insert a page break in documents generated from a Confluence page
Add a configurable floating panel to a Confluence page
Generate an alphabetical list of labels from all spaces, linking each label to its corresponding content pages
Show an expandable page tree for a selected parent page, defaulting to the current page
Based on CQL, show a table with the following columns: Page Title, Author, Updated, and Status
Dynamically list all child pages of the current page in a table format
Display a "Delete me" message only for editors while editing a page
Show filtered issues and their relations