Skip to content

Instantly share code, notes, and snippets.

@saqueib
Last active June 9, 2021 15:57
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save saqueib/317216a4d0143fc19990e0bf43dfdb41 to your computer and use it in GitHub Desktop.
Save saqueib/317216a4d0143fc19990e0bf43dfdb41 to your computer and use it in GitHub Desktop.
Reusable upload component in Laravel with Dropzone.js - visit http://wp.me/p8cmxL-9Q for tutorial
@php $dropzoneId = isset($dz_id) ? $dz_id : str_random(8); @endphp
<div id="{{$dropzoneId}}" class="dropzone">
<div class="dz-default dz-message">
<h3>{{ $title or 'Drop files here or click to upload.'}}</h3>
<p class="text-muted">{{ $desc or 'Any related files you can upload' }} <br>
<small>One file can be max {{ config('attachment.max_size', 0) / 1000 }} MB</small></p>
</div>
</div>
<!-- Dropzone {{ $dropzoneId }} -->
@push('scripts')
<script>
// Turn off auto discovery
Dropzone.autoDiscover = false;
$(function () {
// Attach dropzone on element
$("#{{ $dropzoneId }}").dropzone({
url: "{{ route('attachments.store') }}",
addRemoveLinks: true,
maxFilesize: {{ isset($maxFileSize) ? $maxFileSize : config('attachment.max_size', 1000) / 1000 }},
acceptedFiles: "{!! isset($acceptedFiles) ? $acceptedFiles : config('attachment.allowed') !!}",
headers: {'X-CSRF-TOKEN': "{{ csrf_token() }}"},
params: {!! isset($params) ? json_encode($params) : '{}' !!},
init: function () {
// uploaded files
var uploadedFiles = [];
@if(isset($uploadedFiles) && count($uploadedFiles))
// show already uploaded files
uploadedFiles = {!! json_encode($uploadedFiles) !!};
var self = this;
uploadedFiles.forEach(function (file) {
// Create a mock uploaded file:
var uploadedFile = {
name: file.filename,
size: file.size,
type: file.mime,
dataURL: file.url
};
// Call the default addedfile event
self.emit("addedfile", uploadedFile);
// Image? lets make thumbnail
if( file.mime.indexOf('image') !== -1) {
self.createThumbnailFromUrl(
uploadedFile,
self.options.thumbnailWidth,
self.options.thumbnailHeight,
self.options.thumbnailMethod,
true, function(thumbnail) {
self.emit('thumbnail', uploadedFile, thumbnail);
});
} else {
// we can get the icon for file type
self.emit("thumbnail", uploadedFile, getIconFromFilename(uploadedFile));
}
// fire complete event to get rid of progress bar etc
self.emit("complete", uploadedFile);
})
@endif
// Handle added file
this.on('addedfile', function(file) {
var thumb = getIconFromFilename(file);
$(file.previewElement).find(".dz-image img").attr("src", thumb);
})
// handle remove file to delete on server
this.on("removedfile", function (file) {
// try to find in uploadedFiles
var found = uploadedFiles.find(function (item) {
// check if filename and size matched
return (item.filename === file.name) && (item.size === file.size);
})
// If got the file lets make a delete request by id
if( found ) {
$.ajax({
url: "/attachments/" + found.id,
type: 'DELETE',
headers: {
'X-CSRF-TOKEN': "{{ csrf_token() }}"
},
success: function(response) {
console.log('deleted');
}
});
}
});
// Handle errors
this.on('error', function(file, response) {
var errMsg = response;
if( response.message ) errMsg = response.message;
if( response.file ) errMsg = response.file[0];
$(file.previewElement).find('.dz-error-message').text(errMsg);
});
}
});
})
// Get Icon for file type
function getIconFromFilename(file) {
// get the extension
var ext = file.name.split('.').pop().toLowerCase();
// if its not an image
if( file.type.indexOf('image') === -1 ) {
// handle the alias for extensions
if(ext === 'docx') {
ext = 'doc'
} else if (ext === 'xlsx') {
ext = 'xls'
}
return "/images/icon/"+ext+".svg";
}
// return a placeholder for other files
return '/images/icon/txt.svg';
}
</script>
@endpush
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Attachment extends Model
{
protected $guarded = [];
protected $appends = ['url'];
public function attachable()
{
return $this->morphTo();
}
public function getUrlAttribute()
{
return Storage::url($this->uid);
}
public static function boot()
{
parent::boot();
static::deleting(function($attachment) {
// delete associated file from storage
Storage::disk('public')->delete($attachment->uid);
});
}
}
<?php
return [
// Allowed file types with . prefix
'allowed' => '.pdf,.doc,.xls,.docx,.xlsx,.jpg,.png,.gif,.jpeg',
// Max file size in KB
'max_size' => 5000
];
<?php
namespace App\Http\Controllers;
use App\Attachment;
use Illuminate\Http\Request;
class AttachmentController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$request->validate([
'file' => 'required|file|max:5000|mimes:' . $this->getAllowedFileTypes(),
'attachable_id' => 'required|integer',
'attachable_type' => 'required',
]);
// save the file
if ($fileUid = $request->file->store('/upload', 'public')) {
return Attachment::create([
'filename' => $request->file->getClientOriginalName(),
'uid' => $fileUid,
'size' => $request->file->getClientSize(),
'mime' => $request->file->getMimeType(),
'attachable_id' => $request->get('attachable_id'),
'attachable_type' => $request->get('attachable_type'),
]);
}
return response(['msg' => 'Unable to upload your file.'], 400);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Attachment $attachment
* @return \Illuminate\Http\Response
*/
public function destroy(Attachment $attachment)
{
return (string) $attachment->delete();
}
/**
* Remove . prefix so laravel validator can use allowed files
*
* @return string
*/
private function getAllowedFileTypes()
{
return str_replace('.', '', config('attachment.allowed', ''));
}
}
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAttachmentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('attachments', function (Blueprint $table) {
$table->increments('id');
$table->string('filename');
$table->string('uid');
$table->integer('size');
$table->string('mime', 100);
$table->morphs('attachable');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('attachments');
}
}
/*
* The MIT License
* Copyright (c) 2012 Matias Meno <m@tias.me>
*/
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
@mixin keyframes($name) {
@-webkit-keyframes #{$name} {
@content;
}
@-moz-keyframes #{$name} {
@content;
}
@keyframes #{$name} {
@content;
}
}
@mixin prefix($map, $vendors: webkit moz ms o) {
@each $prop, $value in $map {
@if $vendors {
@each $vendor in $vendors {
#{"-" + $vendor + "-" + $prop}: #{$value};
}
}
// Dump regular property anyway
#{$prop}: #{$value};
}
}
@include keyframes(passing-through) {
0% {
opacity: 0;
@include prefix((transform: translateY(40px)));
}
30%, 70% {
opacity: 1;
@include prefix((transform: translateY(0px)));
}
100% {
opacity: 0;
@include prefix((transform: translateY(-40px)));
}
}
@include keyframes(slide-in) {
0% {
opacity: 0;
@include prefix((transform: translateY(40px)));
}
30% {
opacity: 1;
@include prefix((transform: translateY(0px)));
}
}
@include keyframes(pulse) {
0% { @include prefix((transform: scale(1))); }
10% { @include prefix((transform: scale(1.1))); }
20% { @include prefix((transform: scale(1))); }
}
.dropzone, .dropzone * {
box-sizing: border-box;
}
.dropzone {
$image-size: 120px;
$image-border-radius: 10px;
&.dz-clickable {
cursor: pointer;
* {
cursor: default;
}
.dz-message {
&, * {
cursor: pointer;
}
}
}
min-height: 250px;
border: 1px dashed rgba($brand-primary, 0.6);
background: white;
padding: 20px 20px;
border-radius: $image-border-radius;
&.dz-started {
.dz-message {
display: none;
}
}
&.dz-drag-hover {
border-color: $brand-primary;
.dz-message {
opacity: 0.5;
transform: scale(1.2);
}
}
.dz-message {
text-align: center;
margin-top: 5em;
transform: scale(1);
transition: all ease 0.3s;
h3 {
font-weight: bold;
}
}
.dz-preview {
position: relative;
display: inline-block;
vertical-align: top;
margin: 16px;
min-height: 100px;
&:hover {
// Making sure that always the hovered preview element is on top
z-index: 1000;
.dz-details {
opacity: 1;
}
}
&.dz-file-preview {
.dz-image {
border-radius: $image-border-radius;
background: #999;
background: linear-gradient(to bottom, #eee, #ddd);
}
.dz-details {
opacity: 1;
}
}
&.dz-image-preview {
background: white;
.dz-details {
@include prefix((transition: opacity 0.2s linear));
}
}
.dz-remove {
font-size: 14px;
text-align: center;
display: block;
cursor: pointer;
border: none;
&:hover {
text-decoration: underline;
}
}
&:hover .dz-details {
opacity: 1;
}
.dz-details {
$background-color: #444;
z-index: 20;
position: absolute;
top: 0;
left: 0;
opacity: 0;
font-size: 13px;
min-width: 100%;
max-width: 100%;
padding: 2em 1em;
text-align: center;
color: rgba(0, 0, 0, 0.9);
$width: 120px;
line-height: 150%;
.dz-size {
margin-bottom: 1em;
font-size: 16px;
}
.dz-filename {
white-space: nowrap;
&:hover {
span {
border: 1px solid rgba(200, 200, 200, 0.8);
background-color: rgba(255, 255, 255, 0.8);
}
}
&:not(:hover) {
span {
border: 1px solid transparent;
}
overflow: hidden;
text-overflow: ellipsis;
}
}
.dz-filename, .dz-size {
span {
background-color: rgba(255, 255, 255, 0.4);
padding: 0 0.4em;
border-radius: 3px;
}
}
}
&:hover {
.dz-image {
// opacity: 0.8;
img {
@include prefix((transform: scale(1.05, 1.05))); // Getting rid of that white bleed-in
@include prefix((filter: blur(8px)), webkit); // Getting rid of that white bleed-in
}
}
}
.dz-image {
border-radius: $image-border-radius;
overflow: hidden;
width: $image-size;
height: $image-size;
position: relative;
display: block;
z-index: 10;
img {
display: block;
}
}
&.dz-success {
.dz-success-mark {
@include prefix((animation: passing-through 3s cubic-bezier(0.770, 0.000, 0.175, 1.000)));
svg path {
fill: $brand-success;
}
}
}
&.dz-error {
.dz-error-mark {
opacity: 1;
@include prefix((animation: slide-in 3s cubic-bezier(0.770, 0.000, 0.175, 1.000)));
svg g {
fill: $brand-danger;
}
}
}
.dz-success-mark, .dz-error-mark {
$image-height: 54px;
$image-width: 54px;
pointer-events: none;
opacity: 0;
z-index: 500;
position: absolute;
display: block;
top: 50%;
left: 50%;
margin-left: -($image-width/2);
margin-top: -($image-height/2);
svg {
display: block;
width: $image-width;
height: $image-height;
}
}
&.dz-processing .dz-progress {
opacity: 1;
@include prefix((transition: all 0.2s linear));
}
&.dz-complete .dz-progress {
opacity: 0;
@include prefix((transition: opacity 0.4s ease-in));
}
&:not(.dz-processing) {
.dz-progress {
@include prefix((animation: pulse 6s ease infinite));
}
}
.dz-progress {
opacity: 1;
z-index: 1000;
pointer-events: none;
position: absolute;
height: 16px;
left: 50%;
top: 50%;
margin-top: -8px;
width: 80px;
margin-left: -40px;
// border: 2px solid #333;
background: rgba(255, 255, 255, 0.9);
// Fix for chrome bug: https://code.google.com/p/chromium/issues/detail?id=157218
-webkit-transform: scale(1);
border-radius: 8px;
overflow: hidden;
.dz-upload {
background: #333;
background: linear-gradient(to bottom, #666, #444);
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
@include prefix((transition: width 300ms ease-in-out));
}
}
&.dz-error {
.dz-error-message {
display: block;
}
&:hover .dz-error-message {
opacity: 1;
pointer-events: auto;
}
}
.dz-error-message {
$width: $image-size + 20px;
$color: rgb(190, 38, 38);
pointer-events: none;
z-index: 1000;
position: absolute;
display: block;
display: none;
opacity: 0;
@include prefix((transition: opacity 0.3s ease));
border-radius: 8px;
font-size: 13px;
top: $image-size + 26px;
left: -10px;
width: $width;
background: $color;
background: linear-gradient(to bottom, $color, darken($color, 5%));
padding: 0.5em 1.2em;
color: white;
// The triangle pointing up
&:after {
content: '';
position: absolute;
top: -6px;
left: $width / 2 - 6px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid $color;
}
}
}
}
@extends('layouts.app')
@section('content')
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">Dashboard</div>
<div class="panel-body">
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
@component('partials.uploader', [
'title' => 'Upload Post Images',
'params' => [
'attachable_id' => 1,
'attachable_type' => 'App\Post'
],
'acceptedFiles' => '.jpg,.png',
'uploadedFiles' => $post->attachments->toArray()
])
@endcomponent
<br>
@component('partials.uploader', [
'title' => 'Document Uploader',
'desc' => 'Upload PDF, DOC, or XLS document',
'params' => [
'attachable_id' => 2,
'attachable_type' => 'App\Post'
],
'acceptedFiles' => '.doc,.xls,.pdf, .docx',
'uploadedFiles' => $post2->attachments->toArray()
])
@endcomponent
</div>
</div>
</div>
@endsection
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment