Stand with Ukraine 🇺🇦

Advanced Table — Confluence Cloud Macro

confluence-contentformatting

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.

Try for free

User Parameters

Enable sorting

Turn on column sorting

Add row numbers

Insert a numbering column as the first column

123

Pagination limit

Set the maximum number of rows per page

Show search filter

Display a search input for quick filtering

Enable CSV export

Allow downloading a table as a CSV file

CSV Delimiter

Choose the character used to separate CSV values

Template

$body

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gridjs/dist/theme/mermaid.min.css" />

<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>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const EXPORT_ICON = "ph:download";

    // User parameters
    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']";

    // Parse $body value to extract tables
    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'));

      // Extract header row (first row with <th>)
      const headerRow = allTrs.find(tr => tr.querySelector('th'));
      const headerCells = Array.from(headerRow?.querySelectorAll('th') || [])
        .map(th => th.textContent.trim());

      // Extract data rows
      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
        }))
      ];

      // Create Grid.js container
      const wrapper = document.createElement('div');
      wrapper.style.width = '95%';

      const liveTable = liveTables[idx];
      if (!liveTable) return;
      liveTable.replaceWith(wrapper);

      // Render Grid.js
      const grid = new gridjs.Grid({
        columns,
        data: numberedData,
        sort: enableSorting,
        search: showFilter,
        pagination: {
          limit: paginationLimit
        },
        width: '100%'
      });
      grid.render(wrapper);

      // CSV export
      if (allowExport) {
        setTimeout(() => {
          const btn = document.createElement('button');
          btn.innerHTML = '<span class="iconify" data-icon="' + EXPORT_ICON + '"></span>';
          btn.title = 'Download CSV';
          btn.style.cssText = `
            border: none;
            background: none;
            cursor: pointer;
            font-size: 20px;
            float: right;
          `;

          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);
      }
    });

    // CSV conversion utility
    function convertToCSV(arr, delimiter) {
      return arr.map(row =>
        row.map(cell =>
          '"' + String(cell || '').replace(/"/g, '""') + '"'
        ).join(delimiter)
      ).join('\n');
    }

    // Trigger file download
    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>