Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
pmb
/
javascript
/
dojo
/
snet
/
fileUploader
:
Uploader.js
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
define([ 'dojo/_base/lang', 'dojo/_base/declare', 'dojo/_base/Deferred', 'dojo/_base/array', 'dojo/_base/xhr', 'dojo/dom', 'dojo/dom-construct', 'dojo/dom-class', 'dojo/query', 'dojo/has', 'dojo/on', 'dojo/aspect', 'dijit/registry', 'dijit/Dialog', 'snet/DialogConfirm', 'dijit/ProgressBar', 'snet/fileUploader/ProgressBar' ], function(lang, declare, Deferred, array, xhr, dom, domConstruct, domClass, query, has, on, aspect, registry, Dialog, DialogConfirm, ProgressBar, UploadProgressBar) { return declare(null, { maxKBytes: 3000, // in kbytes limited by php.ini directive upload_max_filesize maxNumFiles: 10, // limited by php.ini directive max_file_uploads bytesOverall: 0, barOverall: null, fileStatus: { numCompleted: 0, // number of completely uploaded files numAborted: 0, // number of canceled files numProgressDone: 0, // number of files where upload progress is 100% numError: 0 // number of files with error }, files: [], // files that will be uploaded after checking their length and max allowed number of uploads progressBars: [], // keeps track of created bars displayTarget: null, // If null, progress is displayed in a dialog, otherwise provide element id dropTarget: null, rememberConfirmDelete: false, // do not ask user again to confirm deleting callback: false, /** * Instantiates the fileUploader. * Expects object with following properties: * id: // {String|Object} DomNode or id of element that the progress bars are created in. * url: // {String} url of php page that handles the upload * target: // {String|Object} DomNode or id of element where files can be dropped onto * * @param {Object} props arguments */ constructor: function(props) { if (!this.hasFeatures()) { this.confirmHasFeatures(); } else { this.append_div = props.append_div; props.dropTarget = dom.byId(props.dropTarget); lang.mixin(this, props); this.maxKBytes *= 1048; // e.g. * (1024 + 24) // add drag and drop events on(window, 'dragover', function(evt) { evt.preventDefault(); // necessary for dnd to work }); on(window, 'drop', function(evt) { evt.preventDefault(); }); on(this.dropTarget, 'dragenter', function() { domClass.add(this, 'targetActive'); }); on(this.dropTarget, 'dragleave', function() { domClass.remove(this, 'targetActive'); }); on(this.dropTarget, 'mouseout', function() { domClass.remove(this, 'targetActive'); }); on(this.dropTarget, 'drop', lang.hitch(this, function(evt) { var files = evt.dataTransfer.files; this.reset(); this.addFiles(files); domClass.remove(this.dropTarget, 'targetActive'); })); } }, /** * Add and filter files to upload. * Add files to internal array and calc total amount of bytes to upload. Also check for size and number of uploads limit. * @param {Array} files instance of FileList object */ addFiles: function(files) { var dfds = [], idx; dfds[0] = new Deferred(); dfds[0].resolve(false); // exclude files that are to large // and chain deferreds so the get fired one after the other this.files = array.filter(files, function(file) { idx = dfds.length - 1; var self = this; if (file.size > this.maxKBytes) { dfds[idx + 1] = dfds[idx].then(function(remember) { var fileName = file.name || file.fileName; if (!remember) { return files.length > 1 ? self.confirmFileSize(fileName) : self.confirmFileSizeSingle(fileName); } else { var dfd = new Deferred(); dfd.resolve(true); return dfd; } }); return false; } else { this.bytesOverall += file.size; return true; } }, this); // limit number of files you can upload if (this.files.length > this.maxNumFiles) { this.files = this.files.slice(0, this.maxNumFiles); idx = dfds.length - 1; dfds[idx + 1] = dfds[idx].then(lang.hitch(this, function() { return this.confirmNumFileLimit(this.maxNumFiles); })); } dfds[dfds.length - 1].then(lang.hitch(this, function() { this.createBars(); this.uploadFiles(); dfds = null; // free memory })); }, /** * Creates a progress bar for each file and uploads it. */ createBars: function() { var self = this; var container, div, strTemplate; var barOverall = this.barOverall; var i = 0, len = this.files.length; if (len === 0) { return false; } // create fileUploader container strTemplate = '<div id="pbwOverallCont">' + '<div id="pbwBarOverall" class="pbwBar">' + '<div class="pbwTxt">Progression totale</div>' + '</div>' + '</div>' + '<div id="pbwBarsCont"></div>'; // display inside dialog pane if (!this.displayTarget) { container = new Dialog({ id: 'snetUploader', title: 'Transfert sur le serveur', content: strTemplate, duration: 500, onHide: function() { window.setTimeout(function() { container.destroyRecursive(); }, container.get('duration') + 20); } }); container.show(); } // display inside target element else { container = domConstruct.create('div', { id: 'snetUploader', innerHTML: strTemplate }, this.displayTarget) } // create progress bar for overall progress div = domConstruct.create('div', null, 'pbwBarOverall'); barOverall = new ProgressBar({ maximum: this.bytesOverall, value: 0 }, div); // create containers for individual progress bars for (; i < len; i++) { (function(i) { var bar, file = self.files[i]; div = domConstruct.create('div', null, 'pbwBarsCont'); bar = new UploadProgressBar({ text: file.name + ', ' + self.formatSize(file.size), bytesTotal: file.size, maximum: file.size, value: 0 }, div); bar.setIcon(file, 64); bar.watch('value', function(valName, valOld, valNew) { var stat, inc; if (valNew != Infinity && valOld != Infinity) { inc = valNew - valOld + barOverall.get('value'); barOverall.set('value', inc); } else if (valNew == Infinity) { stat = self.fileStatus; stat.numProgressDone++; if (stat.numProgressDone + stat.numAborted === len) { barOverall.set('value', Infinity); } } }); aspect.after(bar, 'onComplete', function() { // todo: use bar.watch instead as above var stat = self.fileStatus; stat.numCompleted++; if (stat.numCompleted + stat.numAborted === len) { domClass.add(query('.dijitProgressBarTile', barOverall.domNode)[0], 'pbwBarTileDone'); barOverall.set('value', barOverall.maximum); } }); aspect.after(bar, 'onRetry', function() { self.resume(file, this); }); aspect.after(bar, 'onResume', function() { self.resume(file, this); }); aspect.after(bar, 'onPause', function() { if (bar.xhr) { bar.xhr.abort(); } }); aspect.after(bar, 'onDelete', function() { var dfd = self.del(file, bar); dfd.then(function() { self.progressBars[i] = null; self.files[i] = null; self.remove(container); }); }); aspect.after(bar, 'onAbort', function() { // we can't just use bar.xhr.abort(); since part of the file will still be written to disk on server // => init abort on server first self.abort(file, bar); self.progressBars[i] = null; self.files[i] = null; }); aspect.after(bar, 'onRemove', function() { self.remove(container); }); self.progressBars[i] = bar; })(i); } this.barOverall = barOverall; }, /** * Shows a dialog to confirm skipping files that are to big. * @param {String} fileName name of file * @return {dojo/_base/Deferred} */ confirmFileSize: function(fileName) { var dialog = registry.byId('dialogFileSize') || new DialogConfirm({ id: 'dialogFileSize', title: 'Confirm', content: '<p>Maximum file size is limited to ' + this.formatSize(this.maxKBytes) + '.</p>' + '<p>Press \'OK\' to skip file ' + fileName + '<br/>or press \'Cancel\' to cancel uploading.</p>' }); return dialog.show(); }, /** * Shows a dialog to confirm not uploading file that is to big (only one file overall). * Used only when there is a single file to upload. * @param {String} fileName name of file * @return {dojo/_base/Deferred} */ confirmFileSizeSingle: function(fileName) { var dialog = registry.byId('dialogFileSizeSingle') || new DialogConfirm({ id: 'dialogFileSizeSingle', title: 'Confirm', content: 'Maximum file size is limited to ' + this.formatSize(this.maxKBytes) + '. Uploading file ' + fileName + ' will be canceled.</p>', hasCancelButton: false, hasSkipCheckBox: false }); return dialog.show(); }, /** * Shows a dialog to confirm skipping the remaining files. * If the limit of number of files that can be uploaded is reached, the user can decide to skip the remaining files. * @param {number} limit maximum number of files that can be uploaded * @return {dojo/_base/Deferred} */ confirmNumFileLimit: function(limit) { var dialog = registry.byId('dialogNumFileLimit') || new DialogConfirm({ id: 'dialogNumFileLimit', title: 'Confirm', content: '<p>Maximum number of files to upload is limited to ' + limit + '.</p>' + '<p>Press \'OK\' to upload only the first ' + limit + ' files<br/>or press \'Cancel\' to cancel uploading.</p>', hasSkipCheckBox: false }); return dialog.show(); }, /** * Displays a dialog to confirm deleting a file. * @param {string} fileName name of file to delete * @return {dojo/_base/Deferred} */ confirmDelete: function(fileName) { var dialog = registry.byId('dialogDelete') || new DialogConfirm({ id: 'dialogDelete', title: 'Delete', content: '<p>Do you want to delete the uploaded file ' + fileName + ' on the remote server?</p>' + '<p>Press \'OK\' to delete<br/>or press \'Cancel\' to cancel.</p>', hasSkipCheckBox: true }); return dialog.show(); }, /** * Displays a dialog to confirm that current browser is not supported. * @return {dojo/_base/Deferred} */ confirmHasFeatures: function() { var dialog = registry.byId('dialogHasFeatures') || new DialogConfirm({ id: 'dialogHasFeatures', title: 'Wrong browser', hasCancelButton: false, hasSkipCheckBox: false, content: '<p>Your browser doesn\'t support HTML5 multiple drag and drop upload. Consider downloading Mozilla Firefox.</p>' }); return dialog.show(); }, /** * Removes the overall bar. */ remove: function(container) { var someLeft = array.some(this.files, function(item) { return item !== null; }); if (!someLeft) { window.setTimeout(function() { // TODO: check if this is really necessary // since we might be in progress of already hiding another dialog container.hide(); }, container.get('duration') + 20) } }, /** * Upload all dropped files that don't exceed size and number of files limit. */ uploadFiles: function() { var i = 0, len = this.files.length; for (; i < len; i++) { // this.saveToDb(this.files[i]); this.upload(this.files[i], this.progressBars[i]); } }, /** * Upload file via XmlHttpRequest. * Reads file into binary string and uploads it while displays its progress. * @param {File} file file to upload * @param {snet.fileUploader.ProgressBar} bar progress bar */ upload: function(file, bar) { // Use native XMLHttpRequest instead of XhrGet since dojo 1.5 does not allow to send binary data as per docs var req = bar.xhr = new XMLHttpRequest(); var dfd = this.setReadyStateChangeEvent(req, bar); this.setProgressEvent(req, bar); bar.upload(); if(this.url.indexOf("?") != "-1"){ var elem_param="&"; }else{ var elem_param="?"; } req.open('post', this.url + elem_param + 'fnc=upl', true); req.setRequestHeader("Cache-Control", "no-cache"); req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); req.setRequestHeader("X-File-Name", file.name); req.setRequestHeader("X-File-Size", file.size); req.send(file); return dfd; }, /** * Displays upload status and errors. * @param {XMLHttpRequest} req * @param {snet.fileUploader.ProgressBar} bar */ setReadyStateChangeEvent: function(req, bar) { var dfd = new Deferred(); on(req, 'readystatechange', lang.hitch(this, function() { var err = null; if (req.readyState == 4) { // upload finished successful if (req.status == 200 || req.status == 201) { //window.setTimeout(function() { bar.complete(); dfd.resolve(); if(this.append_div){ dojo.place(req.responseText,this.append_div); } //}, 500); } else { // server error or user aborted (canceled) if (req.status === 0 && (bar.aborted || bar.paused)) { // User canceled or paused upload. Not an error. dfd.resolve(); } else { err = { statusCode: req.status, statusText: req.statusText, responseText: req.responseText }; if (req.statusText == '') { err.responseText = 'Unknown error.'; } bar.error(err); dfd.reject(); this.fileStatus.numError++; } } req = null; bar.xhr = null; } })); return dfd; }, /** * Setup the progress event to display upload progress. * @param {XMLHttpRequest} req * @param {snet/fileUploader/ProgressBar} bar * @param {number} [resumeStart] */ setProgressEvent: function(req, bar, resumeStart) { var cnn = on(req.upload, 'progress', function(evt) { var loaded = evt.loaded + (resumeStart || 0); if (evt.lengthComputable) { //var num = Math.round((evt.total - loaded) / evt.total * 1000); var num = Math.round(loaded / evt.total * 100); // find better measure, see below bar.set('value', loaded); if (num == 100 && (!arguments.callee.done === true)) { // TODO: find better ways to decide when we switch to indeterminate // in FF4 never evt.loaded == bar.maximum, but in chrome // see https://bugzilla.mozilla.org/show_bug.cgi?id=637002 arguments.callee.done = true; cnn.remove(); // make sure this only gets called once per bar after upload is completed bar.wait(); // upload is complete but file has not been written to disk, waits for status 200 } } }); }, /** * Sends request to delete file on remote server. * @param {string} fileName name of file to delete * @param {snet/fileUploader/ProgressBar} bar * @return {dojo/_base/Deferred} */ deleteFile: function(fileName, bar) { return xhr.get({ url: this.url, content: { fnc: 'del', fileName: fileName }, failOk: true, error: function(err, ioArgs) { bar.error({ statusCode: ioArgs.xhr.status, statusText: ioArgs.xhr.statusText, responseText: err.responseText }); } }); }, /** * Deletes the uploaded file and removes the bar from the list. * @param {File} file * @param {snet.fileUploader.ProgressBar} bar */ del: function(file, bar) { var self = this; var dfd = new Deferred(); var fileName = file.name || file.fileName; dfd.resolve(self.rememberConfirmDelete); dfd = dfd.then( function(remember) { if (remember === false) { return self.confirmDelete(fileName); } }).then( function(remember) { self.rememberConfirmDelete = remember; return self.deleteFile(fileName, bar); }).then(function() { bar.remove(); }); return dfd; }, /** * Aborts the upload. * @param {File} file * @param {snet.fileUploader.ProgressBar} bar */ abort: function(file, bar) { if (bar.xhr) { bar.xhr.abort(); } this.fileStatus.numAborted++; var inc = bar.maximum - bar.value; this.barOverall.set('value', this.barOverall.value + inc); window.setTimeout(lang.hitch(this, function() { // after aborting some bytes of the file might still be written to the disk on the server, // Unfortunately the delete request cant be sent right away, since there is some lag on the server // (file is not written yet) and the delete would create a 404. // Ugly, but I just do a the best guess of a 2sec delay to send the delete this.deleteFile(file.name || file.fileName, bar); bar.xhr = null; bar = null; }), 2000); }, resume: function(file, bar) { // Since we do not write anything to disk in this demo, we can't check the size of the partially written file // on the server before continuing, instead we just use the local progress (which of course is not reliable, since // we do not know how much was still sent and saved on the server -> in a realworld app use the extra xhr to server) var dfd = null; if (!this.hasBlobSliceSupport(file)) { var dialog = registry.byId('dialogHasBlobSliceSupport') || new DialogConfirm({ id: 'dialogHasBlobSliceSupport', title: 'Wrong browser', hasCancelButton: false, hasSkipCheckBox: false, content: '<p>Your browser does not support resuming uploads.</p>' }); dialog.show(); } dom.byId(bar.id + '_msg').innerHTML = 'Resuming...'; // Get number of bytes from server which have already been written to the server // This is not possible in demo, since nothing has been written to disk, server will just send nada dfd = xhr.get({ url: this.url, handleAs: 'json', content: { fnc: 'getNumWrittenBytes', fileName: file.name || file.fileName }, failOk: true, error: function(err, ioArgs) { bar.error({ statusCode: ioArgs.xhr.status, statusText: ioArgs.xhr.statusText, responseText: err.responseText }); } }); dfd.then(lang.hitch(this, function(result) { // Blob.slice is temporarily prefixed in Firefox now, because of changed syntax from start, length to start, end var dfd, chunk; var start = (result && result.numWritten) || bar.value; // bar.value for demo only var length = file.size - start; var req = bar.xhr = new XMLHttpRequest(); if ('mozSlice' in file) { chunk = file.mozSlice(start, file.size); // new parameters start, end instead of length } else { chunk = file.slice(start, length); } dfd = this.setReadyStateChangeEvent(req, bar); this.setProgressEvent(req, bar, start); bar.upload(); if(this.url.indexOf("?") != "-1"){ var elem_param="&"; }else{ var elem_param="?"; } req.open('post', this.url + elem_param + 'fnc=resume', true); req.setRequestHeader("Cache-Control", "no-cache"); req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); req.setRequestHeader("X-File-Name", file.name); req.setRequestHeader("X-File-Size", file.size); req.send(chunk); return dfd; })); return dfd; }, /** * Resets the fileUploader. */ reset: function() { this.bytesOverall = 0; this.barOverall = null; this.fileStatus = { numCompleted: 0, numAborted: 0, numProgressDone: 0, numError: 0 }; this.files = []; this.progressBars = []; }, /** * Format file size. * @param {Number} bytes */ formatSize: function(bytes) { var str = ['bytes', 'kb', 'MB', 'GB', 'TB', 'PB']; var num = Math.floor(Math.log(bytes) / Math.log(1024)); bytes = bytes === 0 ? 0 : (bytes / Math.pow(1024, Math.floor(num))).toFixed(1) + ' ' + str[num]; return bytes; }, /** * Check if browser supports necessary features for the uploader to work. */ hasFeatures: function() { // test taken from has.js if (has.add('dnd', function(global, document, anElement) { return 'draggable' in document.createElement('span'); }, true) && has.add('file-api', function(global, document, anElement) { return typeof FileReader != 'undefined'; }, true) && has.add('native-xhr-uploadevents', function(global, document, anElement) { return has("native-xhr") && ("upload" in new XMLHttpRequest); }, true)) { return true } else { return false; } }, hasBlobSliceSupport: function(file) { // file.slice is not supported by FF3.6 and is prefixed in FF5 now return ('slice' in file || 'mozSlice' in file); } }); });