Hi,
I need to add a "slideshow" field.
Is there any field for adding multiple images or should I do it with custom fields?
Thank you.
For now you'll need to do a custom field, or create a Slide model, and a Slideshow model, then have a relationship between them, that way slides can be shared across sliders for example
Hi @joaojoyce ,
I've been thinking about this for a while now. It seems simple, but it's not. You would probably want to upload a bunch of images and reorder them, right? The fact that HTML5 does not allow you to manipulate the <input type="file"> complicates things. So you'd think "ok, I'll use javascript, something like Dropzone". But you can't do that on CREATE, because there's no ID for the entry yet, so you can't upload the images using AJAX and store them, because you'd have no reference to the new entry. And storing them in a temp folder and then assigning them would definitely become unmanageable for big projects.
So what I ended up doing for a project:
1) On CREATE, I used the upload_multiple field type;
2) On UPDATE, I created a "dropzone" field type, which also allows you to reorder the images and add more images; It's definitely not perfect and some things may be hard-coded for a field named "images", but if you're going for the same thing, it should help a lot.
Cheers!
Dropzone field:
<div class="form-group col-md-12">
<strong>{{ $field['label'] }}</strong> <br>
<div class="dropzone sortable dz-clickable sortable">
<div class="dz-message">
Drop files here or click to upload.
</div>
@if ($entry->{$field['name']})
@foreach($entry->{$field['name']} as $key => $image)
<div class="dz-preview" data-id="{{ $key }}" data-path="{{ $image }}">
<img class="dropzone-thumbnail" src={{ asset($image) }}>
<a class="dz-remove" href="javascript:void(0);" data-remove="{{ $key }}" data-path="{{ $image }}">Remove file</a>
</div>
@endforeach
@endif
</div>
</div>
@if ($crud->checkIfFieldIsFirstOfItsType($field, $fields))
{{-- FIELD EXTRA CSS --}}
{{-- push things in the after_styles section --}}
@push('crud_fields_styles')
<style>
.sortable { list-style-type: none; margin: 0; padding: 0; width: 100%; overflow: auto;}
/*border: 1px SOLID #000;*/
.sortable { margin: 3px 3px 3px 0; padding: 1px; float: left; /*width: 120px; height: 120px;*/ vertical-align:bottom; text-align: center; }
.dropzone-thumbnail { width: 115px; cursor: move!important; }
.dz-preview { cursor: move !important; }
</style>
@endpush
{{-- FIELD EXTRA JS --}}
{{-- push things in the after_scripts section --}}
@push('crud_fields_scripts')
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script>
<link rel="stylesheet" href="https://rawgit.com/enyo/dropzone/master/dist/dropzone.css">
<script>
Dropzone.autoDiscover = false;
var uploaded = false;
var dropzone = new Dropzone(".dropzone", {
url: "{{ url($crud->route.'/'.$entry->id.'/'.$field['upload_route']) }}",
paramName: '{{ $field['name'] }}',
uploadMultiple: true,
acceptedFiles: "{{ $field['mimes'] }}",
addRemoveLinks: true,
// autoProcessQueue: false,
maxFilesize: {{ $field['filesize'] }},
parallelUploads: 10,
// previewTemplate:
sending: function(file, xhr, formData) {
formData.append("_token", $('[name=_token').val());
formData.append("id", {{ $entry->id }});
},
error: function(file, response) {
console.log('error');
console.log(file)
console.log(response)
$(file.previewElement).find('.dz-error-message').remove();
$(file.previewElement).remove();
$(function(){
new PNotify({
title: file.name+" was not uploaded!",
text: response,
type: "error",
icon: false
});
});
},
success : function(file, status) {
console.log('success');
// clear the images in the dropzone
$('.dropzone').empty();
// repopulate the dropzone with all images (new and old)
$.each(status.images, function(key, image_path) {
$('.dropzone').append('<div class="dz-preview" data-id="'+key+'" data-path="'+image_path+'"><img class="dropzone-thumbnail" src="{{ url('') }}/'+image_path+'" /><a class="dz-remove" href="javascript:void(0);" data-remove="'+key+'" data-path="'+image_path+'">Remove file</a></div>');
});
var notification_type;
if (status.success) {
notification_type = 'success';
} else {
notification_type = 'error';
}
new PNotify({
text: status.message,
type: notification_type,
icon: false
});
}
});
// Reorder images
$(".dropzone").sortable({
items: '.dz-preview',
cursor: 'move',
opacity: 0.5,
containment: '.dropzone',
distance: 20,
scroll: true,
tolerance: 'pointer',
stop: function (event, ui) {
// console.log('sortable stop');
var image_order = [];
$('.dz-preview').each(function() {
var image_id = $(this).data('id');
var image_path = $(this).data('path');
image_order.push({ id: image_id, path: image_path});
});
// console.log(image_order);
$.ajax({
url: '{{ url($crud->route.'/'.$entry->id.'/'.$field['reorder_route']) }}',
type: 'POST',
data: {
order: image_order,
entry_id: {{ $entry->id }}
},
})
.done(function(status) {
var notification_type;
if (status.success) {
notification_type = 'success';
} else {
notification_type = 'error';
}
new PNotify({
text: status.message,
type: notification_type,
icon: false
});
});
}
});
// Delete image
$(document).on('click', '.dz-remove', function () {
var image_id = $(this).data('remove');
var image_path = $(this).data('path');
$.ajax({
url: '{{ url($crud->route.'/'.$entry->id.'/'.$field['delete_route']) }}',
type: 'POST',
data: {
entry_id: {{ $entry->id }},
image_id: image_id,
image_path: image_path
},
})
.done(function(status) {
var notification_type;
if (status.success) {
notification_type = 'success';
$('div.dz-preview[data-id="'+image_id+'"]').remove();
} else {
notification_type = 'error';
}
new PNotify({
text: status.message,
type: notification_type,
icon: false
});
});
});
</script>
@endpush
@endif
I will check this solution. Thank you very much for the help @OwenMelbz and @tabacitu.
Before I knew this project I was doing my custom backoffice and solving this problem by uploading the image through AJAX and managing the ordering with a drag and drop reordering javascript plugin.
I could eventually try to import it if anyone is interested.
Huh. @joaojoyce , I understand how that works well for "update", but how about for "create"? How did you do it there? In my mind, you don't have an ID yet, so you can't store the filenames in the database.
I was pushing the image through AJAX before the CREATE.
The AJAX call would return the file name/ID of the image (for instance an MD5). On create we would only store the file names since the images were already on the server.
It was probably the unmanageable for big projects solution you were talking about.
That would definitely work for successful form submissions. But what about unsuccessful ones?
If the user quits the form before submitting (or after some validation errors) we'd end up with unused files uploaded on the server...
In another project (non backpack) I solved this by adding a concept status to every new created entry. That way you have an ID on create and you are able to attach images with f.i. dropzone. I think Wordpress also uses this approach.
You might be interested in this - it's not perfect either but is another option.
https://github.com/seandowney/laravel-backpack-gallery-crud
@tabacitu how about upload files only on form submit ?
@tiagosimoesdev I don’t think I understand… what do you mean? Afik that’s how the files are currently uploaded using the image and upload field types… on form submit.
I've tried @seandowney's gallery crud but unfortunately it seems seriously broken at this time.
<div class="form-group col-md-12"> <strong>{{ $field['label'] }}</strong> <br> <div class="dropzone sortable dz-clickable sortable"> <div class="dz-message"> Drop files here or click to upload. </div> @if ($entry->{$field['name']}) @foreach($entry->{$field['name']} as $key => $image) <div class="dz-preview" data-id="{{ $key }}" data-path="{{ $image }}"> <img class="dropzone-thumbnail" src={{ asset($image) }}> <a class="dz-remove" href="javascript:void(0);" data-remove="{{ $key }}" data-path="{{ $image }}">Remove file</a> </div> @endforeach @endif </div> </div> @if ($crud->checkIfFieldIsFirstOfItsType($field, $fields)) {{-- FIELD EXTRA CSS --}} {{-- push things in the after_styles section --}} @push('crud_fields_styles') <style> .sortable { list-style-type: none; margin: 0; padding: 0; width: 100%; overflow: auto;} /*border: 1px SOLID #000;*/ .sortable { margin: 3px 3px 3px 0; padding: 1px; float: left; /*width: 120px; height: 120px;*/ vertical-align:bottom; text-align: center; } .dropzone-thumbnail { width: 115px; cursor: move!important; } .dz-preview { cursor: move !important; } </style> @endpush {{-- FIELD EXTRA JS --}} {{-- push things in the after_scripts section --}} @push('crud_fields_scripts') <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> <script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script> <link rel="stylesheet" href="https://rawgit.com/enyo/dropzone/master/dist/dropzone.css"> <script> Dropzone.autoDiscover = false; var uploaded = false; var dropzone = new Dropzone(".dropzone", { url: "{{ url($crud->route.'/'.$entry->id.'/'.$field['upload_route']) }}", paramName: '{{ $field['name'] }}', uploadMultiple: true, acceptedFiles: "{{ $field['mimes'] }}", addRemoveLinks: true, // autoProcessQueue: false, maxFilesize: {{ $field['filesize'] }}, parallelUploads: 10, // previewTemplate: sending: function(file, xhr, formData) { formData.append("_token", $('[name=_token').val()); formData.append("id", {{ $entry->id }}); }, error: function(file, response) { console.log('error'); console.log(file) console.log(response) $(file.previewElement).find('.dz-error-message').remove(); $(file.previewElement).remove(); $(function(){ new PNotify({ title: file.name+" was not uploaded!", text: response, type: "error", icon: false }); }); }, success : function(file, status) { console.log('success'); // clear the images in the dropzone $('.dropzone').empty(); // repopulate the dropzone with all images (new and old) $.each(status.images, function(key, image_path) { $('.dropzone').append('<div class="dz-preview" data-id="'+key+'" data-path="'+image_path+'"><img class="dropzone-thumbnail" src="{{ url('') }}/'+image_path+'" /><a class="dz-remove" href="javascript:void(0);" data-remove="'+key+'" data-path="'+image_path+'">Remove file</a></div>'); }); var notification_type; if (status.success) { notification_type = 'success'; } else { notification_type = 'error'; } new PNotify({ text: status.message, type: notification_type, icon: false }); } }); // Reorder images $(".dropzone").sortable({ items: '.dz-preview', cursor: 'move', opacity: 0.5, containment: '.dropzone', distance: 20, scroll: true, tolerance: 'pointer', stop: function (event, ui) { // console.log('sortable stop'); var image_order = []; $('.dz-preview').each(function() { var image_id = $(this).data('id'); var image_path = $(this).data('path'); image_order.push({ id: image_id, path: image_path}); }); // console.log(image_order); $.ajax({ url: '{{ url($crud->route.'/'.$entry->id.'/'.$field['reorder_route']) }}', type: 'POST', data: { order: image_order, entry_id: {{ $entry->id }} }, }) .done(function(status) { var notification_type; if (status.success) { notification_type = 'success'; } else { notification_type = 'error'; } new PNotify({ text: status.message, type: notification_type, icon: false }); }); } }); // Delete image $(document).on('click', '.dz-remove', function () { var image_id = $(this).data('remove'); var image_path = $(this).data('path'); $.ajax({ url: '{{ url($crud->route.'/'.$entry->id.'/'.$field['delete_route']) }}', type: 'POST', data: { entry_id: {{ $entry->id }}, image_id: image_id, image_path: image_path }, }) .done(function(status) { var notification_type; if (status.success) { notification_type = 'success'; $('div.dz-preview[data-id="'+image_id+'"]').remove(); } else { notification_type = 'error'; } new PNotify({ text: status.message, type: notification_type, icon: false }); }); }); </script> @endpush @endif
Hello, could you please also post sample code how to add this dropzone field in CrudController. I tried like this
$this->crud->addField([
'name' => 'film_photos',
'label' => 'Изображения',
'type' => 'dropzone',
]);
But it gives error "Undefined variable: entry"
Using Backpack 3.5
I presume in Backpack 3.5 I should use $crud->request instead of $entry
@Jakhongir91 this should work:
$this->crud->addField([
'name' => 'film_photos',
'label' => 'Изображения',
'type' => 'dropzone',
], 'update');
Notice it would only be added on "update", where $entry exists.
Most helpful comment
You might be interested in this - it's not perfect either but is another option.
https://github.com/seandowney/laravel-backpack-gallery-crud