Initial release of Laravel Cropper package
This commit is contained in:
commit
e86010bb75
22
composer.json
Normal file
22
composer.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "takeone/cropper",
|
||||
"description": "Reusable Laravel image cropper using Cropme",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Takeone\\Cropper\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Takeone\\Cropper\\CropperServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0"
|
||||
}
|
||||
}
|
||||
42
src/CropperServiceProvider.php
Normal file
42
src/CropperServiceProvider.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Takeone\Cropper;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class CropperServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
// 1. Load Views from the package
|
||||
$this->loadViewsFrom(__DIR__.'/resources/views', 'takeone');
|
||||
|
||||
// 2. Register Blade Component Alias
|
||||
Blade::component('takeone::components.widget', 'takeone-cropper');
|
||||
|
||||
// 3. Register Routes Automatically
|
||||
$this->registerRoutes();
|
||||
|
||||
// 4. Allow publishing views if user wants to customize
|
||||
$this->publishes([
|
||||
__DIR__.'/resources/views' => resource_path('views/vendor/takeone'),
|
||||
], 'takeone-cropper-views');
|
||||
}
|
||||
|
||||
protected function registerRoutes()
|
||||
{
|
||||
Route::group([
|
||||
'middleware' => ['web'],
|
||||
], function () {
|
||||
// We use the full absolute namespace here to avoid any "not found" errors
|
||||
Route::post('/image-upload', [\takeone\cropper\Http\Controllers\ImageController::class, 'upload'])->name('image.upload');
|
||||
});
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
43
src/Http/Controllers/ImageController.php
Normal file
43
src/Http/Controllers/ImageController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Takeone\Cropper\Http\Controllers;
|
||||
|
||||
// We use the base Laravel controller
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required',
|
||||
'folder' => 'required|string',
|
||||
'filename' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
$folder = trim($request->folder, '/');
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Store in the public disk (storage/app/public)
|
||||
Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $fullPath,
|
||||
'url' => asset('storage/' . $fullPath)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/resources/views/components/widget.blade.php
Normal file
153
src/resources/views/components/widget.blade.php
Normal file
@ -0,0 +1,153 @@
|
||||
@php
|
||||
$id = $attributes->get('id');
|
||||
$width = $attributes->get('width', 300);
|
||||
$height = $attributes->get('height', 300);
|
||||
$shape = $attributes->get('shape', 'circle');
|
||||
$folder = $attributes->get('folder', 'uploads');
|
||||
$filename = $attributes->get('filename', 'cropped_' . time());
|
||||
@endphp
|
||||
|
||||
@once
|
||||
<link rel="stylesheet" href="https://unpkg.com/cropme@1.4.1/dist/cropme.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://unpkg.com/cropme@1.4.1/dist/cropme.min.js"></script>
|
||||
<style>
|
||||
.modal-content-clean { border: none; border-radius: 15px; overflow: hidden; }
|
||||
.cropme-wrapper { overflow: hidden !important; border-radius: 8px; }
|
||||
.cropme-slider { display: none !important; }
|
||||
.takeone-canvas {
|
||||
height: 400px;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
.custom-slider-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.form-range::-webkit-slider-thumb { background: #198754; }
|
||||
.form-range::-moz-range-thumb { background: #198754; }
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<button type="button" class="btn btn-success px-4 fw-bold shadow-sm" data-bs-toggle="modal" data-bs-target="#cropperModal_{{ $id }}">
|
||||
Change Photo
|
||||
</button>
|
||||
|
||||
@push('modals')
|
||||
<div class="modal fade" id="cropperModal_{{ $id }}" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content modal-content-clean shadow-lg">
|
||||
<div class="modal-body p-4 text-start">
|
||||
<div class="mb-3 d-flex align-items-center">
|
||||
<input type="file" id="input_{{ $id }}" class="form-control form-control-sm" accept="image/*">
|
||||
<button type="button" class="btn-close ms-2" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div id="box_{{ $id }}" class="takeone-canvas"></div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="custom-slider-label d-block mb-2">Zoom Level</label>
|
||||
<input type="range" class="form-range" id="zoom_{{ $id }}" min="0" max="100" step="1" value="0">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="custom-slider-label d-block mb-2">Rotation</label>
|
||||
<input type="range" class="form-range" id="rot_{{ $id }}" min="-180" max="180" step="1" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="button" class="btn btn-success btn-lg fw-bold py-3" id="save_{{ $id }}">
|
||||
Crop & Save Image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
let cropper_{{ $id }} = null;
|
||||
const el_{{ $id }} = document.getElementById("box_{{ $id }}");
|
||||
const zoomMin_{{ $id }} = 0.01;
|
||||
const zoomMax_{{ $id }} = 3;
|
||||
|
||||
function applyTransform_{{ $id }}(instance) {
|
||||
if (!instance.properties.image) return;
|
||||
const p = instance.properties;
|
||||
const t = `translate3d(${p.x}px, ${p.y}px, 0) scale(${p.scale}) rotate(${p.deg}deg)`;
|
||||
p.image.style.transform = t;
|
||||
}
|
||||
|
||||
$('#input_{{ $id }}').on('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
if (cropper_{{ $id }}) cropper_{{ $id }}.destroy();
|
||||
|
||||
cropper_{{ $id }} = new Cropme(el_{{ $id }}, {
|
||||
container: { width: '100%', height: 400 },
|
||||
viewport: {
|
||||
width: {{ $width }},
|
||||
height: {{ $height }},
|
||||
type: '{{ $shape }}',
|
||||
border: { enable: true, width: 2, color: '#fff' }
|
||||
},
|
||||
transformOrigin: 'viewport',
|
||||
zoom: { min: zoomMin_{{ $id }}, max: zoomMax_{{ $id }}, enable: true, mouseWheel: true, slider: false },
|
||||
rotation: { enable: true, slider: false }
|
||||
});
|
||||
|
||||
cropper_{{ $id }}.bind({ url: event.target.result }).then(() => {
|
||||
$('#zoom_{{ $id }}').val(0);
|
||||
$('#rot_{{ $id }}').val(0);
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$('#zoom_{{ $id }}').on('input', function() {
|
||||
if (!cropper_{{ $id }} || !cropper_{{ $id }}.properties.image) return;
|
||||
const p = parseFloat($(this).val());
|
||||
const scale = zoomMin_{{ $id }} + (zoomMax_{{ $id }} - zoomMin_{{ $id }}) * (p / 100);
|
||||
cropper_{{ $id }}.properties.scale = Math.min(Math.max(scale, zoomMin_{{ $id }}), zoomMax_{{ $id }});
|
||||
applyTransform_{{ $id }}(cropper_{{ $id }});
|
||||
});
|
||||
|
||||
$('#rot_{{ $id }}').on('input', function() {
|
||||
if (cropper_{{ $id }}) {
|
||||
cropper_{{ $id }}.rotate(parseInt($(this).val(), 10));
|
||||
}
|
||||
});
|
||||
|
||||
$('#save_{{ $id }}').on('click', function() {
|
||||
if (!cropper_{{ $id }}) return;
|
||||
const btn = $(this);
|
||||
btn.prop('disabled', true).text('Uploading...');
|
||||
|
||||
cropper_{{ $id }}.crop({ type: 'base64' }).then(base64 => {
|
||||
$.post("{{ route('image.upload') }}", {
|
||||
_token: "{{ csrf_token() }}",
|
||||
image: base64,
|
||||
folder: '{{ $folder }}',
|
||||
filename: '{{ $filename }}'
|
||||
}).done((res) => {
|
||||
alert('Saved successfully!');
|
||||
$('#cropperModal_{{ $id }}').modal('hide');
|
||||
}).fail((err) => {
|
||||
alert('Upload failed.');
|
||||
}).always(() => {
|
||||
btn.prop('disabled', false).text('Crop & Save Image');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
Loading…
x
Reference in New Issue
Block a user