File uploads: Percentage completed progress bar - javascript

I'm trying to add a 'percentage completed so far' progress bar to avatar uploads in BuddyPress. The aim is to stop users navigating away from the page before the upload is completed.
The upload process is handled in BuddyPress by bp_core_avatar_handle_upload() in file bp-core/bp-core-avatars.php. The function starts off by checking that the file has been uploaded properly using bp_core_check_avatar_upload(). It then checks that the file size is within limits, and that it has an accepted file extension (jpg, gif, png). If everything checks out, the user is allowed to crop the image (uses Jcrop) and then the image is moved to its real location.
The actual upload is handled by the WordPress function wp_handle_upload.
How can I create a 'percentage completed' progress bar and display it when the file is uploading?

I'm not familiar with BuddyPress, but all upload handlers (including the HTML5 XHR one that androbin outlined) will have a file progress hook point that you can bind to.
I've used uploadify, uploadifive and swfupload, and they can all interact with the same progress function handler in order to acheive the same progress bar result.
// SWFUpload
$elem.bind('uploadProgress', function(event, file, bytesLoaded) { fnProgress(file, bytesLoaded); })
// Uploadify
$elem.uploadify({ onUploadProgress: function (file, bytesUploaded, bytesTotal, totalBytesUploaded, totalBytesTotal) { fnProgress(file, bytesUploaded); });
// Uploadfive
$elem.uploadifive({ onProgress: function(file, e) { fn.onProgress(file, e.loaded); });
Uploadifive, being an HTML5 based uploader, simply binds to the XHR 'progress' event, so all these properties will be available to any HTML5 uploader.
As for the actual progress bar code..
HTML:
<div class='progressWrapper' style='float: left; width: 100%'>
<div class='progress' style='float: left; width: 0%; color: red'></div>
<div class='progressText' style='float: left;></div>
</div>
JS:
var fnProgress = function(file, bytes) {
var percentage = (bytesLoaded / file.size) * 100;
// Update DOM
$('.progress').css({ 'width': percentage + '%' });
$('.progressText').html(Math.round(percentage + "%");
}

You should use an XHR object. I don't now if it helps you, but I have a simple XHR uploader written.
HTML:
<form id="uploader" enctype="multipart/form-data" action="uploadimage.php" method="post">
<input type="file" id="file" name="file[]" multiple="multiple" accept="image/jpeg" /><br/>
<input type="submit" value="Upload" />
<div class="list" style="background-color:#000;color:#FFF;padding:5px;display:none;border-radius:5px;">
</div>
</form>
JS:
$("#uploader").submit(function(){
$('#uploader .list').fadeIn(100).css("width","0px");
var data = new FormData();
// if you want to append any other data: data.append("ID","");
$.each($('#file')[0].files, function(i, file) {
data.append('file-'+i, file);
});
$.ajax({
url: 'uploadimage.php',
data: data,
cache: false,
contentType: false,
processData: false,
type: 'POST',
xhr: function() { // custom xhr
myXhr = $.ajaxSettings.xhr();
if(myXhr.upload){ // check if upload property exists
myXhr.upload.addEventListener('progress',progressHandlingFunction, false); // for handling the progress of the upload
}
return myXhr;
},
success: function(data2){
$('#uploader .list').css({
"width":"200px",
"text-align":"center",
"margin":"10px 0 10px 0"
}).html("DONE!").delay(2000).fadeOut(500);
if (data2 == "ERROR_FILESIZE"){
return alert("Choose another file");
}
else{ /*change location*/ }
});
return false;
});
In this case I uploaded the file with uploadimage.php and if it printed: "ERROR_FILESIZE" then it alerted the error.

I think that before you worry about the client-side of things you should be aware of the server-side requirements to actually be able to accomplish this.
For PHP you need to have the session.upload_progress enabled unless the wp_handle_upload() function uses something different, so I'm here just guessing, but chances are they do use the regular PHP session stuff hence it needs to be enabled.
If you look at the comments for the given link many users say that progress state does not work under certain environments such as PHP on FastCGI which is what you'll get in shared hosting environments most of the time.
Now many people here are telling you to use the XHR uploader but the problem is that they are giving you an example of a custom upload.php script or something like that to send the data, but you are using a wordpress plugin which you don't control (kinda)
So considering that the wp_handle_upload() does not actually works in an AJAX way then you would have to hook an event when the file upload form submit button is clicked and set a timer in JS which calls some URL where you pass the form data like an ID, and then query the session with that ID to check the progress of the file:
$_SESSION["upload_id"]["content_length"]
$_SESSION["upload_id"]["bytes_processed"]
With that data you can calculate how much has been transfered. You could set the JS timer to be called like each second but if the files they are uploading are not very large (say, larger than 1mb) and they have a good connection then there won't be much progress to be notified.
Check this link for a step by step example on how to work with this session upload data.

You need to inject the progress bar.
I think the only way is to over-ride the function bp_core_avatar_handle_upload using the filter hook apply_filters( 'bp_core_pre_avatar_handle_upload' etc. )
You'll end up duplicating most of the function but you should be able to add your progress bar code.
If you get this working, you should submit it as an enhancement ticket; it's a good idea.

Related

Why does my script not run on AJAX refresh?

I'm working on an avatar uploader for my project. Everything has been so far so good, and this morning I had no issues. A little while later, BOOM. Death and destruction. Very sadness.
When I first choose a file, it pops up the crop tool immediately, and works fine. If I attempt to upload a different file, the crop tool disappears and not even a preview of the image is presented.
How does my system work? I'm using the Laravel framework as my backend, and the Laravel Livewire package for front-end functionality. Livewire allows me to write Vue-like components in PHP.
Here's the form I'm working on. It's also the component that's refreshed every time that Livewire sees a new file in the input.
<label for="image-upload">
<div class="w-full mt-4 button"><i class="far fa-fw fa-upload"></i> Drag and drop, or <b>browse</b></div>
</label>
<input id="image-upload" type="file" wire:model="image" class="hidden" accept="image/png, image/gif, image/jpeg">
#if($image)
<div id="avatar-preview" class="w-full" style="height: 300px;"></div>
<script>
new Croppie(document.querySelector('#avatar-preview'), {
viewport: { width: 200, height: 200, type: 'circle' },
enforceBoundary: true,
}).bind('{!! $image->temporaryUrl() !!}');
</script>
<button type="submit" class="w-full mt-16 button is-green">Submit new avatar</button>
#endif
I'm using the Croppie JS package for the crop tool. It requires that I either pass it an img element it'll attach to, or a container to fit into. I picked a container so I could control the size of the crop tool. When I upload an image to the input, Livewire will take the image, validate it's an image and doesn't pass a certain size, and uploads the image to a temporary directory. The $image->temporaryUrl() echoes the path to that temporary file. When the file upload is done and the temporary image is ready, Livewire refreshes just the component via AJAX. At this point, I attempt to create a new Croppie instance, attach it to my preview container, and bind the temporary image to it.
This works on the first file upload! Then, when I attempt to change the file (as a user might) the entire instance disappears. I attempted to check and see if it was an issue instantiating Croppie again, since in my mind if the component is refreshed, creating a new instance of the tool shouldn't be an issue.
<script>
if (typeof crop === 'undefined') {
console.log('crop undefined');
let crop = new Croppie(document.querySelector('#avatar-preview'), {
viewport: { width: 200, height: 200, type: 'circle' },
enforceBoundary: true,
}).bind('{!! $image->temporaryUrl() !!}');
} else {
console.log('crop defined');
crop.bind('{!! $image->temporaryUrl() !!}');
}
console.log('crop finished');
</script>
And the 'undefined' and 'finished' logs come in on the first upload, but nothing happens on the refresh.
So I also tested just doing this...
#if($image)
<img src="{{ $image->temporaryUrl() }}">
#endif
To ensure at the very least that the preview was working correctly, and lo and behold it does!
The big problem is that even when the Croppie doesn't refresh, no errors or warnings occur. It just doesn't seem to execute what's in the <script> tag at all.
Any tips or advice?
After further reading, it appears Livewire can, from the component class, emit "events" that can be picked up by JavaScript. My issue was caused by the natural behavior of changing DOM content by .innerHtml.
In my component class, in the updated() function that handles what to do when the input is updated, I added
$this->emit('avatar_preview_updated', $this->image->temporaryUrl());
I then added this listener to my page's global JS:
livewire.on('avatar_preview_updated', image => {
new Croppie(document.querySelector('#avatar-preview'), {
url: image,
viewport: { width: 200, height: 200, type: 'circle' },
boundary: { height: 300 },
enforceBoundary: true,
});
});
Which produces the desired result.

Drag & Drop using Dropzone and Seaside

I'm struggling a little with the dropzone implementation for dragging/dropping files. I am developing on a Smalltalk platform with Seaside on the front end. Currently, I am able to upload a file but unable to see the success icon as well as the progress bar appear at the time of upload. When I inspect elements on the web, I do see that the divs representing the success/progress are present. I do see the file size and file name appear just as the file gets dropped in. Can someone point out what I'm missing? My code is as follows:
| serverURL url |
serverURL := RepWebSettings portalSettingsWebServerURL ifNil: [self session requestContext request uri serverURL].
url := serverURL ,
html context actionUrl printString ,
'&' , (html callbacks store: (Seaside.WAValueCallback on: [self uploadFileDroppedFiles])).
html div class: 'layoutBorder'; with: [
html div id: 'draganddropupload'; class: 'dropzone';
with: [
html div class: 'dz-message'; with: [
html image url: RepWebFileLibrary / #draganddropPng]]].
html script: ('
$(document).ready(function () {
Dropzone.autoDiscover = false;
Dropzone.uploadMultiple = true;
Dropzone.createImageThumbnails = false;
$("#draganddropupload").dropzone({
url: "%1",
success: function (file, response) {
document.location.reload(true);
}
});
});' bindWith: url).
When you run your example with Chrome's Developer Tools opened to the 'Console' tab what javascript errors do you see? Seems like Dropzone manages the progress bar and checkmark.
From looking at their simple example:
https://github.com/enyo/dropzone/blob/gh-pages/examples/simple.html
The only difference between what you've got and what they've got is that they're using a 'form' tag with the 'dropzone' class and you're using a 'div' tag.
So, it may be that Dropzone expects to operate in a 'form' and without any other inputs in that form. So if it were me I'd change that div tag in your seaside code to a form, and make sure the new form wasn't nested inside another form on the page.

how get a php value inside a self-invoked jquery function?

I have a page that generates n links in a foreach loop:
...some html and php code
<?php foreach ($tables as $table):?>
... some elements generated ...
<td><a onclick="setPortalId(<?php echo $table['id']?>);$('#fileupload').trigger('click');" class="btn-success btn-sm"><i class="icon-plus white bigger-125"></i>Add / Change</a></td>
... another elements ...
<?php endforeach;?>
As you can see, the onclick event in each link execute 2 js functions,the first sets a js var with the php value $table['id'] because i will need this value to determine my zend route and the last function trigges the input fileUpload of the type file:
<input id="fileupload" type="file" class="hidden" multiple="" name="files[]">
and in the scripts i have this:
<script src="/js/vendor/jquery.ui.widget.js"></script>
<!-- The Iframe Transport is required for browsers without support for XHR file uploads -->
<script src="/js/jquery.iframe-transport.js"></script>
<!-- The basic File Upload plugin -->
<script src="/js/jquery.fileupload.js"></script>
<!-- Bootstrap JS is not required, but included for the responsive demo navigation -->
<script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script>
var idPortal;
function setPortalId(valor) {
idPortal = valor;
}
/*jslint unparam: true */
/*global window, $ */
$(function () {
'use strict';
// Change this to the location of your server-side upload handler:
var url = '/precos/upload/id/'+ idPortal;
$('#fileupload').fileupload({
url: url,
dataType: 'json',
done: function (e, data) {
$.each(data.result.files, function (index, file) {
$('<p/>').text(file.name + " adicionado").appendTo('#files');
window.alert(file.name + " Adicionado.");
});
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$('#progress .progress-bar').css(
'width',
progress + '%'
);
}
}).prop('disabled', !$.support.fileInput)
.parent().addClass($.support.fileInput ? undefined : 'disabled');
});
</script>
My question is how I can get the idPortal of the clicked link in the last self-invoqued funtion?Any sugestions?
This is horrible design. You should try to keep your JS as unobtrusive as possible, ie. don't use event handler attributes like onclick. Attach the event handler via JS. There are times when this is impractical but I don't see any evidence that that is the case here.
What I would do on the PHP side is to add some classes and a data attribute that I can hook in to from JS:
<?php foreach ($tables as $table):?>
<td>
<a data-portal-id="<?php echo $table['id']?>" class="btn-success btn-sm btn-upload"><i class="icon-plus white bigger-125"></i>Add / Change</a>
</td>
<?php endforeach;?>
Now on the JS side I would simply read the data-portal-id from the clicked link, use it to set the URL on the file uploader, and then trigger the click to begin the upload workflow:
$(selectorForTheTable).on('click', 'a[data-portal-id].btn-upload', function (e) {
// pull the portalId from the link's data-portal-id attribute
var portalId = $(this).data('portalId'),
$uploader = $('#fileupload');
// set the url for the upload based on out portalId
$uploader.fileupload('option', 'url', '/precos/upload/id/'+ portalId);
// invoke the click
$('#fileupload').trigger('click');
});
The one thing missing here is that you might want to set something up so that when the uploader is closed or all the uploads complete the URL is set back to null or a URL of no consequence. This would help to ensure something going wrong on the client cant mistakenly upload files to the wrong endpoint.
Here is an example Fiddle that works as much as a Fiddle can :-)
You need to make your url global and update it later in that context. Use it like
var idPortal;
var url;
function setPortalId(valor) {
idPortal = valor;
url = '/precos/upload/id/'+ idPortal;
}
The easiest approach to seperate PHP (serverside business logic) and Javascript (non-business critical GUI enhencement), is to put all variables from PHP into the DOM and then later work with it:
<script>
var phpValues = <?php echo json_encode($yourPhpValuesArrayOrObject); ?>;
</script>
....
<script>
The attributes connected with business data from inside the HTML (=semantic structure) should go with a data-* attribute as already mentioned.
You're setting url when the page is first loaded, not after the user clicks on the link. Add that to the setPortalId function:
function setPortalId(valor) {
idPortal = valor;
url = '/precos/upload/id/'+ idPortal;
}
thank you everyone,but I used another approach to get the correct value of the clicked element.like was said,the function is self-invoked in the page loading,so in this moment the global var still null.I as using the blueimp jquery file upload,so reading the documentation I saw that is possible send another values during the ajax request just adding news inputs in the form.with this I solved my problem.

How to display cancel button for failed uploads in fineuploader?

I am using fineuploader. Some upload may fail as user try to upload duplicate file.
I see a red color and the error message from server is shown.
I am not seeing any cancel OR Remove button that would allow the user to start fresh again by selecting a new file.
I was expecting fineuploader to show cancel button on failed files but it is not.
I can add onError callback then find the right row and show the cancel or other button by adding Remove button. That sound messy solution.
That cancel button would remove that file and let user select another file.
Can I add this capability by setting options in the settings?
var fileUploader = new qq.FineUploader({
element: $('#manual-fine-uploader')[0],
request: {
endpoint: 'upload/UploadFiles',
},
multiple: true,
autoUpload: false,
validation: {},
text: {uploadButton: '<i class="icon-plus icon-white"></i> Select File'},
callbacks: {
onSubmit: function (id, file) {//my custom code },
onCancel: function(id, file){ if(filesCount>=1) filesCount--;},
onComplete: function (id, fileName, responseJSON) {filesCount--;}
},
failedUploadTextDisplay: {
mode: 'custom',
maxChars: 40,
responseProperty: 'error',
enableTooltip: true
}
});
Thanks
Fine Uploader only displays a cancel button while a file is uploading (as well before uploads have started if you have set the autoUpload option to false). Showing a cancel button after the upload has completed seems like it would be a bit confusing to users, but I can see why you might want to display a "remove" button.
You can easily do this by adding the button to the file portion of your template. For example, your template might look similar to this:
<div class="qq-uploader-selector qq-uploader">
...
<ul class="qq-upload-list-selector qq-upload-list">
<li>
...
<button class="remove-button-selector" style="display:none">Remove</button>
</li>
</ul>
</div>
Then, add a click handler to purge the file from Fine Uploader's internals & the UI:
$('#manual-fine-uploader').on("click", ".remove-button-selector", function() {
var fileId = fileUploader.getId(this);
fileUploader.cancel(fileId);
});
Finally, in your onComplete handler, show the button if the upload has failed:
onComplete: function(id, name, response, xhr) {
var fileItemEl = fileUploader.getItemByFileId(id);
if (!response.success) {
$(fileItemEl).find(".remove-button-selector").show();
}
}
Additionally, since you are already using jQuery in your project, I strongly suggest you make use of the Fine Uploader jQuery plug-in. It will make your life a bit easier.

Javascript Multiple File Upload, Sequentially One at a Time

We have a form with five <input type="file"/> elements that is in production and working great. We get request timeouts and MaxRequestLength exceeded errors on occasion. To prevent these errors, I planned to write some Javascript to upload the files one-at-a-time instead of all at once. Here is how I planned on doing this...
On document.ready, inject a hidden iframe into page
Change the <form> to target the iframe
Disable all elements on the form (which prevents them from being POSTed)
Enable one file-upload at a time and submit the form
Wait for the response from the server
When server response is printed into iframe, start the next upload
When all uploads are done, refresh the page, which will invoke some server-side logic that populates a grid.
My problem is with number 5. Normally I think I could figure this out no problem, but I am just having one of those days where my brain is on strike. Here is my code thus far...
$(function() {
$("<iframe/>").attr("src", "test.htm").attr("name", "postMe").hide().appendTo("body");
$("form").attr("target", "postMe").submit(function(e) {
e.preventDefault();
$("#btnSubmit").attr("disabled", "disabled").val("Please Wait, Files are Uploading");
for(var i = 1; i < 6; i++) {
$("input[type=file]").attr("disabled", "disabled");
$("#FileUpload" + i).removeAttr("disabled");
$("form")[0].submit();
// HELP!!!
// How do I wait for server before next iteration?
}
location.reload(true);
});
});
What kind of construct do I need here in order to "wait" for the server response before kicking off the next upload?
I've had a lot of success lately using Uploadify--it's very configurable, free, and allows for multiple-uploads. It also provides the option for callback functions allowing you to really configure it any way you want.
http://www.uploadify.com/
I think you should listen for iframe's load event and perform input's switching in the handler. I completed with my own uploader today and this solution worked for me.
Just FYI: jquery.forms plugin is all about making ajax form submitions. I use this plugin to submit a form (such as a file upload) in a separate iframe which the plugin takes care of automatically, and gives you a nice callback when completing.
This way most work for you is done.
http://jquery.malsup.com/form/
It can be done with the help of jQuery's queue method and load event.
<!DOCTYPE HTML>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
<script>
//here's an upload script
(function($){
//make 'em accessible within the scope
var $iframe, $form;
$(document).ready(function(){
//create 'em only once, but use 'em many times
$iframe = $('<iframe name="iframe" id="iframe" style="display:none"></iframe>').appendTo('body');
$form = $('<form method="post" enctype="multipart/form-data" target="iframe" style="display:none"></form>').appendTo('body');
});
var iframeUpload = $({});
$.iframeUpload = function(s){
iframeUpload.queue(function(next){
//as we only wanna this new event
$iframe.load(function(){
//we must unbind the old one
$iframe.unbind('load');
//success or error, the question is up to you
s.success();
//but remember to remove or replace the old stuff
$form.find('input').remove();
next();
});
$form.attr('action', s.url).append(s.file).submit();
});
};
})(jQuery);
//and this is how to use the script
(function($){$(document).ready(function(){
$('input[type="submit"]').click(function(){
$('input[type="file"]').each(function(){
$.iframeUpload({
url: 'http://example.com/upload.php',
file: this,
success: function(){
console.log('uploaded');
}
});
});
});
})})(jQuery);
</script>
</head>
<body>
<!-- here are multiple files -->
<input type="file" name="file" />
<input type="file" name="file" />
<input type="file" name="file" />
<!-- to upload -->
<input type="submit" />
</body>
</html>
I was able to do this, by starting with the code at A Strategy for Handling Multiple File Uploads Using Javascript. That code uses an XMLHttpRequest for each file, but actually doesn't check the result from the server. I modified it to wait for the result from the server, sequentially, as follows:
var fileNumber = 0
var fileList = [] // see the code linked above for how to handle the fileList
var resultPane = document.getElementById('resultpane') // a textarea box
sendNext = function() {
if (fileNumber >= fileList.length) {
resultPane.value += 'Done uploading '+fileNumber+' files\n'
return 0
}
var formData = new FormData()
var request = new XMLHttpRequest()
request.onreadystatechange = function() {
if (request.readystate == XMLHttpRequest.DONE) {
resultPane.value += request.responseText // show whatever the server said about each file
sendNext() // and send the next file
}
}
formData.set('file', fileList[fileNumber])
request.open('POST', 'https://example.com/upload-receiver')
request.send(formData)
resultPane.value += 'Sending file number '+fileNumber+'\n'
fileNumber++
}

Categories