I've only done a single but multiple croppers just work on different elements. You would have to add a listener to the parent containing all cropper elements and take it from there.
Upon cropping I let cropperjs put the base64 encoded image on a hidden input and simply decode it on the server side.
I'll copy some here for you (still in dev, unpolished)
The image name and file location are also inputs to allow me to store a reference to the file as well as clean up the filename. See the job handler at the end
@extends('catalog::layout.app')
@section('title', 'Catalog Image Form')
@section('stylesheets')
<link href="{{ asset('public/assets/collection/collection.css') }}" rel="stylesheet" type="text/css" media="all">
<link href="{{ asset('public/assets/catalog/css/custom.css') }}" rel="stylesheet" type="text/css" media="all">
<link href="{{ asset('public/assets/core/cropper.min.css') }}" rel="stylesheet" type="text/css" media="all">
@endsection
@section('content')
<div class="container">
<div class="card mt-5">
<div class="card-header">
<h5>
<i class="fa fa-pencil position-left"></i>
<span class="text-semibold">{{ is_object($data) ? 'Edit' : 'Add' }}</span> Image:
</h5>
<div class="heading-elements">
<a class="btn btn-warning btn-xs" role="button" title="Cancel" href="{{ route('image.index') }}">
<i class="fa fa-reply"></i>
</a>
</div>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="col-12">
<form id="form" method="POST"
action="{{ is_object($data) ? route('image.update', $data->id) : route('image.store') }}"
enctype="multipart/form-data"
class="steps-validation">
@if(is_object($data))
{{ method_field('PUT') }}
@endif
{{ csrf_field() }}
<div class="row align-items-center">
<fieldset class="col-md-5 col-xs-12 card">
<div class="btn-group btn-block">
<a class="btn btn-danger" role="button" title="Cancel" href="{{ route('image.index') }}">Cancel</a>
<button form="form" type="submit" class="btn btn-success" role="button" title="Save">Save</button>
</div>
<div class="form-group mt-3">
<div class="custom-file">
<label class="custom-file-label" for="location">{{ $data->location ?? old('image.location') ?? 'Choose file' }}</label>
<input type="file"
name="image[file]"
class="custom-file-input form-control"
id="location">
<input type="hidden"
name="image[location]"
value="{{ $data->location ?? old('image.location') }}">
<input type="hidden"
name="image[crop]"
value="">
</div>
</div>
<div class="form-group" data-select="link" data-collection="lookup">
<input type="text"
placeholder="add link"
class="form-control">
</div>
<div class="form-group mt-5 mb-5">
<ul id="link-list" class="list-group list-group-flush">
<li class="list-group-item active">Linked To</li>
@if(is_object($data))
@foreach($data->links as $link)
<li class="list-group-item d-flex justify-content-between align-items-center deletable">
<span class="badge badge-{{ $link->item === 'product' ? 'primary' : 'dark' }}">{{ $link->item }}</span>
<span class="btn-block">{{ $link->title }}</span>
<span class="btn btn-sm btn-danger delete"><i class="fa fa-trash"></i></span>
<input type="hidden" name="links[{{ $link->item }}][]" value="{{ $link->id }}">
</li>
@endforeach
@endif
</ul>
</div>
<div class="form-group">
<label class="col-form-label" for="status">Status</label>
<input id="status" type="number"
name="image[status]"
value="{{ $data->status ?? old('image.status') }}"
class="form-control">
</div>
</fieldset>
<div class="col-md-7 col-xs-12">
<div id="controls" class="btn-group btn-group-justified mb-5">
{{--<button class="btn btn-outline-danger" data-method="disable" title="Disable"><span class="fa fa-lock"></span></button>--}}
{{--<button class="btn btn-outline-success" data-method="enable" title="Enable"><span class="fa fa-unlock"></span></button>--}}
<button class="btn btn-outline-primary" data-method="reset" title="Reset"><span class="fa fa-refresh"></span></button>
</div>
<div id="preview">
<img src="{{ isset($data->location)
? asset('public/assets/catalog/img/item/'.$data->location)
: 'https://via.placeholder.com/400x600.png?text=Empty' }}" alt="">
</div>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script src="{{ asset('public/assets/core/cropper.min.js')}}"></script>
<script type="module">
const fileInput = document.getElementById('location'),
preview = document.getElementById('preview'),
input = document.querySelector('input[name="image[crop]"]'),
controls = document.getElementById('controls');
let cropper;
const crop = () =>
{
const set = () =>
{
input.value = cropper.getCroppedCanvas(
{
width: 400,
height: 600,
// fillColor: '#fff',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
}).toDataURL("image/png", 100);
};
const image = preview.querySelector('img');
cropper = new Cropper(image,
{
minContainerWidth: 404,
minContainerHeight: 606,
minCropBoxWidth: 404,
minCropBoxHeight: 606,
wheelZoomRatio: 0.1,
autoCropArea: 1,
ready: event =>
{
set();
}
});
image.addEventListener('cropend', (event) =>
{
set();
});
image.addEventListener('cropmove', (event) =>
{
set();
});
image.addEventListener('zoom', (event) =>
{
set();
});
};
crop();
fileInput.addEventListener('change', event =>
{
const fileName = event.target.files[0].name;
event.target.previousElementSibling.innerText = fileName;
event.target.nextElementSibling.value = fileName;
preview.innerHTML = `<img alt="" src="${URL.createObjectURL(event.target.files[0])}">`;
// cropper.destroy();
setTimeout(() =>
{
crop();
}, 300);
});
controls.addEventListener('click', e =>
{
e.preventDefault();
const method = e.target.getAttribute('data-method');
switch (method)
{
case 'reset':
cropper.reset();
break;
case 'enable':
cropper.enable();
break;
case 'disable':
cropper.disable();
break;
}
}, false);
</script>
<script type="module">
import List from "{{ asset('public/assets/collection/markup/item.js')}}";
import suggest from "{{ asset('public/assets/collection/templates/link.js')}}";
import action from "{{ asset('public/assets/collection/events/action-link.js')}}";
(new List('{{ route('catalog-items') }}'))
.renderWhen('link', 5)
.template(suggest,
{
action: action,
title: ['title', 'item']
});
</script>
@endsection
and the backend which is a job
<?php namespace Extend\Catalog\Domains\Image;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Lucid\Foundation\Job;
class MoveImageJob extends Job
{
/**
* @var UploadedFile $file
*/
private $file, $crop, $location;
public function __construct($file, $crop, $location)
{
$this->file = $file;
$this->crop = $crop;
$this->location = $location;
}
public function handle(Filesystem $filesystem)
{
try
{
if ($this->file || $this->crop)
{
$file = $this->crop ? base64_decode(str_replace('data:image/png;base64,', '', $this->crop)) : null;
if (!$file && $this->file instanceof UploadedFile)
$file = $this->file->get();
if (!$file)
return false;
$fileName = strtolower(str_replace(' ', '-', $this->location));
$filesystem->put(
config('catalog.disks.archive.folder') . $fileName, $file);
return $fileName;
}
}
catch (\Exception $e)
{
logger($e->getTraceAsString());
}
}
}