react-cropperとfilepondの連携
こんなのを作るぞい
これは
こちらの方の記事でも実践されているんだけど、vueがよくわからんのもあるし、callback的なアプローチがうまくいかなくて、どうなんやろーと思っていた。今回はちょっと違うアプローチで解決してみる事とする。
事前準備
何でもいいけどlaravelでリソースルートを1つ作る、てか別にリソースルートである必要は無い。indexとstoreがあればいい。それが面倒じゃなければ自分で組み立てればいい。ここではメソッドを書くのがダルいので-rオプションを使ってるだけね。でまあ、実際にDBに保存とかはいらんと思うからモデルとかは利用していない。
artisan make:controller FilepondCropController -r
でroutes/web.php
Route::resource('fcrops', FilepondCropController ::class);
fcropsという名前にした。名前は重要じゃないが、まあ何かださいけどいいか。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class FilepondCropController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): Response
{
return Inertia::render('FileCrops/Index', [
]);
}
このようにFileCrops/Indexをview指定したのでresources/js/Pages/FileCrops/Index.jsx これが当該jsxになるね。
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
import { Head } from '@inertiajs/react'
export default function FileCropsIndex({ auth }) {
return (
<AuthenticatedLayout
user={auth.user}
header={
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
FilePond Crop
</h2>
}
>
<Head title="FilePond Crop" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
)
}
こういう具合にしておく。http://server/filecropsにアクセスすれば例によって雛形が表示されるであろう。
必要ライブラリーのインストール
react-cropper
react-filepond
filepond-plugin-file-validate-type
filepond-plugin-image-preview
filepond-plugin-image-exif-orientation
filepond-plugin-image-edit
これくらいは必要である。まとめるとこんな感じ
npm install react-cropper react-filepond filepond-plugin-file-validate-type filepond-plugin-image-preview filepond-plugin-image-exif-orientation filepond-plugin-image-edit
filepondを設置する
ここでは前に解説したのもあってあんまり深くは書かない。日本語化もしない。その辺やりたければ前の記事を探ってください。
import { Head, useForm } from '@inertiajs/react';
// <<--- added
import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import PrimaryButton from '@/Components/PrimaryButton';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
registerPlugin(FilePondPluginFileValidateType, FilePondPluginImagePreview);
// --->>
export default function FileCropsIndex({ auth }) {
// <<--- added
const {
data, setData, processing, reset, post,
} = useForm({
files: [],
});
const submit = (e) => {
e.preventDefault();
post(route('fcrops.store'));
};
const handleFilePondUpdate = (fileItems) => {
setData('files', fileItems.map((fileItem) => fileItem.file));
};
// --->>
return (
<AuthenticatedLayout
user={auth.user}
header={(
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
FilePond Crop
</h2>
)}
>
<Head title="FilePond Crop" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
{/* Form Added */}
<form onSubmit={submit}>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</form>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
全部ピロっと貼りこめばこうなるはずだ
おっさんのavatar飽きてきたから違うのにするわw
uploadしてみる
とりあえずuploadを行えるようにする。これはstoreを使う
public function store(Request $request): RedirectResponse
{
$uploadedFile = $request->file('files')[0];
$destinationPath = storage_path('app/public');
$uploadedFile->move($destinationPath, 'avatar.jpg');
return redirect()->route('filecrops.index');
}
ここではavatar.jpg に固定し、public_pathに保存した。まあ名前はどうでもいいんだけどstorage/app/public/avatar.jpg へと正しくuploadされているのを確認すること。
indexでの表示
まずavatar出力ルートを作る
Route::get('fcrops/avatar', [FilepondCropController::class, 'avatar'])->name('fcrops.avatar');
Controllerにavatarメソッドを置く
use \Symfony\Component\HttpFoundation\BinaryFileResponse ;
class FilepondCropController extends Controller
// ...
public function avatar(): BinaryFileResponse
{
$path = storage_path('app/public/avatar.jpg');
if (!file_exists($path)) {
abort(404, 'Avatar version not found');
}
return response()->file($path);
}
ファイルの存在を確認する
public function index(): Response
{
$avatarFile = storage_path('app/public/avatar.jpg');
$fileExists = file_exists($avatarFile);
return Inertia::render('FileCrops/Index', [
'avatarExists' => $fileExists,
]);
}
ファイルがあれば表示
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<form onSubmit={submit}>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</form>
{/* Avatar Added */}
{avatarExists && <img src={route('fcrops.avatar')} />}
</div>
</div>
</div>
</div>
ただし、この場合もキャッシュが効いてしまう事があるから、timestampでも付けておいた方がいいかも。
{avatarExists && (
<img src={`${route('fcrops.avatar')}?t=${Date.now()}`} alt="Avatar" />
)}
</div>
まあとりあえずここは動くのを優先で、必要であればキャッシュ等等はあとからチューニングしてほしい。
編集機能を追加
これはfilepond-plugin-image-editを使う。既にnpmではinstallされているとは思うが、まだコードの中には呼びこんでいない。ので呼びこんでみる。
import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
// Added
import FilePondPluginImageEdit from 'filepond-plugin-image-edit';
import PrimaryButton from '@/Components/PrimaryButton';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginImagePreview,
FilePondPluginImageEdit, // Added
);
ここで読みこんだとて編集ボタンが付くわけではない
ここに編集ボタンを表示させるにあたってはコールバック関数をセットする事が必要となる。コールバック関数に関しては
https://github.com/pqina/filepond-plugin-image-edit/blob/master/index.html
これに従いというかまあいろいろパクってこうする
const editor = {
// Called by FilePond to edit the image
// - should open your image editor
// - receives file object and image edit instructions
open: (file, instructions) => {
// open editor here
},
// Callback set by FilePond
// - should be called by the editor when user confirms editing
// - should receive output object, resulting edit information
onconfirm: (output) => {},
// Callback set by FilePond
// - should be called by the editor when user cancels editing
oncancel: () => {},
// Callback set by FilePond
// - should be called by the editor when user closes the editor
onclose: () => {},
};
(まあこれはドキュメントにも書いてあるけど)
そしてFilePondにimageEditEditorの指定を与える。ここではeditor()って名前にしたのでeditorを指定して\いる
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor} // updated
/>
そうすると画像に編集できそうなアイコンが爆誕する
ただし、これを押しても何も起きない。強いていえば
open: (file, instructions) => {
// open editor here
},
この辺でlogってみると一応押されてる事は理解できる。
アイコンが、爆誕してなければこの先には進めない。ここまでのコードの全量を載せておくからよくチェックして欲しい。
import { Head, useForm } from '@inertiajs/react';
import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
// Added
import FilePondPluginImageEdit from 'filepond-plugin-image-edit';
import PrimaryButton from '@/Components/PrimaryButton';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginImagePreview,
FilePondPluginImageEdit, // Added
);
export default function FileCropsIndex({ auth, avatarExists }) {
const {
data, setData, processing, reset, post,
} = useForm({
files: [],
});
const submit = (e) => {
e.preventDefault();
post(route('fcrops.store'));
};
const handleFilePondUpdate = (fileItems) => {
setData('files', fileItems.map((fileItem) => fileItem.file));
};
const editor = {
// Called by FilePond to edit the image
// - should open your image editor
// - receives file object and image edit instructions
open: (file, instructions) => {
// open editor here
},
// Callback set by FilePond
// - should be called by the editor when user confirms editing
// - should receive output object, resulting edit information
onconfirm: (output) => {},
// Callback set by FilePond
// - should be called by the editor when user cancels editing
oncancel: () => {},
// Callback set by FilePond
// - should be called by the editor when user closes the editor
onclose: () => {},
};
return (
<AuthenticatedLayout
user={auth.user}
header={(
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
FilePond Crop
</h2>
)}
>
<Head title="FilePond Crop" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<form onSubmit={submit}>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor} // updated
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</form>
{/* Avatar Added */}
{avatarExists && (
<img src={`${route('fcrops.avatar')}?t=${Date.now()}`} alt="Avatar" />
)}
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
react-cropperの設置
ここでreact-cropperを配置し、画像を削りこんだりできるようにする
で、ここで見たように、srcパラメーターが必要だがこれはreplaceという関数を使ってもいい。ここではreplaceを使ってみることにする。
まあその前にcropの領域を配置しないといけない。場所はまあどこでもいいけどformとformの間に挟む場合はボタンの扱いに注意が必要にはなる。まあいいか。
まず冒頭でこれを付ける。すんません、ちょっとimportの並びを整理しましたよ。
import PrimaryButton from '@/Components/PrimaryButton';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import FilePondPluginImageEdit from 'filepond-plugin-image-edit';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginImagePreview,
FilePondPluginImageEdit,
);
// Added
import Cropper from 'react-cropper';
import 'cropperjs/dist/cropper.css';
そうしたら
<form onSubmit={submit}>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor} // updated
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
{/* react-cropper Added */}
<Cropper
ref={cropperRef}
/>
</form>
このように、refを渡すものだけを作る。しかし今refを定義していないからこれだとエラーになるのでそのあたりを定義していく。
冒頭で
import React, { useRef } from 'react';
useRefを呼びこんで
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null); // Added
として準備しておく。まあこれだけだ。
react-cropperに画像を引き渡す
そうしたら、editor関数でこのcropperRefを操作する
const editor = {
// Called by FilePond to edit the image
// - should open your image editor
// - receives file object and image edit instructions
open: (file, instructions) => {
const objectURL = URL.createObjectURL(file);
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
imageElement.cropper.replace(objectURL);
}
},
このコードをフルで解説するのはムズいが、とにかくファイルからローカルにblobを作ってcropperのリファレンスをひっぱりだしてreplaceっていう関数をコールするということになる。すると
こういう編集画面になる。しかしこれはちょっとデカいのとfilepondの下にあるのが面倒くさいので微調整していくよ
<form onSubmit={submit}>
{/* react-cropper Added */}
<Cropper
ref={cropperRef}
style={{ height: 300, width: '100%' }}
aspectRatio={1} // force square
/>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor} // updated
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</form>
まー
このように謎の間隔が空いてしまうので最終的には結局調整が必要と思う
コメントにあるように画像のクロッピングに関しては正方形に強制する。avatarですからね。
crop確定ボタン
今、croppingの選択は出来ているが確定のボタンがないのでこれを確定するボタンを配置する
<form onSubmit={submit}>
<Cropper
ref={cropperRef}
style={{ height: 300, width: '100%' }}
aspectRatio={1} // force square
/>
{/* Crop button Added */}
<PrimaryButton type="button">Crop</PrimaryButton>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor} // updated
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</form>
formの中に書いているのでtype="button"を付けているが、まあこの辺の見た目は相当にイマイチなので後でチューニングするよ〜とりあえず動く理論で
そしたらcropという関数を作っておいて
const crop= () => {
};
onClipで
<Cro pper
ref={cropperRef}
style={{ height: 300, width: '100%' }}
aspectRatio={1} // force square
/>
<PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
ひっかけておく。
クロッピングを実施しFilePondに差し戻す
とりあえず現状だ
import React, { useRef } from 'react';
import { Head, useForm } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import FilePondPluginImageEdit from 'filepond-plugin-image-edit';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
import Cropper from 'react-cropper';
import PrimaryButton from '@/Components/PrimaryButton';
import 'cropperjs/dist/cropper.css';
registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginImagePreview,
FilePondPluginImageEdit,
);
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null);
const {
data, setData, processing, reset, post,
} = useForm({
files: [],
});
const crop = () => {
};
const submit = (e) => {
e.preventDefault();
post(route('fcrops.store'));
};
const handleFilePondUpdate = (fileItems) => {
setData('files', fileItems.map((fileItem) => fileItem.file));
};
const editor = {
// Called by FilePond to edit the image
// - should open your image editor
// - receives file object and image edit instructions
open: (file, instructions) => {
const objectURL = URL.createObjectURL(file);
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
imageElement.cropper.replace(objectURL);
}
},
// Callback set by FilePond
// - should be called by the editor when user confirms editing
// - should receive output object, resulting edit information
onconfirm: (output) => {},
// Callback set by FilePond
// - should be called by the editor when user cancels editing
oncancel: () => {},
// Callback set by FilePond
// - should be called by the editor when user closes the editor
onclose: () => {},
};
return (
<AuthenticatedLayout
user={auth.user}
header={(
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
FilePond Crop
</h2>
)}
>
<Head title="FilePond Crop" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<form onSubmit={submit}>
<Cropper
ref={cropperRef}
style={{ height: 300, width: '100%' }}
aspectRatio={1} // force square
/>
<PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
<FilePond
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor} // updated
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</form>
{avatarExists && (
<img src={`${route('fcrops.avatar')}?t=${Date.now()}`} alt="Avatar" />
)}
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
ここでeditorのソースをよくみてみると
// Callback set by FilePond
// - should be called by the editor when user confirms editing
// - should receive output object, resulting edit information
onconfirm: (output) => {},
こんなのがある。
まあこれcallbackってことなんで基本的にはこのメソッドを備えるエディター用なんだろうけど、これのcallの仕方が絶妙によくわからんので、まあ使わないことにするw
ってことで、ここからは独自ではあるが、croppingした実データーをfilepondに差しもどすという割と強引な手法で行っていこう。
croppingする
croppingは今確定ボタンに指定されたcrop関数があるから
const crop = () => {
};
ここに書いていく。ここでもまたreact-cropperのrefを得る必要がある
const crop = () => {
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
//
}
};
そしたらcropからfilepondを操作するのでfilepondのrefも得る必要がある。これは準備が必要である
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null);
const filePondRef = useRef(null); // Added
しといての
<FilePond
ref={filePondRef} // updated
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor}
/>
などでrefを渡せるから、そうしたらcrop関数をこのようにする
const crop = () => {
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
const canvas = imageElement.cropper.getCroppedCanvas();
canvas.toBlob(blob => {
// FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタ
ンスに追加
filePondRef.current.addFile(blob);
}, "image/jpeg");
}
};
image/jpegハードコードが実にダセえけど、これは後で修正するとして、うまいこといくと、以下のような動きになるだろう。
実際に送信して確認する
sendを押してちゃんと切り取られた画像が保存されているかどうか確認する事
微修正
ここまででやりたい事の機能はほとんど実現できているが
デザインがイマイチ
content-typeがハードコードされている
という2つの微妙なポイントを抱えている。後者の方が簡単に修正できるからそっちからやっていこう
content-typeを保存し、それを割り当てる
単純にこれは
const editor = {
// Called by FilePond to edit the image
// - should open your image editor
// - receives file object and image edit instructions
open: (file, instructions) => {
このfileの中に情報として持っているのでoriginalFileとしてstateに持たせてしまう。
import React, { useRef, useState } from 'react';
useStateを呼びこんでおいて
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null);
const filePondRef = useRef(null);
const [originalFile, setOriginalFile] = useState(null); // Added
いつものuseStateを定義しつつ
open: (file, instructions) => {
setOriginalFile(file); // Added
const objectURL = URL.createObjectURL(file);
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
imageElement.cropper.replace(objectURL);
}
},
この辺にセッターをひっかけておく。こうすることで編集ボタンが押される度に編集対象のファイルの情報がoriginalFileとしてstateに保存される
そしたら、後はこの情報から抜いてきて詰めればいい
const crop = () => {
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
const canvas = imageElement.cropper.getCroppedCanvas();
canvas.toBlob(blob => {
// FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタ
ンスに追加
filePondRef.current.addFile(blob);
}, originalFile.type ); // stateを見るように変更
}
};
これで1つ問題は片付いた
デザインの問題
これは、要件にもよる。たとえばeditを押したらfilepondのフォームを全部消してcropperに置き換えたいという事にしてみようか。
まあこれは単純にflagのstateを追加してもいいんだけど、editボタンを押したらローカルのblob URLがあるのでこれを詰めこんでみる
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null);
const filePondRef = useRef(null);
const [originalFile, setOriginalFile] = useState(null);
const [cropperUrl, setCropperUrl] = useState(null); // added
としといての
open: (file, instructions) => {
setOriginalFile(file);
const objectURL = URL.createObjectURL(file);
setCropperUrl(objectURL); // objectURLを状態にセット
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
imageElement.cropper.replace(objectURL);
}
},
とする。そしてそれに応じて
{cropperUrl &&
<>
<Cropper
ref={cropperRef}
src={cropperUrl}
style={{ height: 300, width: '100%' }}
aspectRatio={1} // force square
/>
<PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
</>
}
などする。また編集が終わったときは
const crop = () => {
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
const canvas = imageElement.cropper.getCroppedCanvas();
canvas.toBlob(blob => {
// FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタ
ンスに追加
filePondRef.current.addFile(blob);
// オブジェクトURLをクリア
if (cropperUrl) {
URL.revokeObjectURL(cropperUrl); // メモリを開放
setCropperUrl(null); // cropperUrlの状態をクリア
}
}, originalFile.type);
}
};
こんな感じで掃除しておく。
また、croppingに集中したいという場合オリジナルのフォーム自体が不要だろう。
<div style={{ display: cropperUrl ? 'none' : 'block' }}>
<FilePond
ref={filePondRef}
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor}
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</div>
こっちはrefが残っていて欲しいのでdivで制御した。まあ統一感あってもいいかもしれませんがね…
さらに改造する
ここからはおまけ的なモーダル改造
npm install react-modal
ずいずいっと追加
import Modal from 'react-modal'; // Added
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null);
const filePondRef = useRef(null);
const [originalFile, setOriginalFile] = useState(null);
const [cropperUrl, setCropperUrl] = useState(null);
const [modalIsOpen, setIsOpen] = useState(false); // Added
むー、面倒になってきたので全文貼って終わり
import React, { useRef, useState, useEffect } from 'react';
import { Head, useForm } from '@inertiajs/react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { FilePond, registerPlugin } from 'react-filepond';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import FilePondPluginImageEdit from 'filepond-plugin-image-edit';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
import Cropper from 'react-cropper';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
import 'cropperjs/dist/cropper.css';
registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginImagePreview,
FilePondPluginImageEdit,
);
import Modal from 'react-modal';
export default function FileCropsIndex({ auth, avatarExists }) {
const cropperRef = useRef(null);
const filePondRef = useRef(null);
const [originalFile, setOriginalFile] = useState(null);
const [cropperUrl, setCropperUrl] = useState(null);
const [modalIsOpen, setIsOpen] = useState(false);
useEffect(() => {
Modal.setAppElement('#app');
}, []);
const {
data, setData, processing, reset, post,
} = useForm({
files: [],
});
const crop = () => {
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
const canvas = imageElement.cropper.getCroppedCanvas();
canvas.toBlob((blob) => {
// FilePondの`addFile`メソッドを使用して、編集後のBlobをFilePondインスタンスに追加
filePondRef.current.addFile(blob);
// オブジェクトURLをクリア
if (cropperUrl) {
URL.revokeObjectURL(cropperUrl); // メモリを開放
setCropperUrl(null); // cropperUrlの状態をクリア
}
}, originalFile.type);
}
setIsOpen(false);
};
const submit = (e) => {
e.preventDefault();
post(route('fcrops.store'));
};
const handleFilePondUpdate = (fileItems) => {
setData('files', fileItems.map((fileItem) => fileItem.file));
};
const editor = {
// Called by FilePond to edit the image
// - should open your image editor
// - receives file object and image edit instructions
open: (file, instructions) => {
setOriginalFile(file);
const objectURL = URL.createObjectURL(file);
setCropperUrl(objectURL); // objectURLを状態にセット
const imageElement = cropperRef.current;
if (imageElement && imageElement.cropper) {
imageElement.cropper.replace(objectURL);
}
setIsOpen(true);
},
// Callback set by FilePond
// - should be called by the editor when user confirms editing
// - should receive output object, resulting edit information
onconfirm: (output) => {},
// Callback set by FilePond
// - should be called by the editor when user cancels editing
oncancel: () => {},
// Callback set by FilePond
// - should be called by the editor when user closes the editor
onclose: () => {},
};
const openModal = () => {
setIsOpen(true)
}
const closeModal = () => {
setIsOpen(false)
if (cropperUrl) {
URL.revokeObjectURL(cropperUrl);
setCropperUrl(null);
}
setOriginalFile(null);
}
return (
<AuthenticatedLayout
user={auth.user}
header={(
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
FilePond Crop
</h2>
)}
>
<Head title="FilePond Crop" />
<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div className="p-6 text-gray-900">
<form onSubmit={submit}>
<div style={{ display: cropperUrl ? 'none' : 'block' }}>
<FilePond
ref={filePondRef}
name="files"
onupdatefiles={handleFilePondUpdate}
imageEditEditor={editor}
/>
<PrimaryButton disabled={processing}>Send</PrimaryButton>
</div>
</form>
{avatarExists && (
<img src={`${route('fcrops.avatar')}?t=${Date.now()}`} alt="Avatar" />
)}
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Cropping"
>
<Cropper
ref={cropperRef}
src={cropperUrl}
className="h-1/3 sm:h-1/2 md:h-2/3 lg:h-3/4" // 画面の高さに対する割合で高さを設定
aspectRatio={1}
/>
<div className="my-2">
<PrimaryButton type="button" onClick={crop}>Crop</PrimaryButton>
<SecondaryButton className="ml-2" onClick={closeModal}>Cancel</SecondaryButton>
</div>
</Modal>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
ちなみに、note.comの場合react-easy-cropとか使ってるんだけど、基本的にはこの手法でfilepondにつっこめば連携できるんじゃあないかな?(さすがにくたびれました)
この記事が気に入ったらサポートをしてみませんか?