GROWIでMarkdownテーブルにソート・検索・ページネーション機能を追加するプラグインを作成しました

[オープンソースのWikiであるGROWI](https://growi.org/ja/)にはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。

今回は、GROWIプラグインとして作ったTable Gridプラグインを紹介します。これは、Markdown記法で記述したテーブルにGrid.jsを適用して、ソート・検索・ページネーション機能を追加するものです。

![FireShot Capture 148 - 66d9614f056cf96cc52c8a40 - GROWI - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/0de680de-5971-724b-3f2e-310029814c28.jpeg)


## プラグインを追加する

利用する際には、GROWIの管理画面の `プラグイン` にて追加してください。URLは `https://github.com/goofmint/growi-plugin-table-grid` です。

![Admin](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f5b3e2d3-ccf3-d536-d59e-7301ea1b2f7e.jpeg)

## 使い方

利用する際には、編集画面でいつも通りMarkdown記法にてテーブルを記述します。

```markdown
| Header1 | Header2 | Header3 |
| ------- | ------- | ------- |
| 1       | 2       | 3       |
| 4       | 5       | 6       |
| 7       | 8       | 9       |
```

そうすると、プレビュー画面にはGrid.jsを使った高機能なテーブルが表示されます。

![FireShot Capture 146 - 66d9614f056cf96cc52c8a40 - GROWI - localhost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/51068b54-281d-62ed-7b69-01700b825821.jpeg)


## 注意点

空の行があると、うまく描画できないようです。また、データの編集に対してリアルタイムに反映できない問題があります。反映したい場合には、一旦空白行を入れて（エラーにして）から、再度正しいデータを入力すると反映されます。

## コードについて

今回はtableタグに対してプラグイン機構を追加しています。HTML構造に対して適用するので、Rehypeのプラグインとして実装しています。

```tsx
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); // プラグインを追加
    return options;
  };

  // For preview
  optionsGenerators.customGeneratePreviewOptions = (...args) => {
    const preview = optionsGenerators.generatePreviewOptions(...args);
    preview.rehypePlugins.push(plugin as any); // プラグインを追加
    return preview;
  };
};
```


```tsx
export const plugin: Plugin = function() {
  return (tree) => {
    visit(tree, (node) => {
      const n = node as unknown as GrowiNode;
      try {
		// tableタグ以外は無視
        if (n.type !== 'element' || n.tagName !== 'table') {
          return;
        }
		// この中で実装
      }
      catch (e) {
        n.type = 'html';
        n.value = `<div style="color: red;">Error: ${(e as Error).message}</div>`;
      }
    });
  };
};
```

### 変換処理について

tableタグの中身から、thead/tbodyを取得します。そして、それぞれをGrid.jsの形に合わせて変換します。

```tsx
const [thead, tbody] = n.children;
const columns = thead.children[0].children.map(c => c.children[0].value);
// ["Header1", "Header2", "Header3"] となる
const data = tbody.children.map(row => row.children
	.map(cell => cell.children[0].value));
// [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"]] となる
```

そして、それをGrid.jsに対して適用します。オプションはなるべく多くしています。スタイルは、GROWIのテーマに合わせています。

```tsx
const grid = new Grid({
	columns,            // ヘッダー
	data,               // データ
	pagination: true,   // ページネーション
	search: true,       // 検索
	sort: true,         // ソート
	resizable: true,    // カラムサイズ変更
	// スタイル
	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)',
		},
	},
});
```

この時点ではまだHTMLにDOMが追加されていないので、追加を確認でき次第Grid.jsでレンダリングしています。

```tsx
// 識別用のクラス
const className = `table-grid-${Math.random().toString(36).slice(2)}`;
n.properties.className = `${n.properties.className} ${className}`;
// DOMが追加されるまで待つ
const id = setInterval(() => {
	const el = document.querySelector(`.${className}`);
	if (el) {
		clearInterval(id);
		el.innerHTML = ''; // 元々表示されているHTMLを削除
		grid.render(el); // Grid.jsでレンダリング
	}
}, 1000);
```

## スタイルについて

GROWIではテーマを選択できるので、選択したテーマによって色が異なります。また、テーマによってダークモード・ライトモードもあります。そのため、スタイルはCSS変数を使ってテーマに合わせています。

特にカラムのソートアイコン（画像）はダークモードだと背景色と同じ色になってしまうので、CSSフィルターを使って反転させています。

```css
@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;
}
```

## まとめ

GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。

[OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ](https://growi.org/ja/)