オープンソースのWikiであるGROWIにはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。
今回は、GROWIプラグインとしてExcalidrawを表示・編集できるプラグインを作りました。GROWIページで、手書き風ドローイングが利用できます。
プラグインの動作
Remark Directiveとして、以下のように記述します。 ID
は適当な文字列 001
などを指定します。
:::excalidraw[ID]{theme=dark size=600px}
{
// Excalidraw JSON
}
:::
最初は Excalidraw JSON
の部分は何もなしで大丈夫です。
:::excalidraw[ID]{theme=dark size=600px}
:::
オプション
オプションは以下が利用できます。
パラメータ名 | 説明 | デフォルト |
---|---|---|
theme | テーマ(light/dark) | light |
size | サイズ(600pxなど) | 500px |
編集
編集は、GROWIの表示画面で行います。編集画面では表示のみとなっているので注意してください。
制限事項
本プラグインでは、幾つかの制限事項があります。
- 1ページに1つのドローイングまで
複数作成すると、最後のExcalidrawのみチェンジイベントが発生するため… - 編集画面では表示のみ
編集結果をエディタに反映する方法がないため - 保存は、何もしない状態が2秒間経過したタイミング
Excalidrawのチェンジイベントは非常に激しく、マウスを動かすだけで発生します。そのため、放置して2秒後に保存を実行するようにしています
注意点
ドローイングを編集後、ページの編集ページに入って保存しようとすると衝突します。これは、GROWIの編集時には表示画面の内容を保持しており、ドローイングの編集結果が変数として反映されていないためです。
Excalidraw以外の部分を編集する際には、一度編集画面をリロードしてください。
プラグインを追加する
利用する際には、GROWIの管理画面の プラグイン
にて追加してください。URLは https://github.com/goofmint/growi-plugin-excalidraw
です。
コードについて
コードはgoofmint/growi-plugin-excalidrawにて公開しています。ライセンスはMIT Licenseになります。
最初に :::excalidraw
というRemark Directiveを処理します。この記述があれば、 code
タグに変換します。また、 excalidraw = true
を追加して、他の code
タグとの区別をつけています。
vist
メソッドで、RemarkのAST containerDirective
を処理します。2つ目の引数を指定すると、そのディレクティブの場合のみ呼び出されるので便利です。
export const remarkPlugin: Plugin = () => {
return (tree: Node) => {
visit(tree, 'containerDirective', (node: Node) => {
const n = node as unknown as GrowiNode;
if (n.name !== 'excalidraw') return;
const data = n.data || (n.data = {});
const value = n.children[1] ? (n.children[1] as GrowiNode).children.map((ele) => {
if (ele.type === 'text') return ele.value;
return `:${ele.name}`;
}).join('') : '';
const id = n.children[0] ? (n.children[0] as GrowiNode).children[0].value : 'excalidraw';
// Render your component
const { size, theme } = n.attributes;
data.hName = 'code'; // Tag name
data.hChildren = [
{
type: 'text',
value,
},
];
// Set properties
data.hProperties = {
title: JSON.stringify({
size, theme, excalidraw: true, id,
}),
};
});
};
};
ReactコンポーネントをWeb Components化
ExcalidrawはReactコンポーネントを提供しています。しかし、GROWI本体もReactなので、複数のReactアプリケーションはそのままでは利用できません。
そこで使えるのが @r2wc/react-to-web-component
です。これは、Reactコンポーネントをラップして、Web Componentsとして利用できるようにします。
import r2wc from '@r2wc/react-to-web-component';
const ExcalidrawComponentRap = r2wc(Excalidraw, {
props: {
viewModeEnabled: 'boolean',
isCollaborating: 'boolean',
initialData: 'json',
theme: 'string',
onChange: 'function',
},
});
customElements.define('excalidraw-component', ExcalidrawComponentRap);
このライブラリは、React以外でも使える(はず)なので、かなり汎用性がありそうな気がします。
Excalidrawの埋め込み
上記ライブラリでWeb ComponentsにしたExcalidrawを、GROWIのページに表示します。
const { size, excalidraw, theme, id } = JSON.parse(props.title);
const editMode = window.location.hash === '#edit';
const data = children ? JSON.parse(children) : {};
if (data.appState && data.appState.collaborators) {
data.appState.collaborators = [];
}
return (
<div style={{ height: size || '500px' }}>
<excalidraw-component
view-mode-enabled={editMode}
is-collaborating={false}
initial-data={JSON.stringify(data)}
on-change='excalidrawOnChange'
theme={theme || 'light'}
/>
</div>
);
@r2wc/react-to-web-component
の制限として、 on-change
などのイベントハンドラは、文字列で指定する + グローバル関数として定義する必要があります。そのため、 window
オブジェクトに関数を登録しておきます。
let timerId: ReturnType<typeof setTimeout>;
window.excalidrawOnChange = async(elements: object, appState: any, files: any) => {
const data = { elements, appState, files };
clearTimeout(timerId);
// onChangeが2秒間呼ばれなければ、保存処理を実行する
timerId = setTimeout(window.excalidrawOnSave, 2000, data);
};
実際の保存処理です。GROWIのJavaScript SDKを使って、既存のページを取得し、内容を正規表現で置き換えて更新します。
window.excalidrawOnSave = async(data: any) => {
// 編集画面の場合は何もしない
if (editMode) return;
// SDKの初期化
const growi = new GROWI();
// コンテンツの作成
if (data.appState && data.appState.collaborators) {
data.appState.collaborators = [];
}
// ページの取得
const page = await growi.page({ pageId: window.location.pathname.replace('/', '') });
// コンテンツの取得
const contents = await page.contents();
// 正規表現
const regString = `^(.*)\n:::excalidraw\\[${id}\\](.*?)\n(.*?)\n:::(.*)$`;
const match = contents.match(new RegExp(regString, 's'));
if (!match) return;
// 新しいページの内容
const newContents = `${match[1]}\n:::excalidraw[${id}]${match[2]}\n${JSON.stringify(data)}\n:::${match[4]}`;
// ページ更新
page.contents(newContents);
try {
await page.save();
}
catch (err) {
// console.error(err);
}
};
GROWIコミュニティについて
プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください!
まとめ
GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。