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