見出し画像

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ハードコードが実にダセえけど、これは後で修正するとして、うまいこといくと、以下のような動きになるだろう。


crop確定ボタンが見えてないけど、まあ押したっつーことで

実際に送信して確認する

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につっこめば連携できるんじゃあないかな?(さすがにくたびれました)




この記事が気に入ったらサポートをしてみませんか?