オープンソースのWikiであるGROWIにはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。
今回は、GROWIプラグインとして、GROWIの表示ページ(編集ページではなく)にてファイルのアップロードを実現するプラグインを作りました。編集ページに移動することなく、ファイルをアップロードできます。ファイルアップローダー的に使ってもらえます。
プラグインの動作
Remark Directiveとして、以下のように記述します。 []
ドロップゾーンで表示するテキストを入力します(デフォルトは Drag here to preview
です)。
::dropzone[Drag here to preview]
そうすると、以下のように表示されます。
この枠の中にファイルをドラッグ&ドロップします。ファイルの場合は添付ファイルとして、画像の場合はインライン画像として表示されます。
プラグインを追加する
利用する際には、GROWIの管理画面の プラグイン
にて追加してください。URLは https://github.com/goofmint/growi-plugin-dropzone
です。
コードについて
コードはgoofmint/growi-plugin-dropzone: GROWI plugin for dropzoneにて公開しています。ライセンスはMIT Licenseになります。
最初に ::dropzone
というRemark Directiveを処理します。この記述があれば、 a
タグに変換します。また、 dropzone = true
を追加して、他の a
タグとの区別をつけています。
vist
メソッドで、RemarkのAST leafDirective
を処理します。2つ目の引数を指定すると、そのディレクティブの場合のみ呼び出されるので便利です。 leafDirective
は、行頭で ::
から始まるディレクティブです。
export const remarkPlugin: Plugin = () => {
return (tree: Node) => {
visit(tree, 'leafDirective', (node: Node) => {
const n = node as unknown as GrowiNode;
if (n.name !== 'dropzone') return;
const data = n.data || (n.data = {});
// Render your component
const { value } = n.children[0] || { value: '' };
data.hName = 'a'; // Tag name
data.hChildren = [{ type: 'text', value }]; // Children
// Set properties
data.hProperties = {
href: 'https://example.com/rss',
title: JSON.stringify({ ...n.attributes, ...{ dropzone: true } }), // Pass to attributes to the component
};
});
};
};
外部データを取得して表示
GROWI 7.2から、React Hooksに対応しました。今回のプラグインではそれを使ってファイルのドロップを検知しています。
export const helloGROWI = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
return ({ children, ...props }) => {
try {
const { dropzone } = JSON.parse(props.title);
if (dropzone) {
// 7.2から対応したReact Hooksを使っています
const { react } = growiFacade;
const { useEffect, useCallback, useState } = react;
const [isDragOver, setIsDragOver] = useState(false);
// ページID
const pageId = window.location.pathname.split('/').pop();
// 編集ページかどうかの判定
const edit = window.location.hash.includes('edit');
const onDrop = useCallback(async(event: React.DragEvent<HTMLDivElement>) => {
// ドロップした後の処理
}, []);
return (
<>
<div id="drop-area"
onDragEnter={() => setIsDragOver(true)}
onDragLeave={() => setIsDragOver(false)}
onDragOver={() => setIsDragOver(true)}
onDrop={onDrop}
className={isDragOver ? 'drag-over' : ''}
>
{children || 'Drag here to preview'}
</div>
<input type="file" id="file-input" multiple hidden />
<div id="preview-container"></div>
</>
);
}
}
catch (err) {
// console.error(err);
}
// Return the original component if an error occurs
return (
<Tag {...props}>{children}</Tag>
);
};
};
スタイルシートでは、ドロップゾーンに対して枠の表示を行っています。また、ドラッグしている際の表示反転処理も追加しています。
#drop-area {
width: 400px;
height: 200px;
margin: 20px auto;
line-height: 200px;
text-align: center;
cursor: pointer;
border: 2px dashed var(--bs-body-color);
backgroundColor: var(--bs-body-bg);
}
#preview-container {
text-align: center;
}
#drop-area.drag-over {
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border: 2px dashed var(--bs-primary);
}
ドロップした後の処理
ファイルをドロップすると onDrop
が走ります。ここでは、ファイルをGROWI JS SDKを使ってアップロードし、ページに追記しています。詳細はコメントを参照してください。
const onDrop = useCallback(async(event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
// 編集モードなら何もしない
if (edit) return;
// ドロップしたファイルを取得
const { files } = event.dataTransfer;
// ファイルがない場合は何もしない
if (files.length === 0) return;
// 現在のページを取得
const page = await growi.page({ pageId });
// ファイルをアップロード
const promises = [];
for (const file of files) {
promises.push(page.upload(file.name, file as any));
}
// レスポンスは添付ファイルオブジェクトの配列
const attachments = await Promise.all(promises);
// Markdown記法に変換
const result = attachments.map((attachment) => {
return attachment.fileFormat?.split('/')[0] === 'image'
? `` // 画像の場合はインライン画像
: `[${attachment.fileName}](${attachment.filePathProxied})`; // それ以外は添付ファイル
});
// 現在のページの内容を取得
const contents = await page.contents();
// 現在のページの内容に追記
const newContents = `${contents}\n${result.join('\n')}`;
// 保存
await page.contents(newContents);
await page.save();
// ドラッグオーバーを解除
setIsDragOver(false);
// ページをリロード
window.location.reload();
}, []);
最後にページをリロードしているのは、更新された内容をその場で表示に反映できないためです。
GROWIコミュニティについて
プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください!
まとめ
GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。