DEV Community

Atsushi
Atsushi

Posted on

I created a plugin for adding sort, search, and pagination functions to Markdown tables in GROWI

GROWI is an open-source Wiki platform that offers a plugin system, enabling users to display and customize their data effectively.

In this article, we introduce the Table Grid plugin developed for GROWI. This plugin applies Grid.js to tables written in Markdown, adding functionalities such as sorting, searching, and pagination.

Table Grid Plugin Preview

Adding the Plugin

To use this plugin, navigate to the Plugins section in GROWI's administration panel and add the following URL: https://github.com/goofmint/growi-plugin-table-grid.

Admin Panel

Usage

In the editor, write a table using standard Markdown syntax:

| Header1 | Header2 | Header3 |
| ------- | ------- | ------- |
| 1       | 2       | 3       |
| 4       | 5       | 6       |
| 7       | 8       | 9       |
Enter fullscreen mode Exit fullscreen mode

Upon previewing, the table will be enhanced with Grid.js, providing advanced features.

Enhanced Table Preview

Important Notes

  • Empty Rows: The plugin may not render tables correctly if empty rows are present.
  • Real-time Updates: Changes to data might not reflect immediately. To update, insert a blank line (causing an error) and then re-enter the correct data.

About the Code

The plugin integrates with the table tag using GROWI's plugin system and is implemented as a Rehype plugin.

const activate = (): void => {
  if (growiFacade == null || growiFacade.markdownRenderer == null) {
    return;
  }

  const { optionsGenerators } = growiFacade.markdownRenderer;

  // For page view
  optionsGenerators.customGenerateViewOptions = (...args) => {
    const options = optionsGenerators.generateViewOptions(...args);
    options.rehypePlugins.push(plugin as any); // Add plugin
    return options;
  };

  // For preview
  optionsGenerators.customGeneratePreviewOptions = (...args) => {
    const preview = optionsGenerators.generatePreviewOptions(...args);
    preview.rehypePlugins.push(plugin as any); // Add plugin
    return preview;
  };
};
Enter fullscreen mode Exit fullscreen mode

The plugin processes the table tag, extracting thead and tbody to transform them into a format compatible with Grid.js.

export const plugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
      const n = node as unknown as GrowiNode;
      try {
        // Ignore non-table tags
        if (n.type !== 'element' || n.tagName !== 'table') {
          return;
        }
        // Implementation here
      }
      catch (e) {
        n.type = 'html';
        n.value = `<div style="color: red;">Error: ${(e as Error).message}</div>`;
      }
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

The transformation involves mapping the table headers and data to Grid.js's structure:

const [thead, tbody] = n.children;
const columns = thead.children[0].children.map(c => c.children[0].value);
// Results in ["Header1", "Header2", "Header3"]
const data = tbody.children.map(row => row.children
  .map(cell => cell.children[0].value));
// Results in [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"]]
Enter fullscreen mode Exit fullscreen mode

These are then applied to Grid.js with various options, styled to match GROWI's theme:

const grid = new Grid({
  columns,            // Headers
  data,               // Data
  pagination: true,   // Pagination
  search: true,       // Search
  sort: true,         // Sorting
  resizable: true,    // Column resizing
  // Styling
  style: {
    table: {
      border: '2px solid var(--bs-secondary-bg)',
    },
    th: {
      color: 'var(--bs-body-color)',
      backgroundColor: 'var(--bs-body-bg)',
    },
    td: {
      color: 'var(--bs-body-color)',
      backgroundColor: 'var(--bs-body-bg)',
    },
    footer: {
      backgroundColor: 'var(--bs-secondary-bg)',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Since the DOM isn't updated immediately, the plugin waits for the DOM to be ready before rendering with Grid.js:

// Unique class for identification
const className = `table-grid-${Math.random().toString(36).slice(2)}`;
n.properties.className = `${n.properties.className} ${className}`;
// Wait for DOM to be ready
const id = setInterval(() => {
  const el = document.querySelector(`.${className}`);
  if (el) {
    clearInterval(id);
    el.innerHTML = ''; // Clear existing HTML
    grid.render(el);   // Render with Grid.js
  }
}, 1000);
Enter fullscreen mode Exit fullscreen mode

Styling Considerations

GROWI supports multiple themes, including dark and light modes. To ensure compatibility, the plugin utilizes CSS variables. For instance, sort icons may blend with the background in dark mode; applying a CSS filter inverts their color:

@media (prefers-color-scheme: dark) {
  .gridjs-sort {
    filter: invert(100%);
  }
}

.gridjs-input {
  color: var(--bs-body-color) !important;
  background-color: var(--bs-body-bg) !important;
}

.gridjs-pages button {
  color: var(--bs-body-color) !important;
  background-color: var(--bs-body-bg) !important;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

GROWI's plugin system allows for extensive customization of its display features. If you find any functionalities lacking, consider developing and adding your

Top comments (0)