I am building a JavaScript file uploader based on XHR. (Ajax in fact as I use jQuery)
I use some HTML5 features as the Drag&Drop API, and - at the moment - the FileReader API.
I have some questions about the process, because there are some things I do not understand, theoretically.
I - voluntarily - choose to not use one of the existing and well designed plugins / standalone uploaders for some reasons :
I have some specific needs, by example to add a title and description (as in facebook) to the item
I would to implement it myself as a try for the HTML5 API's.
I've read a tutorial Upload Files with Ajax but I find strange to use the FileReader API to finally append the images to the FormData API.
More than it, I do not really would to have a form, I just drop the images in a drop-area, get the files content with the FileReader API then send it with an AJAX request to my server.
I am just interested to know if someone's got a better process idea, because at the moment :
I get the dropped file(s) and read it's content (asynchronously, but take some time...)
I send this content with an AJAX request to the server
I write the content to a physical file on the server (take again some time)
I don't see how that could be optimized / faster, but it is silly to see the time for a big picture, and I just don't want to imagine it for a video by example...
Is it possible by example to only send a reference of the file with Ajax, and manage the upload directly, instead that get the file content then wait that the user click on the send button to send it ?
By example, how does the facebook uploader work, they seems to solve the problem / use something to fix it ?
(If someone is interested to get it or to take a look, here are a small sample of what I have done at the moment)
/* HTML */
<div id='drop-area'>
<h3 class='placeholder'>Drop your files there!</h3>
</div>
/* CSS */
#drop-area {
position: relative;
width: 100%;
height: 400px;
border: 1px dashed grey;
border-radius: 10px;
}
#drop-area.drag-over {
border: 1px solid cyan;
}
#drop-area .placeholder {
position: absolute;
top: 50%;
left: 50%;
width: 235px;
height: 25px;
margin: -12px 0 0 -117px;
line-height: 25px;
text-transform: uppercase;
}
/* JavaScript - jQuery way */
// This code is NOT AT ALL full-browser compatible !!
$(document).ready(function () {
var i,
$dropArea = $('drop-area'),
fileReader,
file,
result,
sendRequest = function () {
$.ajax({
url: 'upload.php',
type: 'post',
data: {
title: $('#title').val(),
description: $('#description').val(),
data: result, // base64 encoded data
type: file.type
},
processData: false, // No query string
contentType: false // No data formatting
}).fail(function (error, status) {
console.log(error, status);
alert('Fail...');
}).done(function (response, output) {
console.log(response, output);
alert('Uploaded!');
}).always(function () {
$('#file-form-container').empty().detach();
});
},
onFileFormSubmit = function (e) {
e.preventDefault();
sendRequest();
return false;
},
displayForm = function (data) {
$('body').append($('<div/>', {
'id': 'file-form-container'
}).append($('<form/>', {
'action': '#',
'id': 'file-form'
}).append($('<img/>', {
'src': data,
'alt': 'File',
'width': 100px
})).append($('<input/>', {
'id': 'title',
'type': 'text',
'name': 'title'
})).append($('<textarea/>', {
'id': 'description',
'class': 'description',
'name': 'description'
})).append($('<input/>', {
'type': 'submit',
'name': 'submit',
'value': 'Send!'
}))));
},
onProgress = function (e) {
console.log('progress', e);
// Update a <progress> element...
},
onLoadEnd = function (e) {
console.log('load end', e);
result = e.target.result;
displayForm(result);
},
onError = function (e) {
console.log('error', e);
// Display a message...
}
readFile = function (file) {
fileReader = new FileReader();
fileReader.onprogress = onProgress;
fileReader.onerror = onError;
fileReader.onloadend = onLoadEnd;
fileReader.readAsDataURL(file);
};
$dropArea.on('dragenter', function (e) {
$(this).addClass('drag-over');
}).on('dragover', function (e) {
e.preventDefault();
e.originalEvent.dataTransfer.dropEffect = 'copy'; // Check the browser support if you want to fix the compatibility
}).on('dragleave', function (e) {
e.preventDefault();
$(this).removeClass('drag-over');
}).on('dragend', function (e) {
e.preventDefault();
$(this).removeClass('drag-over');
}).on('drop', function (e) {
// e || e.originalEvent ?
e.preventDefault();
e.stopPropagation();
$(this).removeClass('drag-over');
if (e.originalEvent.dataTransfer.files) {
for (i = 0, i < e.originalEvent.dataTransfer.files.length; i++) {
// Work with arrays to manage multiple files
file = e.originalEvent.dataTransfer.files[i];
readFile(file); // Read the file content then process the upload (callbacks)
}
}
})
});
/* PHP - Symfony2 */
<?php
// Symfony 2 Controller Context
$em = $this->getDoctrine()->getEntityManager();
// get params
$request = $this->getRequest();
// Check for types, sizes, errors etc.
if ($request->get('data') && [...]) {
[...]
}
// write the decoded data into a file
// fwrite(base64_decode(...));
// By the way, is there any OOP - MVC way to do that with Symfony2 ?
$path = ''; // define the file path
// Save the entity
$entity = new Picture();
$entity->setTitle($request->get('title'));
$entity->setDescription($request->get('description'));
$entity->setPath($path);
$em->persist($entity);
$em->flush();
return new JsonResponse(array($entity), 202);
?>
Related
could someone help me with one problem? I want to add a process bar when you waiting for a response from the server (Django 3.x).
Step to reproduce:
On the page 'A' we have the form.
Enter data to form.
Submit POST request by clicking to button on the page 'A'.
Waiting for getting the result on the page 'A'.
Get the result on the page 'A'.
So, I want to add process bar after 4th and before 5th points on the page 'A'. When you will get the result on the page 'A' it should disappear.
Python 3.7
Django 3.x
You can use nprogress, it's a library used for progress bars. Use this inside the interceptor where you can config it for displaying only when request is in progress until finished.
There are lots of ways to do this. I think using jquery would be easier. Basically you just need to prevent submitting the page and do an Ajax request to server. something like
<script type='text/javascript'>
$(document).ready(function () {
$("form").submit(function (e) {
// prevent page loading
e.preventDefault(e);
$('#loadinAnimation').show();
// preapre formdata
$.ajax({
type: "yourRequestType",
url: "yourUrlEndpoint",
data: formdata,
success: function (data) {
$('#loadinAnimation').hide();
// do rest of the work with data
}
});
});
});
</script>
and show appropriate loading animation in your html part
<div id='loadinAnimation' style='display:none'>
<div>loading gif</div>
</div>
You can also do it using UiKit Library in Javascript on your Django Template Page.
Below code is when a file is Uploaded
In your template file (template.html)
<body>
..
<form>
<progress id="js-progressbar" class="uk-progress" value="0" max="100" hidden></progress>
...
<div class="uk-alert-danger uk-margin-top uk-hidden" id="upload_error" uk-alert></div>
...
</form>
</head>
<script type="text/javascript">
$(document).ready(function(){
var bar = document.getElementById('js-progressbar');
UIkit.upload('.js-upload-list', {
url: '',
name : "customer-docs",
params :{
"csrfmiddlewaretoken":"{{csrf_token}}"
},
method : "POST",
concurrent:1,
allow:'*.(csv|xlsx)',
beforeSend: function (environment) {
console.log('beforeSend', arguments);
// The environment object can still be modified here.
// var {data, method, headers, xhr, responseType} = environment;
},
beforeAll: function (args,files) {
console.log('beforeAll', arguments);
},
load: function () {
console.log('load', arguments);
},
error: function (files) {
console.log("---------------")
},
complete: function () {
console.log('complete', arguments);
},
loadStart: function (e) {
console.log('loadStart', arguments);
bar.removeAttribute('hidden');
bar.max = e.total;
bar.value = e.loaded;
},
progress: function (e) {
console.log('progress', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
loadEnd: function (e) {
console.log('loadEnd', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
completeAll: function (data) {
console.log('completeAll', arguments);
console.log('completeAll', data);
let redirect_loc = ""
setTimeout(function () {
bar.setAttribute('hidden', 'hidden');
}, 1000);
// This is the response from your POST method of views.py
data.responseText = JSON.parse(data.responseText)
if(data.responseText.status == 201){
// swal is another library to show sweet alert pop ups
swal({
icon: data.responseText.status_icon,
closeOnClickOutside: true,
text: data.responseText.message,
buttons: {
Done: true
},
}).then((value) => {
switch (value) {
case "Done":
window.location.href = ""
break;
}
});
}
else if(data.responseText.status == 500){
swal({
icon: data.responseText.status_icon,
closeOnClickOutside: true,
text: data.responseText.message,
buttons: {
Ok: true
},
}).then((value) => {
switch (value) {
case "Ok":
window.location.href = ""
break;
}
});
}
}
});
// This block of code is to restrict user to upload only specific FILE formats (below example is for CSV & XLSX files)
(function() {
var _old_alert = window.alert;
window.alert = function(e) {
console.log(e)
if(e.includes("csv|xlsx") || e.includes("Invalid file type")) {
$("#upload_error").html("Invalid file format. Valid formats are CSV, XLSX").removeClass('uk-hidden')
}else if(e.includes("Internal Server Error")) {
$("#upload_error").html("Internal Server Error Kindly upload Documents again").removeClass('uk-hidden')
}
else {
_old_alert.apply(window,arguments);
$("#upload_error").addClass('uk-hidden').html("")
}
};
})();
});
</script>
On your views.py you can do your computation and once done, you can return a response like below
resp_json = {
"status" : 201,
"status_icon" : "success",
"url" : "/",
"message": message
}
return HttpResponse(json.dumps(resp_json))
For more info on SWAL (Sweet Alerts), visit https://sweetalert.js.org/guides/
I am using dropzone for uploading files which I save with some meta data in a database in the backend.
When a user tries to upload a bulk of files I want to check if he has already uploaded a file with this name and warn him with an alert with an option to continue ot stop.
So I disabled the autoProcessQueue.
I am also listening for addedfile event, I get the name, I perform an ajax to check if it exists in the database and return true or false, good.
Let's say the user tries to upload 40 files which all already exist, I don't want 40 warnings that this file already exists, I want one notification printing all 40 filenames.
I was looking for addedfile but for multiple files, I didn't found a solution.
Here is my code for now
This is where I'm stuck
How can I know when I've checked every file ?
$(document).ready(function() {
#if(isset($checkRoute))
function filenameExists(name) {
$.ajax({
type: 'GET',
url: '{{ $checkRoute }}',
data: {
name: name
}
})
.done(function(res) {
// returns true or false
return res
})
.fail(function(err) {
console.log(err)
})
}
#endif
let existingFilenames = []
let fileCount = 0
$('#{{ $element_id }}').dropzone({
url: "{{ $upload }}", // Set the url for your upload script location
paramName: "documents", // The name that will be used to transfer the file
maxFiles: 100,
maxFilesize: 100, // MB
parallelUploads: 100,
timeout: 240000,
addRemoveLinks: true,
acceptedFiles: "application/msword, application/vnd.ms-excel, application/pdf, image/*, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, image/*",
uploadMultiple: true,
headers: {
'X-CSRF-TOKEN': "{{ csrf_token() }}"
},
init: function() {
#if(isset($checkRoute))
this.on('addedfile', function(file) {
console.log(this.getAcceptedFiles())
fileCount++
if (filenameExists(file.name)) {
console.log(file.name)
existingFilenames.push(file.name)
}
})
#endif
},
autoProcessQueue: false,
sendingmultiple: function (files) {
// Begin loading
KTApp.blockPage({
overlayColor: '#000000',
type: 'v2',
state: 'success',
message: '{{ __('documents_uploading') }}'
});
},
queuecomplete: function () {
// End loading
KTApp.unblockPage();
$.notify({
// options
message: '{{ __('documents_upload_success') }}'
}, {
// settings
type: 'success',
placement: {
from: "top",
align: "center"
},
animate: {
enter: 'animated fadeInDown',
exit: 'animated fadeOutUp'
},
});
window.location.replace("{{ $redirect }}");
}
});
});
Another thing that concerns me is how will I process the queue at the press of the button in the notification.
I had a similar problem as i wanted to provide the user with a dropdown of different upload directories on the server.
As each directory has a different ruleset of acceptedFiles, the static option acceptedFiles was out of the question.
Instead i used a combination of listening to the drop event on dropzone init and to "extend" the accept function:
The drop event gets the total number of files dropped and stores it in droppedFilesCounter. It also initializes/resets a global Object dzCustomFeedback and clears the "feedback" area from prior feedback.
The extended accept function, which is called for by each dropped file checks which upload directory is currently selected and compares the current file extension against the configured accepted ones.
If there is a match the done() function "bubbles" an "all clear".
Otherwise the file is removed from the preview list (or actually never makes it into it) and the global dzCustomFeedback Object cumulates error-messages of the type invalidFileTypes for all files accordingly.
Once all files have been run through the accept function (droppedFilesCounter == 0) and some feedback messages have been prepared the prepareFeedback function is called, which basically populates a jQuery <div> container with the cumulated content of dzCustomFeedback.
init: function() {
var myDropzone = this;
myDropzone.on("drop", function(event) {
if(typeof event.dataTransfer.files == 'object')
{
droppedFilesCounter = event.dataTransfer.files.length;
}
dzCustomFeedback = {};
jQuery('#dz-feedback').empty();
});
},
accept: function(file, done) {
/ * Erweiterung der vordefinierten accept Function * /
customTypeSelector = document.getElementById("selectUploadFolder");
var currentAcceptedFiles = customTypeAcceptedFiles[customTypeSelector.options[customTypeSelector.selectedIndex].value]
var currentFileExtension = file.name.split(".").pop()
droppedFilesCounter--;
if (typeof customTypeAcceptedFiles[customTypeSelector.options[customTypeSelector.selectedIndex].value] == "object"
&&
jQuery.inArray(currentFileExtension, currentAcceptedFiles["extensions"] ) >= 0) {
//accepted file
done();
}
else {
//Unaccepted file revert
this.removeFile(file);
if(typeof dzCustomFeedback["invalidFileTypes"] == "undefined")
{
dzCustomFeedback["invalidFileTypes"] = {
"msg": [
uploadDndConf["textElements"]["invalidFileTypesUploadFolder"].replace("{{uploadFolder}}", currentAcceptedFiles["label"])
, currentAcceptedFiles["extensions"].join()
]
, "type": "error"
, "filesIgnored": {} };
}
dzCustomFeedback["invalidFileTypes"]["filesIgnored"][file.name] = file;
done(this.options.dictInvalidFileType);
}
if(droppedFilesCounter == 0 && !jQuery.isEmptyObject(dzCustomFeedback))
{
prepareFeedback(dzCustomFeedback);
}
},
I'm using a TradingView widget to pull charts and the data is coded in this format:
symbols: {
Equities: ['H&P', 'Google'],
},
symbols_description: {
'H&P': ' BATS:HP ',
'Google': 'BATS:GOOG',
}
I want to convert the symbols and the symbols description into dynamic code using json. I'm struggling on how to use a json file instead of hard coded. Thanks in advance. The entire widget code is:
new TradingView.MiniWidget({
container_id: 'tv-miniwidget-2', tabs: ['Equities'],
symbols: {
Equities: ['H&P', 'Google'],
},
symbols_description: {
'H&P': ' BATS:HP ',
'Google': 'BATS:GOOG',
},
width: 240,
large_chart_url: 'http://www.futuresmag.com/page/interactive-charts',
gridLineColor: "#e9e9ea",
fontColor: "#83888D",
underLineColor: "#dbeffb",
trendLineColor: "#4bafe9",
height: 400,
locale: "en"
});
Build data in steps, not all at once
let options = {
container_id: 'tv-miniwidget-2', tabs: ['Equities'],
width: 240,
large_chart_url: 'http://www.futuresmag.com/page/interactive-charts',
gridLineColor: "#e9e9ea",
fontColor: "#83888D",
underLineColor: "#dbeffb",
trendLineColor: "#4bafe9",
height: 400,
locale: "en"
}
// data format
let jsonData = {
symbols: {
Equities: ['H&P', 'Google'],
},
symbols_description: {
'H&P': ' BATS:HP ',
'Google': 'BATS:GOOG',
}
} // get data from server through ajax request or whatever you need
options.symbols = jsonData.symbols
options.symbols_description = jsonData.symbols_description
new TradingView.MiniWidget(options)
To load a json file in a browser, you typically take the following steps :
Make an Ajax call to your json file
Parse the json file once it's been received
Do something with the content of the file once it's parsed
Here's some demo code for how to do that without using any library:
var getJSON = function(url, successHandler, errorHandler) {
// 1. Make an Ajax call to your json file
var xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.onreadystatechange = function() {
var status, data;
if (xhr.readyState == 4) {
status = xhr.status;
if (status == 200) {
// 2. Parse the json file once it's been received
data = JSON.parse(xhr.responseText);
successHandler && successHandler(data);
} else {
errorHandler && errorHandler(status);
}
}
};
xhr.send();
};
getJSON('widgetdata.json', function(data) {
// 3. Do something with the content of the file once it's parsed
var tradingView = new TradingView.MiniWidget(data);
}, function(status) {
// Error handling goes here
});
I am getting this response from the server for a jpeg image get request(with CORS):
ÿØÿàJFIFÿÛ ( %!1"%)+.............
After converting this to base64 it using btoa(encodeURIComponent(data)) looks like:
data:image/jpeg;base64,JUVGJUJGJUJEJUVGJUJGJUJEJUVGJUJGJUJEJUVGJUJGJUJEJTAwJTEwSkZJRiUwMCUwMSUwMSUwMSUwMEglMDBIJTAwJTAwJUVGJUJGJUJEJUVGJUJGJUJEJTAwJUVGJUJGJUJERXhpZiUwMCUwME1NJTAwKiUwMCUwMCUwMCUwOCUwMCUw...............................
On setting the img tag src attribute to the base64 above the image is not rendering in the browser.
However, when I open the image link in the browser tab, it loads properly. Also, on sending the same request in postman it renders the image in the response, in postman.
Even using the blob approach doesn't work (used bower: angular-img-http-src)
$scope.$watch('objectURL', function (objectURL) {
elem.attr('src', objectURL);
});
$scope.$on('$destroy', function () {
revokeObjectURL();
});
$http.get(url)
.then(function (response) {
var blob = new Blob(
[ response.data ],
{ type: response.headers('Content-Type') }
);
$scope.objectURL = URL.createObjectURL(blob);
});
Kindly help here.
In Service:
yourService : function(options) {
return $http.get(url, {
responseType : 'arraybuffer'
}
).success(function(data) {
var file = new Blob([ data ], {
type : 'image/jpeg'
});
var fileURL = URL.createObjectURL(file);
if(options && options.successCallBack) {
return options.successCallBack(fileURL, {});
}
});
},
In Controller:
function yourImageSuccessHandler(fileUrl, options) {
$scope.objectUrl= fileUrl; // now you will have fileUrl in
// controller
}
yourService.getDownloadDoc(empId, {
successCallBack: yourImageSuccessHandler
});
In template HTML:
<img ng-src={{objectUrl}}></img>
Though I don't know if there's a specific angular routine, the general JS solution for created images goes something like this...
function addIMG(durl, callback) {
var img = new Image();
img.addEventListener('load', function() {
callback(this);
}, false);
img.src = durl;
}
addIMG(objectURL, function(img) {
$('#element').appendChild(img);
});
Hope that helped :)
I have a strange behavior of extjs file upload.
The file upload defined as:
items: [{
xtype: 'filefield',
itemId: 'uploadandsign',
buttonText: NG.getLabel('browse'),
buttonOnly: true,
hideLabel: true,
width: 100
}]
If the file uploading is success I show successful label on the screen with remove "X" button:
onOpenFileBrowserChange: function (filefield, newValue, oldValue, eOpts) {
var me = this,
form = filefield.up('form').getForm(),
infoBox = invoiceorigin.down('#fileuploadinfoplaceholder'),
fileDescription,
secondfilefield,
customerFileName = newValue.replace(/^.*[\\\/]/, ''),
draft = me.getDraft(),
isSigned = true,
files = draft.files();
if (filefield.itemId === 'uploadandsign') {
isSigned = false;
secondfilefield = invoiceorigin.down('#uploadnosign');
fileDescription = 'File system, Unsigned';
}
secondfilefield.disable();
if (form.isValid()) {
form.submit({
url: NG.getLatestApiPath('WebInvoice', 'UploadInvoiceFile'),
waitMsg: NG.getLabel('webinvoiceInvoiceOriginUploadingFile'),
success: function (fp, o) {
if (o.result.success) {
var file = o.result.file;
files.add({
fileName: file.fileName,
createDate: file.createDate,
isAttachment: false,
isSigned: isSigned,
fileOrigin: fileDescription,
customerFileName: customerFileName,
invoiceFileOrigin: 'Local'
});
filefield.disable();
infoBox.removeAll();
infoBox.add(Ext.create('NG.view.webinvoice.InformationBox', {
data: {
closable: true,
icon: true,
iconCls: 'n-pdf-icon',
content: '<div class="n-text-overflow" style="width:145px;">' + fileDescription + '<br/>' + customerFileName + '</div>'
}
}));
}
else {
}
},
failure: function (form, action) {
}
});
}
return false;
},
Then if I remove the file from #infobox, the reset() function called:
onRemoveFileClick: function (view) {
var me = this,
invoiceorigin = view.up('invoiceorigin'),
uploadNoSignBtn = invoiceorigin.down('#uploadnosign'),
uploadAndSignBtn = invoiceorigin.down('#uploadandsign'),
infoBox = invoiceorigin.down('#fileuploadinfoplaceholder'),
draft = me.getDraft(),
files = draft.files(),
pagemanager = view.up('webinvoicepagemanager'),
invoiceFilePlace = files.findExact('isAttachment', false);
me.deleteFileConfirmReject(
NG.getLabel('webinvoiceInvoiceOriginDeleteInvoiceFileTitle'),
NG.getLabel('webinvoiceInvoiceOriginDeleteInvoiceFileMsg'),
function (buttonId, text, opt) {
if (buttonId == 'yes') {
infoBox.removeAll();
if (invoiceFilePlace > -1) {
files.removeAt(invoiceFilePlace);
}
me.fillInvoiceOriginStep(pagemanager);
uploadNoSignBtn.reset();
uploadAndSignBtn.reset();
uploadNoSignBtn.enable();
uploadAndSignBtn.enable();
}
});
}
After this action if I will choose the same file.... nothing happens with page... no any change event fired on the page.. However if I choose different file the behavior is OK.
In the ExtJS documentation said that the reset() function have to clear previous files uploads... however it does not helps.
Is any body met such file upload ExtJS behaivour and could help with this issue?
Thanks a lot.
What I tried and worked quite good was to get the file from the form with a typical JS document.getElementsByName('[name you gave]'); and it got perfectly the file uploaded without mattering in wich execution you are in.
Hope it helps.