1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GROWIで手書き風ドローができるExcalidrawを使えるプラグインを作りました

Posted at

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

今回は、GROWIプラグインとしてExcalidrawを表示・編集できるプラグインを作りました。GROWIページで、手書き風ドローイングが利用できます。

image.jpg

プラグインの動作

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 です。

Admin

コードについて

コードは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 Slackへの参加はこちらから

まとめ

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

OSS開発wikiツールのGROWI | 快適な情報共有を、全ての人へ

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?