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
Parses a table containing cost and currency columns, converts each row's value into a selected result currency, and appends a footer row showing the total sum. Useful for tracking multi-currency expenses and reporting totals in a unified currency.
Insert a page break in documents generated from a Confluence page
Generate a list of labels from all spaces leading to corresponding content pages, organized in alphabetical order.
Add a configurable floating panel to a Confluence page
Show an expandable page tree for a selected parent page (defaults to the current page)
Based on CQL it shows a table with: Page title, Author, Updated, Status
Macro that dynamically lists all its child pages in a table format
Show filtered issues and their relations
Shows page creation date