diff options
Diffstat (limited to 'apps/files/js')
-rw-r--r-- | apps/files/js/file-upload.js | 938 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 49 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 948 | ||||
-rw-r--r-- | apps/files/js/files.js | 809 | ||||
-rw-r--r-- | apps/files/js/jquery.fileupload.js | 1023 | ||||
-rw-r--r-- | apps/files/js/jquery.iframe-transport.js | 70 | ||||
-rw-r--r-- | apps/files/js/keyboardshortcuts.js | 6 |
7 files changed, 2535 insertions, 1308 deletions
diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 942a07dfccc..e9663353f74 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -1,343 +1,681 @@ -$(document).ready(function() { - - file_upload_param = { - dropZone: $('#content'), // restrict dropZone to content div - //singleFileUploads is on by default, so the data.files array will always have length 1 - add: function(e, data) { +/** + * The file upload code uses several hooks to interact with blueimps jQuery file upload library: + * 1. the core upload handling hooks are added when initializing the plugin, + * 2. if the browser supports progress events they are added in a separate set after the initialization + * 3. every app can add it's own triggers for fileupload + * - files adds d'n'd handlers and also reacts to done events to add new rows to the filelist + * - TODO pictures upload button + * - TODO music upload button + */ - if(data.files[0].type === '' && data.files[0].size == 4096) - { - data.textStatus = 'dirorzero'; - data.errorThrown = t('files','Unable to upload your file as it is a directory or has 0 bytes'); - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - return true; //don't upload this file but go on with next in queue - } +/** + * Function that will allow us to know if Ajax uploads are supported + * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html + * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata + */ +function supportAjaxUploadWithProgress() { + return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData(); - var totalSize=0; - $.each(data.originalFiles, function(i,file){ - totalSize+=file.size; - }); + // Is the File API supported? + function supportFileAPI() { + var fi = document.createElement('INPUT'); + fi.type = 'file'; + return 'files' in fi; + } - if(totalSize>$('#max_upload').val()){ - data.textStatus = 'notenoughspace'; - data.errorThrown = t('files','Not enough space available'); - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - return false; //don't upload anything - } + // Are progress events supported? + function supportAjaxUploadProgressEvents() { + var xhr = new XMLHttpRequest(); + return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload)); + } - // start the actual file upload - var jqXHR = data.submit(); + // Is FormData supported? + function supportFormData() { + return !! window.FormData; + } +} - // remember jqXHR to show warning to user when he navigates away but an upload is still in progress - if (typeof data.context !== 'undefined' && data.context.data('type') === 'dir') { - var dirName = data.context.data('file'); - if(typeof uploadingFiles[dirName] === 'undefined') { - uploadingFiles[dirName] = {}; +/** + * keeps track of uploads in progress and implements callbacks for the conflicts dialog + * @type {OC.Upload} + */ +OC.Upload = { + _uploads: [], + /** + * deletes the jqHXR object from a data selection + * @param {object} data + */ + deleteUpload:function(data) { + delete data.jqXHR; + }, + /** + * cancels all uploads + */ + cancelUploads:function() { + this.log('canceling uploads'); + jQuery.each(this._uploads,function(i, jqXHR) { + jqXHR.abort(); + }); + this._uploads = []; + }, + rememberUpload:function(jqXHR) { + if (jqXHR) { + this._uploads.push(jqXHR); } - uploadingFiles[dirName][data.files[0].name] = jqXHR; - } else { - uploadingFiles[data.files[0].name] = jqXHR; - } - - //show cancel button - if($('html.lte9').length === 0 && data.dataType !== 'iframe') { - $('#uploadprogresswrapper input.stop').show(); - } }, /** - * called after the first add, does NOT have the data param - * @param e + * Checks the currently known uploads. + * returns true if any hxr has the state 'pending' + * @returns {boolean} + */ + isProcessing:function() { + var count = 0; + + jQuery.each(this._uploads,function(i, data) { + if (data.state() === 'pending') { + count++; + } + }); + return count > 0; + }, + /** + * callback for the conflicts dialog + * @param {object} data */ - start: function(e) { - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - return; - } - $('#uploadprogressbar').progressbar({value:0}); - $('#uploadprogressbar').fadeIn(); + onCancel:function(data) { + this.cancelUploads(); }, - fail: function(e, data) { - if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { - if (data.textStatus === 'abort') { - $('#notification').text(t('files', 'Upload cancelled.')); + /** + * callback for the conflicts dialog + * calls onSkip, onReplace or onAutorename for each conflict + * @param {object} conflicts - list of conflict elements + */ + onContinue:function(conflicts) { + var self = this; + //iterate over all conflicts + jQuery.each(conflicts, function (i, conflict) { + conflict = $(conflict); + var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1; + var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1; + if (keepOriginal && keepReplacement) { + // when both selected -> autorename + self.onAutorename(conflict.data('data')); + } else if (keepReplacement) { + // when only replacement selected -> overwrite + self.onReplace(conflict.data('data')); + } else { + // when only original seleted -> skip + // when none selected -> skip + self.onSkip(conflict.data('data')); + } + }); + }, + /** + * handle skipping an upload + * @param {object} data + */ + onSkip:function(data) { + this.log('skip', null, data); + this.deleteUpload(data); + }, + /** + * handle replacing a file on the server with an uploaded file + * @param {object} data + */ + onReplace:function(data) { + this.log('replace', null, data); + if (data.data) { + data.data.append('resolution', 'replace'); } else { - // HTTP connection problem - $('#notification').text(data.errorThrown); + data.formData.push({name:'resolution', value:'replace'}); //hack for ie8 } - $('#notification').fadeIn(); - //hide notification after 5 sec - setTimeout(function() { - $('#notification').fadeOut(); - }, 5000); - } - delete uploadingFiles[data.files[0].name]; + data.submit(); }, - progress: function(e, data) { - // TODO: show nice progress bar in file row + /** + * handle uploading a file and letting the server decide a new name + * @param {object} data + */ + onAutorename:function(data) { + this.log('autorename', null, data); + if (data.data) { + data.data.append('resolution', 'autorename'); + } else { + data.formData.push({name:'resolution', value:'autorename'}); //hack for ie8 + } + data.submit(); }, - progressall: function(e, data) { - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - return; - } - var progress = (data.loaded/data.total)*100; - $('#uploadprogressbar').progressbar('value',progress); + _trace:false, //TODO implement log handler for JS per class? + log:function(caption, e, data) { + if (this._trace) { + console.log(caption); + console.log(data); + } }, /** - * called for every successful upload - * @param e - * @param data + * TODO checks the list of existing files prior to uploading and shows a simple dialog to choose + * skip all, replace all or choose which files to keep + * @param {array} selection of files to upload + * @param {object} callbacks - object with several callback methods + * @param {function} callbacks.onNoConflicts + * @param {function} callbacks.onSkipConflicts + * @param {function} callbacks.onReplaceConflicts + * @param {function} callbacks.onChooseConflicts + * @param {function} callbacks.onCancel */ - done:function(e, data) { - // handle different responses (json or body from iframe for ie) - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - //fetch response from iframe - response = data.result[0].body.innerText; - } - var result=$.parseJSON(response); + checkExistingFiles: function (selection, callbacks) { + // TODO check filelist before uploading and show dialog on conflicts, use callbacks + callbacks.onNoConflicts(selection); + } +}; - if(typeof result[0] !== 'undefined' && result[0].status === 'success') { - var file = result[0]; - } else { - data.textStatus = 'servererror'; - data.errorThrown = t('files', result.data.message); - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - } +$(document).ready(function() { - var filename = result[0].originalname; + if ( $('#file_upload_start').exists() ) { - // delete jqXHR reference - if (typeof data.context !== 'undefined' && data.context.data('type') === 'dir') { - var dirName = data.context.data('file'); - delete uploadingFiles[dirName][filename]; - if ($.assocArraySize(uploadingFiles[dirName]) == 0) { - delete uploadingFiles[dirName]; - } - } else { - delete uploadingFiles[filename]; - } + var file_upload_param = { + dropZone: $('#content'), // restrict dropZone to content div + autoUpload: false, + sequentialUploads: true, + //singleFileUploads is on by default, so the data.files array will always have length 1 + /** + * on first add of every selection + * - check all files of originalFiles array with files in dir + * - on conflict show dialog + * - skip all -> remember as single skip action for all conflicting files + * - replace all -> remember as single replace action for all conflicting files + * - choose -> show choose dialog + * - mark files to keep + * - when only existing -> remember as single skip action + * - when only new -> remember as single replace action + * - when both -> remember as single autorename action + * - start uploading selection + * @param {object} e + * @param {object} data + * @returns {boolean} + */ + add: function(e, data) { + OC.Upload.log('add', e, data); + var that = $(this); + + // we need to collect all data upload objects before starting the upload so we can check their existence + // and set individual conflict actions. unfortunately there is only one variable that we can use to identify + // the selection a data upload is part of, so we have to collect them in data.originalFiles + // turning singleFileUploads off is not an option because we want to gracefully handle server errors like + // already exists + + // create a container where we can store the data objects + if ( ! data.originalFiles.selection ) { + // initialize selection and remember number of files to upload + data.originalFiles.selection = { + uploads: [], + filesToUpload: data.originalFiles.length, + totalBytes: 0 + }; + } + var selection = data.originalFiles.selection; + + // add uploads + if ( selection.uploads.length < selection.filesToUpload ) { + // remember upload + selection.uploads.push(data); + } + + //examine file + var file = data.files[0]; + + if (file.type === '' && file.size === 4096) { + data.textStatus = 'dirorzero'; + data.errorThrown = t('files', 'Unable to upload {filename} as it is a directory or has 0 bytes', + {filename: file.name} + ); + } + + // add size + selection.totalBytes += file.size; + + //check max upload size + if (selection.totalBytes > $('#max_upload').val()) { + data.textStatus = 'notenoughspace'; + data.errorThrown = t('files', 'Not enough space available'); + } + + // end upload for whole selection on error + if (data.errorThrown) { + // trigger fileupload fail + var fu = that.data('blueimp-fileupload') || that.data('fileupload'); + fu._trigger('fail', e, data); + return false; //don't upload anything + } - }, - /** - * called after last upload - * @param e - * @param data - */ - stop: function(e, data) { - if(data.dataType !== 'iframe') { - $('#uploadprogresswrapper input.stop').hide(); - } + // check existing files when all is collected + if ( selection.uploads.length >= selection.filesToUpload ) { + + //remove our selection hack: + delete data.originalFiles.selection; - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - return; - } + var callbacks = { + + onNoConflicts: function (selection) { + $.each(selection.uploads, function(i, upload) { + upload.submit(); + }); + }, + onSkipConflicts: function (selection) { + //TODO mark conflicting files as toskip + }, + onReplaceConflicts: function (selection) { + //TODO mark conflicting files as toreplace + }, + onChooseConflicts: function (selection) { + //TODO mark conflicting files as chosen + }, + onCancel: function (selection) { + $.each(selection.uploads, function(i, upload) { + upload.abort(); + }); + } + }; - $('#uploadprogressbar').progressbar('value',100); - $('#uploadprogressbar').fadeOut(); - } - } - var file_upload_handler = function() { - $('#file_upload_start').fileupload(file_upload_param); - }; + OC.Upload.checkExistingFiles(selection, callbacks); + + } + return true; // continue adding files + }, + /** + * called after the first add, does NOT have the data param + * @param {object} e + */ + start: function(e) { + OC.Upload.log('start', e, null); + }, + submit: function(e, data) { + OC.Upload.rememberUpload(data); + if ( ! data.formData ) { + // noone set update parameters, we set the minimum + data.formData = { + requesttoken: oc_requesttoken, + dir: $('#dir').val() + }; + } + }, + fail: function(e, data) { + OC.Upload.log('fail', e, data); + if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { + if (data.textStatus === 'abort') { + OC.Notification.show(t('files', 'Upload cancelled.')); + } else { + // HTTP connection problem + OC.Notification.show(data.errorThrown); + } + //hide notification after 10 sec + setTimeout(function() { + OC.Notification.hide(); + }, 10000); + } + OC.Upload.deleteUpload(data); + }, + /** + * called for every successful upload + * @param {object} e + * @param {object} data + */ + done:function(e, data) { + OC.Upload.log('done', e, data); + // handle different responses (json or body from iframe for ie) + var response; + if (typeof data.result === 'string') { + response = data.result; + } else { + //fetch response from iframe + response = data.result[0].body.innerText; + } + var result=$.parseJSON(response); + delete data.jqXHR; - if ( document.getElementById('data-upload-form') ) { - $(file_upload_handler); - } - $.assocArraySize = function(obj) { - // http://stackoverflow.com/a/6700/11236 - var size = 0, key; - for (key in obj) { - if (obj.hasOwnProperty(key)) size++; - } - return size; - }; + if (result.status === 'error' && result.data && result.data.message){ + data.textStatus = 'servererror'; + data.errorThrown = result.data.message; + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } else if (typeof result[0] === 'undefined') { + data.textStatus = 'servererror'; + data.errorThrown = t('files', 'Could not get result from server.'); + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } else if (result[0].status === 'existserror') { + //show "file already exists" dialog + var original = result[0]; + var replacement = data.files[0]; + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + OC.dialogs.fileexists(data, original, replacement, OC.Upload, fu); + } else if (result[0].status !== 'success') { + //delete data.jqXHR; + data.textStatus = 'servererror'; + data.errorThrown = result[0].data.message; // error message has been translated on server + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } + }, + /** + * called after last upload + * @param {object} e + * @param {object} data + */ + stop: function(e, data) { + OC.Upload.log('stop', e, data); + } + }; - // warn user not to leave the page while upload is in progress - $(window).bind('beforeunload', function(e) { - if ($.assocArraySize(uploadingFiles) > 0) - return t('files','File upload is in progress. Leaving the page now will cancel the upload.'); - }); + // initialize jquery fileupload (blueimp) + var fileupload = $('#file_upload_start').fileupload(file_upload_param); + window.file_upload_param = fileupload; - //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used) - if(navigator.userAgent.search(/konqueror/i)==-1){ - $('#file_upload_start').attr('multiple','multiple') - } + if (supportAjaxUploadWithProgress()) { - //if the breadcrumb is to long, start by replacing foldernames with '...' except for the current folder - var crumb=$('div.crumb').first(); - while($('div.controls').height()>40 && crumb.next('div.crumb').length>0){ - crumb.children('a').text('...'); - crumb=crumb.next('div.crumb'); - } - //if that isn't enough, start removing items from the breacrumb except for the current folder and it's parent - var crumb=$('div.crumb').first(); - var next=crumb.next('div.crumb'); - while($('div.controls').height()>40 && next.next('div.crumb').length>0){ - crumb.remove(); - crumb=next; - next=crumb.next('div.crumb'); - } - //still not enough, start shorting down the current folder name - var crumb=$('div.crumb>a').last(); - while($('div.controls').height()>40 && crumb.text().length>6){ - var text=crumb.text() - text=text.substr(0,text.length-6)+'...'; - crumb.text(text); - } + // add progress handlers + fileupload.on('fileuploadadd', function(e, data) { + OC.Upload.log('progress handle fileuploadadd', e, data); + //show cancel button + //if (data.dataType !== 'iframe') { //FIXME when is iframe used? only for ie? + // $('#uploadprogresswrapper input.stop').show(); + //} + }); + // add progress handlers + fileupload.on('fileuploadstart', function(e, data) { + OC.Upload.log('progress handle fileuploadstart', e, data); + $('#uploadprogresswrapper input.stop').show(); + $('#uploadprogressbar').progressbar({value:0}); + $('#uploadprogressbar').fadeIn(); + }); + fileupload.on('fileuploadprogress', function(e, data) { + OC.Upload.log('progress handle fileuploadprogress', e, data); + //TODO progressbar in row + }); + fileupload.on('fileuploadprogressall', function(e, data) { + OC.Upload.log('progress handle fileuploadprogressall', e, data); + var progress = (data.loaded / data.total) * 100; + $('#uploadprogressbar').progressbar('value', progress); + }); + fileupload.on('fileuploadstop', function(e, data) { + OC.Upload.log('progress handle fileuploadstop', e, data); + + $('#uploadprogresswrapper input.stop').fadeOut(); + $('#uploadprogressbar').fadeOut(); + Files.updateStorageStatistics(); + }); + fileupload.on('fileuploadfail', function(e, data) { + OC.Upload.log('progress handle fileuploadfail', e, data); + //if user pressed cancel hide upload progress bar and cancel button + if (data.errorThrown === 'abort') { + $('#uploadprogresswrapper input.stop').fadeOut(); + $('#uploadprogressbar').fadeOut(); + } + }); - $(document).click(function(){ - $('#new>ul').hide(); - $('#new').removeClass('active'); - $('#new li').each(function(i,element){ - if($(element).children('p').length==0){ - $(element).children('form').remove(); - $(element).append('<p>'+$(element).data('text')+'</p>'); - } - }); - }); - $('#new li').click(function(){ - if($(this).children('p').length==0){ - return; + } else { + console.log('skipping file progress because your browser is broken'); + } } - $('#new li').each(function(i,element){ - if($(element).children('p').length==0){ - $(element).children('form').remove(); - $(element).append('<p>'+$(element).data('text')+'</p>'); - } - }); - - var type=$(this).data('type'); - var text=$(this).children('p').text(); - $(this).data('text',text); - $(this).children('p').remove(); - var form=$('<form></form>'); - var input=$('<input>'); - form.append(input); - $(this).append(form); - input.focus(); - form.submit(function(event){ - event.stopPropagation(); - event.preventDefault(); - var newname=input.val(); - if(type == 'web' && newname.length == 0) { - OC.Notification.show(t('files', 'URL cannot be empty.')); - return false; - } else if (type != 'web' && !Files.isFileNameValid(newname)) { - return false; - } else if( type == 'folder' && $('#dir').val() == '/' && newname == 'Shared') { - OC.Notification.show(t('files','Invalid folder name. Usage of \'Shared\' is reserved by ownCloud')); - return false; - } - if (FileList.lastAction) { - FileList.lastAction(); - } - var name = getUniqueName(newname); - if (newname != name) { - FileList.checkName(name, newname, true); - var hidden = true; - } else { - var hidden = false; - } - switch(type){ - case 'file': - $.post( - OC.filePath('files','ajax','newfile.php'), - {dir:$('#dir').val(),filename:name}, - function(result){ - if (result.status == 'success') { - var date=new Date(); - FileList.addFile(name,0,date,false,hidden); - var tr=$('tr').filterAttr('data-file',name); - tr.attr('data-mime',result.data.mime); - tr.attr('data-id', result.data.id); - getMimeIcon(result.data.mime,function(path){ - tr.find('td.filename').attr('style','background-image:url('+path+')'); - }); - } else { - OC.dialogs.alert(result.data.message, t('core', 'Error')); + $.assocArraySize = function(obj) { + // http://stackoverflow.com/a/6700/11236 + var size = 0, key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + size++; } - } - ); - break; - case 'folder': - $.post( - OC.filePath('files','ajax','newfolder.php'), - {dir:$('#dir').val(),foldername:name}, - function(result){ - if (result.status == 'success') { - var date=new Date(); - FileList.addDir(name,0,date,hidden); - var tr=$('tr').filterAttr('data-file',name); - tr.attr('data-id', result.data.id); - } else { - OC.dialogs.alert(result.data.message, t('core', 'Error')); - } - } - ); - break; - case 'web': - if(name.substr(0,8)!='https://' && name.substr(0,7)!='http://'){ - name='http://'+name; } - var localName=name; - if(localName.substr(localName.length-1,1)=='/'){//strip / - localName=localName.substr(0,localName.length-1) + return size; + }; + + // warn user not to leave the page while upload is in progress + $(window).on('beforeunload', function(e) { + if (OC.Upload.isProcessing()) { + return t('files', 'File upload is in progress. Leaving the page now will cancel the upload.'); } - if(localName.indexOf('/')){//use last part of url - localName=localName.split('/').pop(); - } else { //or the domain - localName=(localName.match(/:\/\/(.[^\/]+)/)[1]).replace('www.',''); + }); + + //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used) + if (navigator.userAgent.search(/konqueror/i) === -1) { + $('#file_upload_start').attr('multiple', 'multiple'); + } + + //if the breadcrumb is to long, start by replacing foldernames with '...' except for the current folder + var crumb=$('div.crumb').first(); + while($('div.controls').height() > 40 && crumb.next('div.crumb').length > 0) { + crumb.children('a').text('...'); + crumb = crumb.next('div.crumb'); + } + //if that isn't enough, start removing items from the breacrumb except for the current folder and it's parent + var crumb = $('div.crumb').first(); + var next = crumb.next('div.crumb'); + while($('div.controls').height()>40 && next.next('div.crumb').length > 0) { + crumb.remove(); + crumb = next; + next = crumb.next('div.crumb'); + } + //still not enough, start shorting down the current folder name + var crumb=$('div.crumb>a').last(); + while($('div.controls').height() > 40 && crumb.text().length > 6) { + var text=crumb.text(); + text = text.substr(0,text.length-6)+'...'; + crumb.text(text); + } + + $(document).click(function(ev) { + // do not close when clicking in the dropdown + if ($(ev.target).closest('#new').length){ + return; } - localName = getUniqueName(localName); - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - } else { - $('#uploadprogressbar').progressbar({value:0}); - $('#uploadprogressbar').fadeIn(); + $('#new>ul').hide(); + $('#new').removeClass('active'); + if ($('#new .error').length > 0) { + $('#new .error').tipsy('hide'); + } + $('#new li').each(function(i,element) { + if ($(element).children('p').length === 0) { + $(element).children('form').remove(); + $(element).append('<p>'+$(element).data('text')+'</p>'); + } + }); + }); + $('#new').click(function(event) { + event.stopPropagation(); + }); + $('#new>a').click(function() { + $('#new>ul').toggle(); + $('#new').toggleClass('active'); + }); + $('#new li').click(function() { + if ($(this).children('p').length === 0) { + return; } + + $('#new .error').tipsy('hide'); - var eventSource=new OC.EventSource(OC.filePath('files','ajax','newfile.php'),{dir:$('#dir').val(),source:name,filename:localName}); - eventSource.listen('progress',function(progress){ - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - } else { - $('#uploadprogressbar').progressbar('value',progress); - } + $('#new li').each(function(i,element) { + if ($(element).children('p').length === 0) { + $(element).children('form').remove(); + $(element).append('<p>'+$(element).data('text')+'</p>'); + } }); - eventSource.listen('success',function(data){ - var mime=data.mime; - var size=data.size; - var id=data.id; - $('#uploadprogressbar').fadeOut(); - var date=new Date(); - FileList.addFile(localName,size,date,false,hidden); - var tr=$('tr').filterAttr('data-file',localName); - tr.data('mime',mime).data('id',id); - tr.attr('data-id', id); - getMimeIcon(mime,function(path){ - tr.find('td.filename').attr('style','background-image:url('+path+')'); - }); + + var type=$(this).data('type'); + var text=$(this).children('p').text(); + $(this).data('text',text); + $(this).children('p').remove(); + + // add input field + var form = $('<form></form>'); + var input = $('<input type="text">'); + var newName = $(this).attr('data-newname') || ''; + if (newName) { + input.val(newName); + } + form.append(input); + $(this).append(form); + var lastPos; + var checkInput = function () { + var filename = input.val(); + if (type === 'web' && filename.length === 0) { + throw t('files', 'URL cannot be empty'); + } else if (type !== 'web' && !Files.isFileNameValid(filename)) { + // Files.isFileNameValid(filename) throws an exception itself + } else if ($('#dir').val() === '/' && filename === 'Shared') { + throw t('files', 'In the home folder \'Shared\' is a reserved filename'); + } else if (FileList.inList(filename)) { + throw t('files', '{new_name} already exists', {new_name: filename}); + } else { + return true; + } + }; + + // verify filename on typing + input.keyup(function(event) { + try { + checkInput(); + input.tipsy('hide'); + input.removeClass('error'); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } }); - eventSource.listen('error',function(error){ - $('#uploadprogressbar').fadeOut(); - alert(error); + + input.focus(); + // pre select name up to the extension + lastPos = newName.lastIndexOf('.'); + if (lastPos === -1) { + lastPos = newName.length; + } + input.selectRange(0, lastPos); + form.submit(function(event) { + event.stopPropagation(); + event.preventDefault(); + try { + checkInput(); + var newname = input.val(); + if (FileList.lastAction) { + FileList.lastAction(); + } + var name = getUniqueName(newname); + if (newname !== name) { + FileList.checkName(name, newname, true); + var hidden = true; + } else { + var hidden = false; + } + switch(type) { + case 'file': + $.post( + OC.filePath('files', 'ajax', 'newfile.php'), + {dir:$('#dir').val(), filename:name}, + function(result) { + if (result.status === 'success') { + var date = new Date(); + // TODO: ideally addFile should be able to receive + // all attributes and set them automatically, + // and also auto-load the preview + var tr = FileList.addFile(name, 0, date, false, hidden); + tr.attr('data-size', result.data.size); + tr.attr('data-mime', result.data.mime); + tr.attr('data-id', result.data.id); + tr.attr('data-etag', result.data.etag); + tr.find('.filesize').text(humanFileSize(result.data.size)); + var path = getPathForPreview(name); + Files.lazyLoadPreview(path, result.data.mime, function(previewpath) { + tr.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }, null, null, result.data.etag); + FileActions.display(tr.find('td.filename'), true); + } else { + OC.dialogs.alert(result.data.message, t('core', 'Could not create file')); + } + } + ); + break; + case 'folder': + $.post( + OC.filePath('files','ajax','newfolder.php'), + {dir:$('#dir').val(), foldername:name}, + function(result) { + if (result.status === 'success') { + var date=new Date(); + FileList.addDir(name, 0, date, hidden); + var tr=$('tr[data-file="'+name+'"]'); + tr.attr('data-id', result.data.id); + } else { + OC.dialogs.alert(result.data.message, t('core', 'Could not create folder')); + } + } + ); + break; + case 'web': + if (name.substr(0,8) !== 'https://' && name.substr(0,7) !== 'http://') { + name = 'http://' + name; + } + var localName=name; + if (localName.substr(localName.length-1,1)==='/') {//strip / + localName=localName.substr(0,localName.length-1); + } + if (localName.indexOf('/')) {//use last part of url + localName=localName.split('/').pop(); + } else { //or the domain + localName=(localName.match(/:\/\/(.[^\/]+)/)[1]).replace('www.',''); + } + localName = getUniqueName(localName); + //IE < 10 does not fire the necessary events for the progress bar. + if ($('html.lte9').length === 0) { + $('#uploadprogressbar').progressbar({value:0}); + $('#uploadprogressbar').fadeIn(); + } + + var eventSource=new OC.EventSource(OC.filePath('files','ajax','newfile.php'),{dir:$('#dir').val(),source:name,filename:localName}); + eventSource.listen('progress',function(progress) { + //IE < 10 does not fire the necessary events for the progress bar. + if ($('html.lte9').length === 0) { + $('#uploadprogressbar').progressbar('value',progress); + } + }); + eventSource.listen('success',function(data) { + var mime = data.mime; + var size = data.size; + var id = data.id; + $('#uploadprogressbar').fadeOut(); + var date = new Date(); + FileList.addFile(localName, size, date, false, hidden); + var tr = $('tr[data-file="'+localName+'"]'); + tr.data('mime', mime).data('id', id); + tr.attr('data-id', id); + var path = $('#dir').val()+'/'+localName; + Files.lazyLoadPreview(path, mime, function(previewpath) { + tr.find('td.filename').attr('style', 'background-image:url('+previewpath+')'); + }, null, null, data.etag); + FileActions.display(tr.find('td.filename'), true); + }); + eventSource.listen('error',function(error) { + $('#uploadprogressbar').fadeOut(); + alert(error); + }); + break; + } + var li=form.parent(); + form.remove(); + /* workaround for IE 9&10 click event trap, 2 lines: */ + $('input').first().focus(); + $('#content').focus(); + li.append('<p>'+li.data('text')+'</p>'); + $('#new>a').click(); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } }); - break; - } - var li=form.parent(); - form.remove(); - li.append('<p>'+li.data('text')+'</p>'); - $('#new>a').click(); }); - }); + window.file_upload_param = file_upload_param; }); diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index aa66a57a7b5..03e23189a97 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -61,13 +61,22 @@ var FileActions = { var actions = this.get(mime, type, permissions); return actions[name]; }, - display: function (parent) { + /** + * Display file actions for the given element + * @param parent "td" element of the file for which to display actions + * @param triggerEvent if true, triggers the fileActionsReady on the file + * list afterwards (false by default) + */ + display: function (parent, triggerEvent) { FileActions.currentFile = parent; var actions = FileActions.get(FileActions.getCurrentMimeType(), FileActions.getCurrentType(), FileActions.getCurrentPermissions()); var file = FileActions.getCurrentFile(); - if ($('tr').filterAttr('data-file', file).data('renaming')) { + if ($('tr[data-file="'+file+'"]').data('renaming')) { return; } + + // recreate fileactions + parent.children('a.name').find('.fileactions').remove(); parent.children('a.name').append('<span class="fileactions" />'); var defaultAction = FileActions.getDefault(FileActions.getCurrentMimeType(), FileActions.getCurrentType(), FileActions.getCurrentPermissions()); @@ -117,24 +126,27 @@ var FileActions = { addAction('Share', actions.Share); } + // remove the existing delete action + parent.parent().children().last().find('.action.delete').remove(); if (actions['Delete']) { var img = FileActions.icons['Delete']; if (img.call) { img = img(file); } if (typeof trashBinApp !== 'undefined' && trashBinApp) { - var html = '<a href="#" original-title="' + t('files', 'Delete permanently') + '" class="action delete" />'; + var html = '<a href="#" original-title="' + t('files', 'Delete permanently') + '" class="action delete delete-icon" />'; } else { - var html = '<a href="#" original-title="' + t('files', 'Delete') + '" class="action delete" />'; + var html = '<a href="#" class="action delete delete-icon" />'; } var element = $(html); - if (img) { - element.append($('<img class ="svg" src="' + img + '"/>')); - } element.data('action', actions['Delete']); element.on('click', {a: null, elem: parent, actionFunc: actions['Delete']}, actionHandler); parent.parent().children().last().append(element); } + + if (triggerEvent){ + $('#fileList').trigger(jQuery.Event("fileActionsReady")); + } }, getCurrentFile: function () { return FileActions.currentFile.parent().attr('data-file'); @@ -164,30 +176,18 @@ $(document).ready(function () { window.location = OC.filePath('files', 'ajax', 'download.php') + '?files=' + encodeURIComponent(filename) + '&dir=' + encodeURIComponent($('#dir').val()); }); } - $('#fileList tr').each(function () { FileActions.display($(this).children('td.filename')); }); + + $('#fileList').trigger(jQuery.Event("fileActionsReady")); }); FileActions.register('all', 'Delete', OC.PERMISSION_DELETE, function () { return OC.imagePath('core', 'actions/delete'); }, function (filename) { - if (Files.cancelUpload(filename)) { - if (filename.substr) { - filename = [filename]; - } - $.each(filename, function (index, file) { - var filename = $('tr').filterAttr('data-file', file); - filename.hide(); - filename.find('input[type="checkbox"]').removeAttr('checked'); - filename.removeClass('selected'); - }); - procesSelection(); - } else { - FileList.do_delete(filename); - } + FileList.do_delete(filename); $('.tipsy').remove(); }); @@ -198,13 +198,12 @@ FileActions.register('all', 'Rename', OC.PERMISSION_UPDATE, function () { FileList.rename(filename); }); - FileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) { - var dir = $('#dir').val() + var dir = $('#dir').val() || '/'; if (dir !== '/') { dir = dir + '/'; } - window.location = OC.linkTo('files', 'index.php') + '?dir=' + encodeURIComponent(dir + filename); + FileList.changeDirectory(dir + filename); }); FileActions.setDefault('dir', 'Open'); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index b858e2580ee..473bcf25f2d 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1,9 +1,28 @@ var FileList={ useUndo:true, + postProcessList: function() { + $('#fileList tr').each(function() { + //little hack to set unescape filenames in attribute + $(this).attr('data-file',decodeURIComponent($(this).attr('data-file'))); + }); + }, update:function(fileListHtml) { - $('#fileList').empty().html(fileListHtml); + var $fileList = $('#fileList'); + $fileList.empty().html(fileListHtml); + FileList.updateEmptyContent(); + $fileList.find('tr').each(function () { + FileActions.display($(this).children('td.filename')); + }); + $fileList.trigger(jQuery.Event("fileActionsReady")); + FileList.postProcessList(); + // "Files" might not be loaded in extending apps + if (window.Files) { + Files.setupDragAndDrop(); + } + FileList.updateFileSummary(); + $fileList.trigger(jQuery.Event("updated")); }, - createRow:function(type, name, iconurl, linktarget, size, lastModified, permissions){ + createRow:function(type, name, iconurl, linktarget, size, lastModified, permissions) { var td, simpleSize, basename, extension; //containing tr var tr = $('<tr></tr>').attr({ @@ -15,15 +34,16 @@ var FileList={ // filename td td = $('<td></td>').attr({ "class": "filename", - "style": 'background-image:url('+iconurl+')' + "style": 'background-image:url('+iconurl+'); background-size: 32px;' }); - td.append('<input type="checkbox" />'); + var rand = Math.random().toString(16).slice(2); + td.append('<input id="select-'+rand+'" type="checkbox" /><label for="select-'+rand+'"></label>'); var link_elem = $('<a></a>').attr({ "class": "name", "href": linktarget }); //split extension from filename for non dirs - if (type != 'dir' && name.indexOf('.')!=-1) { + if (type !== 'dir' && name.indexOf('.') !== -1) { basename=name.substr(0,name.lastIndexOf('.')); extension=name.substr(name.lastIndexOf('.')); } else { @@ -32,11 +52,11 @@ var FileList={ } var name_span=$('<span></span>').addClass('nametext').text(basename); link_elem.append(name_span); - if(extension){ + if (extension) { name_span.append($('<span></span>').addClass('extension').text(extension)); } //dirs can show the number of uploaded files - if (type == 'dir') { + if (type === 'dir') { link_elem.append($('<span></span>').attr({ 'class': 'uploadtext', 'currentUploads': 0 @@ -46,9 +66,9 @@ var FileList={ tr.append(td); //size column - if(size!=t('files', 'Pending')){ + if (size !== t('files', 'Pending')) { simpleSize = humanFileSize(size); - }else{ + } else { simpleSize=t('files', 'Pending'); } var sizeColor = Math.round(160-Math.pow((size/(1024*1024)),2)); @@ -70,7 +90,7 @@ var FileList={ tr.append(td); return tr; }, - addFile:function(name,size,lastModified,loading,hidden,param){ + addFile:function(name, size, lastModified, loading, hidden, param) { var imgurl; if (!param) { @@ -100,18 +120,17 @@ var FileList={ ); FileList.insertElement(name, 'file', tr); - if(loading){ - tr.data('loading',true); - }else{ + if (loading) { + tr.data('loading', true); + } else { tr.find('td.filename').draggable(dragOptions); } if (hidden) { tr.hide(); } - FileActions.display(tr.find('td.filename')); return tr; }, - addDir:function(name,size,lastModified,hidden){ + addDir:function(name, size, lastModified, hidden) { var tr = this.createRow( 'dir', @@ -123,83 +142,221 @@ var FileList={ $('#permissions').val() ); - FileList.insertElement(name,'dir',tr); + FileList.insertElement(name, 'dir', tr); var td = tr.find('td.filename'); td.draggable(dragOptions); td.droppable(folderDropOptions); if (hidden) { tr.hide(); } - FileActions.display(tr.find('td.filename')); + FileActions.display(tr.find('td.filename'), true); return tr; }, - refresh:function(data) { - var result = jQuery.parseJSON(data.responseText); - if(typeof(result.data.breadcrumb) != 'undefined'){ - updateBreadcrumb(result.data.breadcrumb); + getCurrentDirectory: function(){ + return $('#dir').val() || '/'; + }, + /** + * @brief Changes the current directory and reload the file list. + * @param targetDir target directory (non URL encoded) + * @param changeUrl false if the URL must not be changed (defaults to true) + * @param {boolean} force set to true to force changing directory + */ + changeDirectory: function(targetDir, changeUrl, force) { + var $dir = $('#dir'), + url, + currentDir = $dir.val() || '/'; + targetDir = targetDir || '/'; + if (!force && currentDir === targetDir) { + return; } + FileList.setCurrentDir(targetDir, changeUrl); + $('#fileList').trigger( + jQuery.Event('changeDirectory', { + dir: targetDir, + previousDir: currentDir + } + )); + FileList.reload(); + }, + linkTo: function(dir) { + return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/'); + }, + setCurrentDir: function(targetDir, changeUrl) { + $('#dir').val(targetDir); + if (changeUrl !== false) { + if (window.history.pushState && changeUrl !== false) { + url = FileList.linkTo(targetDir); + window.history.pushState({dir: targetDir}, '', url); + } + // use URL hash for IE8 + else{ + window.location.hash = '?dir='+ encodeURIComponent(targetDir).replace(/%2F/g, '/'); + } + } + }, + /** + * @brief Reloads the file list using ajax call + */ + reload: function() { + FileList.showMask(); + if (FileList._reloadCall) { + FileList._reloadCall.abort(); + } + FileList._reloadCall = $.ajax({ + url: OC.filePath('files','ajax','list.php'), + data: { + dir : $('#dir').val(), + breadcrumb: true + }, + error: function(result) { + FileList.reloadCallback(result); + }, + success: function(result) { + FileList.reloadCallback(result); + } + }); + }, + reloadCallback: function(result) { + var $controls = $('#controls'); + + delete FileList._reloadCall; + FileList.hideMask(); + + if (!result || result.status === 'error') { + OC.Notification.show(result.data.message); + return; + } + + if (result.status === 404) { + // go back home + FileList.changeDirectory('/'); + return; + } + + // TODO: should rather return upload file size through + // the files list ajax call + Files.updateStorageStatistics(true); + + if (result.data.permissions) { + FileList.setDirectoryPermissions(result.data.permissions); + } + + if (typeof(result.data.breadcrumb) !== 'undefined') { + $controls.find('.crumb').remove(); + $controls.prepend(result.data.breadcrumb); + + var width = $(window).width(); + Files.initBreadCrumbs(); + Files.resizeBreadcrumbs(width, true); + + // in case svg is not supported by the browser we need to execute the fallback mechanism + if (!SVGSupport()) { + replaceSVG(); + } + } + FileList.update(result.data.files); - resetFileActionPanel(); + }, + setDirectoryPermissions: function(permissions) { + var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; + $('#permissions').val(permissions); + $('.creatable').toggleClass('hidden', !isCreatable); + $('.notCreatable').toggleClass('hidden', isCreatable); + }, + /** + * Shows/hides action buttons + * + * @param show true for enabling, false for disabling + */ + showActions: function(show){ + $('.actions,#file_action_panel').toggleClass('hidden', !show); + if (show){ + // make sure to display according to permissions + var permissions = $('#permissions').val(); + var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; + $('.creatable').toggleClass('hidden', !isCreatable); + $('.notCreatable').toggleClass('hidden', isCreatable); + } + else{ + $('.creatable, .notCreatable').addClass('hidden'); + } + }, + /** + * Enables/disables viewer mode. + * In viewer mode, apps can embed themselves under the controls bar. + * In viewer mode, the actions of the file list will be hidden. + * @param show true for enabling, false for disabling + */ + setViewerMode: function(show){ + this.showActions(!show); + $('#filestable').toggleClass('hidden', show); }, remove:function(name){ $('tr').filterAttr('data-file',name).find('td.filename').draggable('destroy'); $('tr').filterAttr('data-file',name).remove(); - if($('tr[data-file]').length==0){ - $('#emptyfolder').show(); + FileList.updateFileSummary(); + if ( ! $('tr[data-file]').exists() ) { + $('#emptycontent').removeClass('hidden'); + $('#filescontent th').addClass('hidden'); } }, - insertElement:function(name,type,element){ + insertElement:function(name, type, element) { //find the correct spot to insert the file or folder var pos, fileElements=$('tr[data-file][data-type="'+type+'"]:visible'); - if(name.localeCompare($(fileElements[0]).attr('data-file'))<0){ - pos=-1; - }else if(name.localeCompare($(fileElements[fileElements.length-1]).attr('data-file'))>0){ - pos=fileElements.length-1; - }else{ - for(pos=0;pos<fileElements.length-1;pos++){ - if(name.localeCompare($(fileElements[pos]).attr('data-file'))>0 && name.localeCompare($(fileElements[pos+1]).attr('data-file'))<0){ + if (name.localeCompare($(fileElements[0]).attr('data-file')) < 0) { + pos = -1; + } else if (name.localeCompare($(fileElements[fileElements.length-1]).attr('data-file')) > 0) { + pos = fileElements.length - 1; + } else { + for(pos = 0; pos<fileElements.length-1; pos++) { + if (name.localeCompare($(fileElements[pos]).attr('data-file')) > 0 + && name.localeCompare($(fileElements[pos+1]).attr('data-file')) < 0) + { break; } } } - if(fileElements.length){ - if(pos==-1){ + if (fileElements.exists()) { + if (pos === -1) { $(fileElements[0]).before(element); - }else{ + } else { $(fileElements[pos]).after(element); } - }else if(type=='dir' && $('tr[data-file]').length>0){ + } else if (type === 'dir' && $('tr[data-file]').exists()) { $('tr[data-file]').first().before(element); - } else if(type=='file' && $('tr[data-file]').length>0) { + } else if (type === 'file' && $('tr[data-file]').exists()) { $('tr[data-file]').last().before(element); - }else{ + } else { $('#fileList').append(element); } - $('#emptyfolder').hide(); + $('#emptycontent').addClass('hidden'); + $('#filestable th').removeClass('hidden'); + FileList.updateFileSummary(); }, - loadingDone:function(name, id){ - var mime, tr=$('tr').filterAttr('data-file',name); - tr.data('loading',false); - mime=tr.data('mime'); - tr.attr('data-mime',mime); - if (id != null) { + loadingDone:function(name, id) { + var mime, tr = $('tr[data-file="'+name+'"]'); + tr.data('loading', false); + mime = tr.data('mime'); + tr.attr('data-mime', mime); + if (id) { tr.attr('data-id', id); } - getMimeIcon(mime,function(path){ - tr.find('td.filename').attr('style','background-image:url('+path+')'); - }); + var path = getPathForPreview(name); + Files.lazyLoadPreview(path, mime, function(previewpath) { + tr.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }, null, null, tr.attr('data-etag')); tr.find('td.filename').draggable(dragOptions); }, - isLoading:function(name){ - return $('tr').filterAttr('data-file',name).data('loading'); + isLoading:function(name) { + return $('tr[data-file="'+name+'"]').data('loading'); }, - rename:function(name){ + rename:function(oldname) { var tr, td, input, form; - tr=$('tr').filterAttr('data-file',name); + tr = $('tr[data-file="'+oldname+'"]'); tr.data('renaming',true); - td=tr.children('td.filename'); - input=$('<input class="filename"/>').val(name); - form=$('<form></form>'); + td = tr.children('td.filename'); + input = $('<input type="text" class="filename"/>').val(oldname); + form = $('<form></form>'); form.append(input); td.children('a.name').hide(); td.append(form); @@ -209,18 +366,29 @@ var FileList={ if (len === -1) { len = input.val().length; } - input.selectRange(0,len); + input.selectRange(0, len); - form.submit(function(event){ + var checkInput = function () { + var filename = input.val(); + if (filename !== oldname) { + if (!Files.isFileNameValid(filename)) { + // Files.isFileNameValid(filename) throws an exception itself + } else if($('#dir').val() === '/' && filename === 'Shared') { + throw t('files','In the home folder \'Shared\' is a reserved filename'); + } else if (FileList.inList(filename)) { + throw t('files', '{new_name} already exists', {new_name: filename}); + } + } + return true; + }; + + form.submit(function(event) { event.stopPropagation(); event.preventDefault(); - var newname=input.val(); - if (!Files.isFileNameValid(newname)) { - return false; - } else if (newname != name) { - if (FileList.checkName(name, newname, false)) { - newname = name; - } else { + try { + var newname = input.val(); + if (newname !== oldname) { + checkInput(); // save background image, because it's replaced by a spinner while async request var oldBackgroundImage = td.css('background-image'); // mark as loading @@ -230,16 +398,16 @@ var FileList={ data: { dir : $('#dir').val(), newname: newname, - file: name + file: oldname }, success: function(result) { if (!result || result.status === 'error') { - OC.Notification.show(result.data.message); - newname = name; + OC.dialogs.alert(result.data.message, t('core', 'Could not rename file')); // revert changes + newname = oldname; tr.attr('data-file', newname); var path = td.children('a.name').attr('href'); - td.children('a.name').attr('href', path.replace(encodeURIComponent(name), encodeURIComponent(newname))); + td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newname))); if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { var basename=newname.substr(0,newname.lastIndexOf('.')); } else { @@ -247,78 +415,99 @@ var FileList={ } td.find('a.name span.nametext').text(basename); if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { - if (td.find('a.name span.extension').length === 0 ) { + if ( ! td.find('a.name span.extension').exists() ) { td.find('a.name span.nametext').append('<span class="extension"></span>'); } td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.'))); } tr.find('.fileactions').effect('highlight', {}, 5000); tr.effect('highlight', {}, 5000); + // remove loading mark and recover old image + td.css('background-image', oldBackgroundImage); + } + else { + var fileInfo = result.data; + tr.attr('data-mime', fileInfo.mime); + tr.attr('data-etag', fileInfo.etag); + if (fileInfo.isPreviewAvailable) { + Files.lazyLoadPreview(fileInfo.directory + '/' + fileInfo.name, result.data.mime, function(previewpath) { + tr.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }, null, null, result.data.etag); + } + else { + tr.find('td.filename').removeClass('preview').attr('style','background-image:url('+fileInfo.icon+')'); + } } - // remove loading mark and recover old image - td.css('background-image', oldBackgroundImage); + // reinsert row + tr.detach(); + FileList.insertElement( tr.attr('data-file'), tr.attr('data-type'),tr ); + // update file actions in case the extension changed + FileActions.display( tr.find('td.filename'), true); } }); } - } - tr.data('renaming',false); - tr.attr('data-file', newname); - var path = td.children('a.name').attr('href'); - td.children('a.name').attr('href', path.replace(encodeURIComponent(name), encodeURIComponent(newname))); - if (newname.indexOf('.') > 0 && tr.data('type') != 'dir') { - var basename=newname.substr(0,newname.lastIndexOf('.')); - } else { - var basename=newname; - } - td.find('a.name span.nametext').text(basename); - if (newname.indexOf('.') > 0 && tr.data('type') != 'dir') { - if (td.find('a.name span.extension').length == 0 ) { - td.find('a.name span.nametext').append('<span class="extension"></span>'); + input.tipsy('hide'); + tr.data('renaming',false); + tr.attr('data-file', newname); + var path = td.children('a.name').attr('href'); + // FIXME this will fail if the path contains the filename. + td.children('a.name').attr('href', path.replace(encodeURIComponent(oldname), encodeURIComponent(newname))); + var basename = newname; + if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { + basename = newname.substr(0, newname.lastIndexOf('.')); + } + td.find('a.name span.nametext').text(basename); + if (newname.indexOf('.') > 0 && tr.data('type') !== 'dir') { + if ( ! td.find('a.name span.extension').exists() ) { + td.find('a.name span.nametext').append('<span class="extension"></span>'); + } + td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.'))); } - td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.'))); + form.remove(); + td.children('a.name').show(); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); } - form.remove(); - td.children('a.name').show(); return false; }); - input.keyup(function(event){ - if (event.keyCode == 27) { + input.keyup(function(event) { + // verify filename on typing + try { + checkInput(); + input.tipsy('hide'); + input.removeClass('error'); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } + if (event.keyCode === 27) { + input.tipsy('hide'); tr.data('renaming',false); form.remove(); td.children('a.name').show(); } }); - input.click(function(event){ + input.click(function(event) { event.stopPropagation(); event.preventDefault(); }); - input.blur(function(){ + input.blur(function() { form.trigger('submit'); }); }, - checkName:function(oldName, newName, isNewFile) { - if (isNewFile || $('tr').filterAttr('data-file', newName).length > 0) { - var html; - if(isNewFile){ - html = t('files', '{new_name} already exists', {new_name: escapeHTML(newName)})+'<span class="replace">'+t('files', 'replace')+'</span><span class="suggest">'+t('files', 'suggest name')+'</span> <span class="cancel">'+t('files', 'cancel')+'</span>'; - }else{ - html = t('files', '{new_name} already exists', {new_name: escapeHTML(newName)})+'<span class="replace">'+t('files', 'replace')+'</span><span class="cancel">'+t('files', 'cancel')+'</span>'; - } - html = $('<span>' + html + '</span>'); - html.attr('data-oldName', oldName); - html.attr('data-newName', newName); - html.attr('data-isNewFile', isNewFile); - OC.Notification.showHtml(html); - return true; - } else { - return false; - } + inList:function(filename) { + return $('#fileList tr[data-file="'+filename+'"]').length; }, replace:function(oldName, newName, isNewFile) { // Finish any existing actions - $('tr').filterAttr('data-file', oldName).hide(); - $('tr').filterAttr('data-file', newName).hide(); - var tr = $('tr').filterAttr('data-file', oldName).clone(); + $('tr[data-file="'+oldName+'"]').hide(); + $('tr[data-file="'+newName+'"]').hide(); + var tr = $('tr[data-file="'+oldName+'"]').clone(); tr.attr('data-replace', 'true'); tr.attr('data-file', newName); var td = tr.children('td.filename'); @@ -347,14 +536,14 @@ var FileList={ FileList.finishReplace(); }; if (!isNewFile) { - OC.Notification.showHtml(t('files', 'replaced {new_name} with {old_name}', {new_name: newName}, {old_name: oldName})+'<span class="undo">'+t('files', 'undo')+'</span>'); + OC.Notification.showHtml(t('files', 'replaced {new_name} with {old_name}', {new_name: newName}, {old_name: oldName})+'<span class="undo">'+t('files', 'undo')+'</span>'); } }, finishReplace:function() { if (!FileList.replaceCanceled && FileList.replaceOldName && FileList.replaceNewName) { $.ajax({url: OC.filePath('files', 'ajax', 'rename.php'), async: false, data: { dir: $('#dir').val(), newname: FileList.replaceNewName, file: FileList.replaceOldName }, success: function(result) { - if (result && result.status == 'success') { - $('tr').filterAttr('data-replace', 'true').removeAttr('data-replace'); + if (result && result.status === 'success') { + $('tr[data-replace="true"').removeAttr('data-replace'); } else { OC.dialogs.alert(result.data.message, 'Error moving file'); } @@ -365,15 +554,13 @@ var FileList={ }}); } }, - do_delete:function(files){ - if(files.substr){ + do_delete:function(files) { + if (files.substr) { files=[files]; } for (var i=0; i<files.length; i++) { - var deleteAction = $('tr').filterAttr('data-file',files[i]).children("td.date").children(".action.delete"); - var oldHTML = deleteAction.html(); - var newHTML = '<img class="move2trash" data-action="Delete" title="'+t('files', 'perform delete operation')+'" src="'+ OC.imagePath('core', 'loading.gif') +'"></a>'; - deleteAction.html(newHTML); + var deleteAction = $('tr[data-file="'+files[i]+'"]').children("td.date").children(".action.delete"); + deleteAction.removeClass('delete-icon').addClass('progress-icon'); } // Finish any existing actions if (FileList.lastAction) { @@ -383,178 +570,386 @@ var FileList={ var fileNames = JSON.stringify(files); $.post(OC.filePath('files', 'ajax', 'delete.php'), {dir:$('#dir').val(),files:fileNames}, - function(result){ - if (result.status == 'success') { - $.each(files,function(index,file){ - var files = $('tr').filterAttr('data-file',file); + function(result) { + if (result.status === 'success') { + $.each(files,function(index,file) { + var files = $('tr[data-file="'+file+'"]'); files.remove(); files.find('input[type="checkbox"]').removeAttr('checked'); files.removeClass('selected'); }); procesSelection(); checkTrashStatus(); + FileList.updateFileSummary(); + FileList.updateEmptyContent(); + Files.updateStorageStatistics(); } else { + if (result.status === 'error' && result.data.message) { + OC.Notification.show(result.data.message); + } + else { + OC.Notification.show(t('files', 'Error deleting file.')); + } + // hide notification after 10 sec + setTimeout(function() { + OC.Notification.hide(); + }, 10000); $.each(files,function(index,file) { - var deleteAction = $('tr').filterAttr('data-file',file).children("td.date").children(".move2trash"); - deleteAction.html(oldHTML); + var deleteAction = $('tr[data-file="' + file + '"] .action.delete'); + deleteAction.removeClass('progress-icon').addClass('delete-icon'); }); } }); + }, + createFileSummary: function() { + if( $('#fileList tr').exists() ) { + var summary = this._calculateFileSummary(); + + // Get translations + var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs); + var fileInfo = n('files', '%n file', '%n files', summary.totalFiles); + + var infoVars = { + dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">', + files: '</span><span class="fileinfo">'+fileInfo+'</span>' + }; + + var info = t('files', '{dirs} and {files}', infoVars); + + // don't show the filesize column, if filesize is NaN (e.g. in trashbin) + if (isNaN(summary.totalSize)) { + var fileSize = ''; + } else { + var fileSize = '<td class="filesize">'+humanFileSize(summary.totalSize)+'</td>'; + } + + var $summary = $('<tr class="summary" data-file="undefined"><td><span class="info">'+info+'</span></td>'+fileSize+'<td></td></tr>'); + $('#fileList').append($summary); + + var $dirInfo = $summary.find('.dirinfo'); + var $fileInfo = $summary.find('.fileinfo'); + var $connector = $summary.find('.connector'); + + // Show only what's necessary, e.g.: no files: don't show "0 files" + if (summary.totalDirs === 0) { + $dirInfo.hide(); + $connector.hide(); + } + if (summary.totalFiles === 0) { + $fileInfo.hide(); + $connector.hide(); + } + } + }, + _calculateFileSummary: function() { + var result = { + totalDirs: 0, + totalFiles: 0, + totalSize: 0 + }; + $.each($('tr[data-file]'), function(index, value) { + var $value = $(value); + if ($value.data('type') === 'dir') { + result.totalDirs++; + } else if ($value.data('type') === 'file') { + result.totalFiles++; + } + if ($value.data('size') !== undefined && $value.data('id') !== -1) { + //Skip shared as it does not count toward quota + result.totalSize += parseInt($value.data('size')); + } + }); + return result; + }, + updateFileSummary: function() { + var $summary = $('.summary'); + + // Check if we should remove the summary to show "Upload something" + if ($('#fileList tr').length === 1 && $summary.length === 1) { + $summary.remove(); + } + // If there's no summary create one (createFileSummary checks if there's data) + else if ($summary.length === 0) { + FileList.createFileSummary(); + } + // There's a summary and data -> Update the summary + else if ($('#fileList tr').length > 1 && $summary.length === 1) { + var fileSummary = this._calculateFileSummary(); + var $dirInfo = $('.summary .dirinfo'); + var $fileInfo = $('.summary .fileinfo'); + var $connector = $('.summary .connector'); + + // Substitute old content with new translations + $dirInfo.html(n('files', '%n folder', '%n folders', fileSummary.totalDirs)); + $fileInfo.html(n('files', '%n file', '%n files', fileSummary.totalFiles)); + $('.summary .filesize').html(humanFileSize(fileSummary.totalSize)); + + // Show only what's necessary (may be hidden) + if (fileSummary.totalDirs === 0) { + $dirInfo.hide(); + $connector.hide(); + } else { + $dirInfo.show(); + } + if (fileSummary.totalFiles === 0) { + $fileInfo.hide(); + $connector.hide(); + } else { + $fileInfo.show(); + } + if (fileSummary.totalDirs > 0 && fileSummary.totalFiles > 0) { + $connector.show(); + } + } + }, + updateEmptyContent: function() { + var $fileList = $('#fileList'); + var permissions = $('#permissions').val(); + var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0; + var exists = $fileList.find('tr:first').exists(); + $('#emptycontent').toggleClass('hidden', !isCreatable || exists); + $('#filestable th').toggleClass('hidden', !exists); + }, + showMask: function() { + // in case one was shown before + var $mask = $('#content .mask'); + if ($mask.exists()) { + return; + } + + $mask = $('<div class="mask transparent"></div>'); + + $mask.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + $mask.css('background-repeat', 'no-repeat'); + $('#content').append($mask); + + // block UI, but only make visible in case loading takes longer + FileList._maskTimeout = window.setTimeout(function() { + // reset opacity + $mask.removeClass('transparent'); + }, 250); + }, + hideMask: function() { + var $mask = $('#content .mask').remove(); + if (FileList._maskTimeout) { + window.clearTimeout(FileList._maskTimeout); + } + }, + scrollTo:function(file) { + //scroll to and highlight preselected file + var $scrolltorow = $('tr[data-file="'+file+'"]'); + if ($scrolltorow.exists()) { + $scrolltorow.addClass('searchresult'); + $(window).scrollTop($scrolltorow.position().top); + //remove highlight when hovered over + $scrolltorow.one('hover', function() { + $scrolltorow.removeClass('searchresult'); + }); + } + }, + filter:function(query) { + $('#fileList tr:not(.summary)').each(function(i,e) { + if ($(e).data('file').toString().toLowerCase().indexOf(query.toLowerCase()) !== -1) { + $(e).addClass("searchresult"); + } else { + $(e).removeClass("searchresult"); + } + }); + //do not use scrollto to prevent removing searchresult css class + var first = $('#fileList tr.searchresult').first(); + if (first.exists()) { + $(window).scrollTop(first.position().top); + } + }, + unfilter:function() { + $('#fileList tr.searchresult').each(function(i,e) { + $(e).removeClass("searchresult"); + }); } }; -$(document).ready(function(){ +$(document).ready(function() { + var isPublic = !!$('#isPublic').val(); // handle upload events var file_upload_start = $('#file_upload_start'); + file_upload_start.on('fileuploaddrop', function(e, data) { - // only handle drop to dir if fileList exists - if ($('#fileList').length > 0) { - var dropTarget = $(e.originalEvent.target).closest('tr'); - if(dropTarget && dropTarget.data('type') === 'dir') { // drag&drop upload to folder - var dirName = dropTarget.data('file'); - // update folder in form - data.formData = function(form) { - var formArray = form.serializeArray(); - // array index 0 contains the max files size - // array index 1 contains the request token - // array index 2 contains the directory - var parentDir = formArray[2]['value']; - if (parentDir === '/') { - formArray[2]['value'] += dirName; - } else { - formArray[2]['value'] += '/'+dirName; - } - return formArray; + OC.Upload.log('filelist handle fileuploaddrop', e, data); + + var dropTarget = $(e.originalEvent.target).closest('tr, .crumb'); + if (dropTarget && (dropTarget.data('type') === 'dir' || dropTarget.hasClass('crumb'))) { // drag&drop upload to folder + + // remember as context + data.context = dropTarget; + + var dir = dropTarget.data('file'); + // if from file list, need to prepend parent dir + if (dir) { + var parentDir = $('#dir').val() || '/'; + if (parentDir[parentDir.length - 1] !== '/') { + parentDir += '/'; } + dir = parentDir + dir; } - } + else{ + // read full path from crumb + dir = dropTarget.data('dir') || '/'; + } + + // update folder in form + data.formData = function(form) { + return [ + {name: 'dir', value: dir}, + {name: 'requesttoken', value: oc_requesttoken} + ]; + }; + } + }); file_upload_start.on('fileuploadadd', function(e, data) { - // only add to fileList if it exists - if ($('#fileList').length > 0) { + OC.Upload.log('filelist handle fileuploadadd', e, data); - if(FileList.deleteFiles && FileList.deleteFiles.indexOf(data.files[0].name)!=-1){//finish delete if we are uploading a deleted file - FileList.finishDelete(null, true); //delete file before continuing + //finish delete if we are uploading a deleted file + if (FileList.deleteFiles && FileList.deleteFiles.indexOf(data.files[0].name)!==-1) { + FileList.finishDelete(null, true); //delete file before continuing + } + + // add ui visualization to existing folder + if (data.context && data.context.data('type') === 'dir') { + // add to existing folder + + // update upload counter ui + var uploadtext = data.context.find('.uploadtext'); + var currentUploads = parseInt(uploadtext.attr('currentUploads')); + currentUploads += 1; + uploadtext.attr('currentUploads', currentUploads); + + var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); + if (currentUploads === 1) { + var img = OC.imagePath('core', 'loading.gif'); + data.context.find('td.filename').attr('style','background-image:url('+img+')'); + uploadtext.text(translatedText); + uploadtext.show(); + } else { + uploadtext.text(translatedText); } + } + + }); + /* + * when file upload done successfully add row to filelist + * update counter when uploading to sub folder + */ + file_upload_start.on('fileuploaddone', function(e, data) { + OC.Upload.log('filelist handle fileuploaddone', e, data); + + var response; + if (typeof data.result === 'string') { + response = data.result; + } else { + // fetch response from iframe + response = data.result[0].body.innerText; + } + var result=$.parseJSON(response); - // add ui visualization to existing folder or as new stand-alone file? - var dropTarget = $(e.originalEvent.target).closest('tr'); - if(dropTarget && dropTarget.data('type') === 'dir') { - // add to existing folder - var dirName = dropTarget.data('file'); + if (typeof result[0] !== 'undefined' && result[0].status === 'success') { + var file = result[0]; - // set dir context - data.context = $('tr').filterAttr('data-type', 'dir').filterAttr('data-file', dirName); + if (data.context && data.context.data('type') === 'dir') { // update upload counter ui var uploadtext = data.context.find('.uploadtext'); var currentUploads = parseInt(uploadtext.attr('currentUploads')); - currentUploads += 1; + currentUploads -= 1; uploadtext.attr('currentUploads', currentUploads); - if(currentUploads === 1) { - var img = OC.imagePath('core', 'loading.gif'); + var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); + if (currentUploads === 0) { + var img = OC.imagePath('core', 'filetypes/folder.png'); data.context.find('td.filename').attr('style','background-image:url('+img+')'); - uploadtext.text(t('files', '1 file uploading')); - uploadtext.show(); + uploadtext.text(translatedText); + uploadtext.hide(); } else { - uploadtext.text(currentUploads + ' ' + t('files', 'files uploading')); + uploadtext.text(translatedText); } + + // update folder size + var size = parseInt(data.context.data('size')); + size += parseInt(file.size); + data.context.attr('data-size', size); + data.context.find('td.filesize').text(humanFileSize(size)); + } else { + // only append new file if dragged onto current dir's crumb (last) + if (data.context && data.context.hasClass('crumb') && !data.context.hasClass('last')) { + return; + } + // add as stand-alone row to filelist - var uniqueName = getUniqueName(data.files[0].name); - var size=t('files','Pending'); - if(data.files[0].size>=0){ + var size=t('files', 'Pending'); + if (data.files[0].size>=0) { size=data.files[0].size; } var date=new Date(); var param = {}; - if ($('#publicUploadRequestToken').length) { - param.download_url = document.location.href + '&download&path=/' + $('#dir').val() + '/' + uniqueName; + if ($('#publicUploadRequestToken').exists()) { + param.download_url = document.location.href + '&download&path=/' + $('#dir').val() + '/' + file.name; } + //should the file exist in the list remove it + FileList.remove(file.name); + // create new file context - data.context = FileList.addFile(uniqueName,size,date,true,false,param); + data.context = FileList.addFile(file.name, file.size, date, false, false, param); + + // update file data + data.context.attr('data-mime',file.mime).attr('data-id',file.id).attr('data-etag', file.etag); + var permissions = data.context.data('permissions'); + if (permissions !== file.permissions) { + data.context.attr('data-permissions', file.permissions); + data.context.data('permissions', file.permissions); + } + FileActions.display(data.context.find('td.filename'), true); + + var path = getPathForPreview(file.name); + Files.lazyLoadPreview(path, file.mime, function(previewpath) { + data.context.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }, null, null, file.etag); } } }); - file_upload_start.on('fileuploaddone', function(e, data) { - // only update the fileList if it exists - if ($('#fileList').length > 0) { - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - // fetch response from iframe - response = data.result[0].body.innerText; - } - var result=$.parseJSON(response); - - if(typeof result[0] !== 'undefined' && result[0].status === 'success') { - var file = result[0]; - - if (data.context.data('type') === 'file') { - // update file data - data.context.attr('data-mime',file.mime).attr('data-id',file.id); - var size = data.context.data('size'); - if(size!=file.size){ - data.context.attr('data-size', file.size); - data.context.find('td.filesize').text(humanFileSize(file.size)); - } - if (FileList.loadingDone) { - FileList.loadingDone(file.name, file.id); - } - } else { - // update upload counter ui - var uploadtext = data.context.find('.uploadtext'); - var currentUploads = parseInt(uploadtext.attr('currentUploads')); - currentUploads -= 1; - uploadtext.attr('currentUploads', currentUploads); - if(currentUploads === 0) { - var img = OC.imagePath('core', 'filetypes/folder.png'); - data.context.find('td.filename').attr('style','background-image:url('+img+')'); - uploadtext.text(''); - uploadtext.hide(); - } else { - uploadtext.text(currentUploads + ' ' + t('files', 'files uploading')); - } - - // update folder size - var size = parseInt(data.context.data('size')); - size += parseInt(file.size) ; - data.context.attr('data-size', size); - data.context.find('td.filesize').text(humanFileSize(size)); + file_upload_start.on('fileuploadstop', function(e, data) { + OC.Upload.log('filelist handle fileuploadstop', e, data); - } - } + //if user pressed cancel hide upload chrome + if (data.errorThrown === 'abort') { + //cleanup uploading to a dir + var uploadtext = $('tr .uploadtext'); + var img = OC.imagePath('core', 'filetypes/folder.png'); + uploadtext.parents('td.filename').attr('style','background-image:url('+img+')'); + uploadtext.fadeOut(); + uploadtext.attr('currentUploads', 0); } }); file_upload_start.on('fileuploadfail', function(e, data) { - // only update the fileList if it exists - // cleanup files, error notification has been shown by fileupload code - var tr = data.context; - if (typeof tr === 'undefined') { - tr = $('tr').filterAttr('data-file', data.files[0].name); - } - if (tr.attr('data-type') === 'dir') { + OC.Upload.log('filelist handle fileuploadfail', e, data); + + //if user pressed cancel hide upload chrome + if (data.errorThrown === 'abort') { //cleanup uploading to a dir - var uploadtext = tr.find('.uploadtext'); + var uploadtext = $('tr .uploadtext'); var img = OC.imagePath('core', 'filetypes/folder.png'); - tr.find('td.filename').attr('style','background-image:url('+img+')'); - uploadtext.text(''); - uploadtext.hide(); //TODO really hide already - } else { - //remove file - tr.fadeOut(); - tr.remove(); + uploadtext.parents('td.filename').attr('style','background-image:url('+img+')'); + uploadtext.fadeOut(); + uploadtext.attr('currentUploads', 0); } }); $('#notification').hide(); - $('#notification').on('click', '.undo', function(){ + $('#notification').on('click', '.undo', function() { if (FileList.deleteFiles) { - $.each(FileList.deleteFiles,function(index,file){ - $('tr').filterAttr('data-file',file).show(); + $.each(FileList.deleteFiles,function(index,file) { + $('tr[data-file="'+file+'"]').show(); }); FileList.deleteCanceled=true; FileList.deleteFiles=null; @@ -564,26 +959,26 @@ $(document).ready(function(){ FileList.deleteCanceled = false; FileList.deleteFiles = [FileList.replaceOldName]; } else { - $('tr').filterAttr('data-file', FileList.replaceOldName).show(); + $('tr[data-file="'+FileList.replaceOldName+'"]').show(); } - $('tr').filterAttr('data-replace', 'true').remove(); - $('tr').filterAttr('data-file', FileList.replaceNewName).show(); + $('tr[data-replace="true"').remove(); + $('tr[data-file="'+FileList.replaceNewName+'"]').show(); FileList.replaceCanceled = true; FileList.replaceOldName = null; FileList.replaceNewName = null; FileList.replaceIsNewFile = null; } FileList.lastAction = null; - OC.Notification.hide(); + OC.Notification.hide(); }); $('#notification:first-child').on('click', '.replace', function() { - OC.Notification.hide(function() { - FileList.replace($('#notification > span').attr('data-oldName'), $('#notification > span').attr('data-newName'), $('#notification > span').attr('data-isNewFile')); - }); + OC.Notification.hide(function() { + FileList.replace($('#notification > span').attr('data-oldName'), $('#notification > span').attr('data-newName'), $('#notification > span').attr('data-isNewFile')); + }); }); $('#notification:first-child').on('click', '.suggest', function() { - $('tr').filterAttr('data-file', $('#notification > span').attr('data-oldName')).show(); - OC.Notification.hide(); + $('tr[data-file="'+$('#notification > span').attr('data-oldName')+'"]').show(); + OC.Notification.hide(); }); $('#notification:first-child').on('click', '.cancel', function() { if ($('#notification > span').attr('data-isNewFile')) { @@ -592,12 +987,71 @@ $(document).ready(function(){ } }); FileList.useUndo=(window.onbeforeunload)?true:false; - $(window).bind('beforeunload', function (){ + $(window).bind('beforeunload', function () { if (FileList.lastAction) { FileList.lastAction(); } }); - $(window).unload(function (){ + $(window).unload(function () { $(window).trigger('beforeunload'); }); + + function decodeQuery(query) { + return query.replace(/\+/g, ' '); + } + + function parseHashQuery() { + var hash = window.location.hash, + pos = hash.indexOf('?'), + query; + if (pos >= 0) { + return hash.substr(pos + 1); + } + return ''; + } + + function parseCurrentDirFromUrl() { + var query = parseHashQuery(), + params, + dir = '/'; + // try and parse from URL hash first + if (query) { + params = OC.parseQueryString(decodeQuery(query)); + } + // else read from query attributes + if (!params) { + params = OC.parseQueryString(decodeQuery(location.search)); + } + return (params && params.dir) || '/'; + } + + // disable ajax/history API for public app (TODO: until it gets ported) + if (!isPublic) { + // fallback to hashchange when no history support + if (!window.history.pushState) { + $(window).on('hashchange', function() { + FileList.changeDirectory(parseCurrentDirFromUrl(), false); + }); + } + window.onpopstate = function(e) { + var targetDir; + if (e.state && e.state.dir) { + targetDir = e.state.dir; + } + else{ + // read from URL + targetDir = parseCurrentDirFromUrl(); + } + if (targetDir) { + FileList.changeDirectory(targetDir, false); + } + }; + + if (parseInt($('#ajaxLoad').val(), 10) === 1) { + // need to initially switch the dir to the one from the hash (IE8) + FileList.changeDirectory(parseCurrentDirFromUrl(), false, true); + } + } + + FileList.createFileSummary(); }); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 3fad3fae7d3..fdaa3aa3342 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -1,45 +1,54 @@ -var uploadingFiles = {}; Files={ - cancelUpload:function(filename) { - if(uploadingFiles[filename]) { - uploadingFiles[filename].abort(); - delete uploadingFiles[filename]; - return true; - } - return false; - }, - cancelUploads:function() { - $.each(uploadingFiles,function(index,file) { - if(typeof file['abort'] === 'function') { - file.abort(); - var filename = $('tr').filterAttr('data-file',index); - filename.hide(); - filename.find('input[type="checkbox"]').removeAttr('checked'); - filename.removeClass('selected'); - } else { - $.each(file,function(i,f) { - f.abort(); - delete file[i]; - }); + // file space size sync + _updateStorageStatistics: function() { + Files._updateStorageStatisticsTimeout = null; + var currentDir = FileList.getCurrentDirectory(), + state = Files.updateStorageStatistics; + if (state.dir){ + if (state.dir === currentDir) { + return; } - delete uploadingFiles[index]; + // cancel previous call, as it was for another dir + state.call.abort(); + } + state.dir = currentDir; + state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php') + '?dir=' + encodeURIComponent(currentDir),function(response) { + state.dir = null; + state.call = null; + Files.updateMaxUploadFilesize(response); }); - procesSelection(); }, + updateStorageStatistics: function(force) { + if (!OC.currentUser) { + return; + } + + // debounce to prevent calling too often + if (Files._updateStorageStatisticsTimeout) { + clearTimeout(Files._updateStorageStatisticsTimeout); + } + if (force) { + Files._updateStorageStatistics(); + } + else { + Files._updateStorageStatisticsTimeout = setTimeout(Files._updateStorageStatistics, 250); + } + }, + updateMaxUploadFilesize:function(response) { - if(response == undefined) { + if (response === undefined) { return; } - if(response.data !== undefined && response.data.uploadMaxFilesize !== undefined) { + if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) { $('#max_upload').val(response.data.uploadMaxFilesize); $('#upload.button').attr('original-title', response.data.maxHumanFilesize); $('#usedSpacePercent').val(response.data.usedSpacePercent); Files.displayStorageWarnings(); } - if(response[0] == undefined) { + if (response[0] === undefined) { return; } - if(response[0].uploadMaxFilesize !== undefined) { + if (response[0].uploadMaxFilesize !== undefined) { $('#max_upload').val(response[0].uploadMaxFilesize); $('#upload.button').attr('original-title', response[0].maxHumanFilesize); $('#usedSpacePercent').val(response[0].usedSpacePercent); @@ -47,25 +56,31 @@ Files={ } }, + + /** + * Fix path name by removing double slash at the beginning, if any + */ + fixPath: function(fileName) { + if (fileName.substr(0, 2) == '//') { + return fileName.substr(1); + } + return fileName; + }, + isFileNameValid:function (name) { if (name === '.') { - OC.Notification.show(t('files', '\'.\' is an invalid file name.')); - return false; - } - if (name.length == 0) { - OC.Notification.show(t('files', 'File name cannot be empty.')); - return false; + throw t('files', '\'.\' is an invalid file name.'); + } else if (name.length === 0) { + throw t('files', 'File name cannot be empty.'); } // check for invalid characters var invalid_characters = ['\\', '/', '<', '>', ':', '"', '|', '?', '*']; for (var i = 0; i < invalid_characters.length; i++) { - if (name.indexOf(invalid_characters[i]) != -1) { - OC.Notification.show(t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed.")); - return false; + if (name.indexOf(invalid_characters[i]) !== -1) { + throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed."); } } - OC.Notification.hide(); return true; }, displayStorageWarnings: function() { @@ -81,33 +96,135 @@ Files={ if (usedSpacePercent > 90) { OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)', {usedSpacePercent: usedSpacePercent})); } + }, + + displayEncryptionWarning: function() { + + if (!OC.Notification.isHidden()) { + return; + } + + var encryptedFiles = $('#encryptedFiles').val(); + var initStatus = $('#encryptionInitStatus').val(); + if (initStatus === '0') { // enc not initialized, but should be + OC.Notification.show(t('files_encryption', 'Encryption App is enabled but your keys are not initialized, please log-out and log-in again')); + return; + } + if (initStatus === '1') { // encryption tried to init but failed + OC.Notification.showHtml(t('files_encryption', 'Invalid private key for Encryption App. Please update your private key password in your personal settings to recover access to your encrypted files.')); + return; + } + if (encryptedFiles === '1') { + OC.Notification.show(t('files_encryption', 'Encryption was disabled but your files are still encrypted. Please go to your personal settings to decrypt your files.')); + return; + } + }, + + setupDragAndDrop: function() { + var $fileList = $('#fileList'); + + //drag/drop of files + $fileList.find('tr td.filename').each(function(i,e) { + if ($(e).parent().data('permissions') & OC.PERMISSION_DELETE) { + $(e).draggable(dragOptions); + } + }); + + $fileList.find('tr[data-type="dir"] td.filename').each(function(i,e) { + if ($(e).parent().data('permissions') & OC.PERMISSION_CREATE) { + $(e).droppable(folderDropOptions); + } + }); + }, + + lastWidth: 0, + + initBreadCrumbs: function () { + var $controls = $('#controls'); + + Files.lastWidth = 0; + Files.breadcrumbs = []; + + // initialize with some extra space + Files.breadcrumbsWidth = 64; + if ( document.getElementById("navigation") ) { + Files.breadcrumbsWidth += $('#navigation').get(0).offsetWidth; + } + Files.hiddenBreadcrumbs = 0; + + $.each($('.crumb'), function(index, breadcrumb) { + Files.breadcrumbs[index] = breadcrumb; + Files.breadcrumbsWidth += $(breadcrumb).get(0).offsetWidth; + }); + + $.each($('#controls .actions>div'), function(index, action) { + Files.breadcrumbsWidth += $(action).get(0).offsetWidth; + }); + + // event handlers for breadcrumb items + $controls.find('.crumb a').on('click', onClickBreadcrumb); + + // setup drag and drop + $controls.find('.crumb:not(.last)').droppable(crumbDropOptions); + }, + + resizeBreadcrumbs: function (width, firstRun) { + if (width !== Files.lastWidth) { + if ((width < Files.lastWidth || firstRun) && width < Files.breadcrumbsWidth) { + if (Files.hiddenBreadcrumbs === 0) { + Files.breadcrumbsWidth -= $(Files.breadcrumbs[1]).get(0).offsetWidth; + $(Files.breadcrumbs[1]).find('a').hide(); + $(Files.breadcrumbs[1]).append('<span>...</span>'); + Files.breadcrumbsWidth += $(Files.breadcrumbs[1]).get(0).offsetWidth; + Files.hiddenBreadcrumbs = 2; + } + var i = Files.hiddenBreadcrumbs; + while (width < Files.breadcrumbsWidth && i > 1 && i < Files.breadcrumbs.length - 1) { + Files.breadcrumbsWidth -= $(Files.breadcrumbs[i]).get(0).offsetWidth; + $(Files.breadcrumbs[i]).hide(); + Files.hiddenBreadcrumbs = i; + i++; + } + } else if (width > Files.lastWidth && Files.hiddenBreadcrumbs > 0) { + var i = Files.hiddenBreadcrumbs; + while (width > Files.breadcrumbsWidth && i > 0) { + if (Files.hiddenBreadcrumbs === 1) { + Files.breadcrumbsWidth -= $(Files.breadcrumbs[1]).get(0).offsetWidth; + $(Files.breadcrumbs[1]).find('span').remove(); + $(Files.breadcrumbs[1]).find('a').show(); + Files.breadcrumbsWidth += $(Files.breadcrumbs[1]).get(0).offsetWidth; + } else { + $(Files.breadcrumbs[i]).show(); + Files.breadcrumbsWidth += $(Files.breadcrumbs[i]).get(0).offsetWidth; + if (Files.breadcrumbsWidth > width) { + Files.breadcrumbsWidth -= $(Files.breadcrumbs[i]).get(0).offsetWidth; + $(Files.breadcrumbs[i]).hide(); + break; + } + } + i--; + Files.hiddenBreadcrumbs = i; + } + } + Files.lastWidth = width; + } } }; $(document).ready(function() { + // FIXME: workaround for trashbin app + if (window.trashBinApp) { + return; + } + Files.displayEncryptionWarning(); Files.bindKeyboardShortcuts(document, jQuery); - $('#fileList tr').each(function(){ - //little hack to set unescape filenames in attribute - $(this).attr('data-file',decodeURIComponent($(this).attr('data-file'))); - }); + + FileList.postProcessList(); + Files.setupDragAndDrop(); $('#file_action_panel').attr('activeAction', false); - //drag/drop of files - $('#fileList tr td.filename').each(function(i,e){ - if ($(e).parent().data('permissions') & OC.PERMISSION_DELETE) { - $(e).draggable(dragOptions); - } - }); - $('#fileList tr[data-type="dir"] td.filename').each(function(i,e){ - if ($(e).parent().data('permissions') & OC.PERMISSION_CREATE){ - $(e).droppable(folderDropOptions); - } - }); - $('div.crumb:not(.last)').droppable(crumbDropOptions); - $('ul#apps>li:first-child').data('dir',''); - if($('div.crumb').length){ - $('ul#apps>li:first-child').droppable(crumbDropOptions); - } + // allow dropping on the "files" app icon + $('ul#apps li:first-child').data('dir','').droppable(crumbDropOptions); // Triggers invisible file input $('#upload a').on('click', function() { @@ -117,7 +234,8 @@ $(document).ready(function() { // Trigger cancelling of file upload $('#uploadprogresswrapper .stop').on('click', function() { - Files.cancelUploads(); + OC.Upload.cancelUploads(); + procesSelection(); }); // Show trash bin @@ -139,7 +257,7 @@ $(document).ready(function() { var rows = $(this).parent().parent().parent().children('tr'); for (var i = start; i < end; i++) { $(rows).each(function(index) { - if (index == i) { + if (index === i) { var checkbox = $(this).children().children('input:checkbox'); $(checkbox).attr('checked', 'checked'); $(checkbox).parent().parent().addClass('selected'); @@ -156,23 +274,23 @@ $(document).ready(function() { } else { $(checkbox).attr('checked', 'checked'); $(checkbox).parent().parent().toggleClass('selected'); - var selectedCount=$('td.filename input:checkbox:checked').length; - if (selectedCount == $('td.filename input:checkbox').length) { + var selectedCount = $('td.filename input:checkbox:checked').length; + if (selectedCount === $('td.filename input:checkbox').length) { $('#select_all').attr('checked', 'checked'); } } procesSelection(); } else { var filename=$(this).parent().parent().attr('data-file'); - var tr=$('tr').filterAttr('data-file',filename); + var tr=$('tr[data-file="'+filename+'"]'); var renaming=tr.data('renaming'); - if(!renaming && !FileList.isLoading(filename)){ + if (!renaming && !FileList.isLoading(filename)) { FileActions.currentFile = $(this).parent(); var mime=FileActions.getCurrentMimeType(); var type=FileActions.getCurrentType(); var permissions = FileActions.getCurrentPermissions(); var action=FileActions.getDefault(mime,type, permissions); - if(action){ + if (action) { event.preventDefault(); action(filename); } @@ -183,11 +301,11 @@ $(document).ready(function() { // Sets the select_all checkbox behaviour : $('#select_all').click(function() { - if($(this).attr('checked')){ + if ($(this).attr('checked')) { // Check all $('td.filename input:checkbox').attr('checked', true); $('td.filename input:checkbox').parent().parent().addClass('selected'); - }else{ + } else { // Uncheck all $('td.filename input:checkbox').attr('checked', false); $('td.filename input:checkbox').parent().parent().removeClass('selected'); @@ -204,7 +322,7 @@ $(document).ready(function() { var rows = $(this).parent().parent().parent().children('tr'); for (var i = start; i < end; i++) { $(rows).each(function(index) { - if (index == i) { + if (index === i) { var checkbox = $(this).children().children('input:checkbox'); $(checkbox).attr('checked', 'checked'); $(checkbox).parent().parent().addClass('selected'); @@ -214,10 +332,10 @@ $(document).ready(function() { } var selectedCount=$('td.filename input:checkbox:checked').length; $(this).parent().parent().toggleClass('selected'); - if(!$(this).attr('checked')){ + if (!$(this).attr('checked')) { $('#select_all').attr('checked',false); - }else{ - if(selectedCount==$('td.filename input:checkbox').length){ + } else { + if (selectedCount===$('td.filename input:checkbox').length) { $('#select_all').attr('checked',true); } } @@ -225,21 +343,22 @@ $(document).ready(function() { }); $('.download').click('click',function(event) { - var files=getSelectedFiles('name'); + var files=getSelectedFilesTrash('name'); var fileslist = JSON.stringify(files); var dir=$('#dir').val()||'/'; OC.Notification.show(t('files','Your download is being prepared. This might take some time if the files are big.')); // use special download URL if provided, e.g. for public shared files - if ( (downloadURL = document.getElementById("downloadURL")) ) { - window.location=downloadURL.value+"&download&files="+encodeURIComponent(fileslist); + var downloadURL = document.getElementById("downloadURL"); + if ( downloadURL ) { + window.location = downloadURL.value+"&download&files=" + encodeURIComponent(fileslist); } else { - window.location=OC.filePath('files', 'ajax', 'download.php') + '?'+ $.param({ dir: dir, files: fileslist }); + window.location = OC.filePath('files', 'ajax', 'download.php') + '?'+ $.param({ dir: dir, files: fileslist }); } return false; }); $('.delete-selected').click(function(event) { - var files=getSelectedFiles('name'); + var files=getSelectedFilesTrash('name'); event.preventDefault(); FileList.do_delete(files); return false; @@ -251,309 +370,57 @@ $(document).ready(function() { e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone }); - $.assocArraySize = function(obj) { - // http://stackoverflow.com/a/6700/11236 - var size = 0, key; - for (key in obj) { - if (obj.hasOwnProperty(key)) size++; - } - return size; - }; - - // warn user not to leave the page while upload is in progress - $(window).bind('beforeunload', function(e) { - if ($.assocArraySize(uploadingFiles) > 0) - return t('files','File upload is in progress. Leaving the page now will cancel the upload.'); - }); - - //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used) - if(navigator.userAgent.search(/konqueror/i)==-1){ - $('#file_upload_start').attr('multiple','multiple') - } - - //if the breadcrumb is to long, start by replacing foldernames with '...' except for the current folder - var crumb=$('div.crumb').first(); - while($('div.controls').height()>40 && crumb.next('div.crumb').length>0){ - crumb.children('a').text('...'); - crumb=crumb.next('div.crumb'); - } - //if that isn't enough, start removing items from the breacrumb except for the current folder and it's parent - var crumb=$('div.crumb').first(); - var next=crumb.next('div.crumb'); - while($('div.controls').height()>40 && next.next('div.crumb').length>0){ - crumb.remove(); - crumb=next; - next=crumb.next('div.crumb'); - } - //still not enough, start shorting down the current folder name - var crumb=$('div.crumb>a').last(); - while($('div.controls').height()>40 && crumb.text().length>6){ - var text=crumb.text() - text=text.substr(0,text.length-6)+'...'; - crumb.text(text); - } - - $(document).click(function(){ - $('#new>ul').hide(); - $('#new').removeClass('active'); - $('#new li').each(function(i,element){ - if($(element).children('p').length==0){ - $(element).children('form').remove(); - $(element).append('<p>'+$(element).data('text')+'</p>'); - } - }); - }); - $('#new').click(function(event){ - event.stopPropagation(); - }); - $('#new>a').click(function(){ - $('#new>ul').toggle(); - $('#new').toggleClass('active'); - }); - $('#new li').click(function(){ - if($(this).children('p').length==0){ - return; - } - - $('#new li').each(function(i,element){ - if($(element).children('p').length==0){ - $(element).children('form').remove(); - $(element).append('<p>'+$(element).data('text')+'</p>'); - } - }); - - var type=$(this).data('type'); - var text=$(this).children('p').text(); - $(this).data('text',text); - $(this).children('p').remove(); - var form=$('<form></form>'); - var input=$('<input>'); - form.append(input); - $(this).append(form); - input.focus(); - form.submit(function(event){ - event.stopPropagation(); - event.preventDefault(); - var newname=input.val(); - if(type == 'web' && newname.length == 0) { - OC.Notification.show(t('files', 'URL cannot be empty.')); - return false; - } else if (type != 'web' && !Files.isFileNameValid(newname)) { - return false; - } else if( type == 'folder' && $('#dir').val() == '/' && newname == 'Shared') { - OC.Notification.show(t('files','Invalid folder name. Usage of \'Shared\' is reserved by Owncloud')); - return false; - } - if (FileList.lastAction) { - FileList.lastAction(); - } - var name = getUniqueName(newname); - if (newname != name) { - FileList.checkName(name, newname, true); - var hidden = true; - } else { - var hidden = false; - } - switch(type){ - case 'file': - $.post( - OC.filePath('files','ajax','newfile.php'), - {dir:$('#dir').val(),filename:name}, - function(result){ - if (result.status == 'success') { - var date=new Date(); - FileList.addFile(name,0,date,false,hidden); - var tr=$('tr').filterAttr('data-file',name); - tr.attr('data-mime',result.data.mime); - tr.attr('data-id', result.data.id); - getMimeIcon(result.data.mime,function(path){ - tr.find('td.filename').attr('style','background-image:url('+path+')'); - }); - } else { - OC.dialogs.alert(result.data.message, t('core', 'Error')); - } - } - ); - break; - case 'folder': - $.post( - OC.filePath('files','ajax','newfolder.php'), - {dir:$('#dir').val(),foldername:name}, - function(result){ - if (result.status == 'success') { - var date=new Date(); - FileList.addDir(name,0,date,hidden); - var tr=$('tr').filterAttr('data-file',name); - tr.attr('data-id', result.data.id); - } else { - OC.dialogs.alert(result.data.message, t('core', 'Error')); - } - } - ); - break; - case 'web': - if(name.substr(0,8)!='https://' && name.substr(0,7)!='http://'){ - name='http://'+name; - } - var localName=name; - if(localName.substr(localName.length-1,1)=='/'){//strip / - localName=localName.substr(0,localName.length-1) - } - if(localName.indexOf('/')){//use last part of url - localName=localName.split('/').pop(); - }else{//or the domain - localName=(localName.match(/:\/\/(.[^/]+)/)[1]).replace('www.',''); - } - localName = getUniqueName(localName); - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - } else { - $('#uploadprogressbar').progressbar({value:0}); - $('#uploadprogressbar').fadeIn(); - } - - var eventSource=new OC.EventSource(OC.filePath('files','ajax','newfile.php'),{dir:$('#dir').val(),source:name,filename:localName}); - eventSource.listen('progress',function(progress){ - //IE < 10 does not fire the necessary events for the progress bar. - if($('html.lte9').length > 0) { - } else { - $('#uploadprogressbar').progressbar('value',progress); - } - }); - eventSource.listen('success',function(data){ - var mime=data.mime; - var size=data.size; - var id=data.id; - $('#uploadprogressbar').fadeOut(); - var date=new Date(); - FileList.addFile(localName,size,date,false,hidden); - var tr=$('tr').filterAttr('data-file',localName); - tr.data('mime',mime).data('id',id); - tr.attr('data-id', id); - getMimeIcon(mime,function(path){ - tr.find('td.filename').attr('style','background-image:url('+path+')'); - }); - }); - eventSource.listen('error',function(error){ - $('#uploadprogressbar').fadeOut(); - alert(error); - }); - break; - } - var li=form.parent(); - form.remove(); - li.append('<p>'+li.data('text')+'</p>'); - $('#new>a').click(); - }); - }); - //do a background scan if needed scanFiles(); - var lastWidth = 0; - var breadcrumbs = []; - var breadcrumbsWidth = 0; - if ( document.getElementById("navigation") ) { - breadcrumbsWidth = $('#navigation').get(0).offsetWidth; - } - var hiddenBreadcrumbs = 0; - - $.each($('.crumb'), function(index, breadcrumb) { - breadcrumbs[index] = breadcrumb; - breadcrumbsWidth += $(breadcrumb).get(0).offsetWidth; - }); - - - $.each($('#controls .actions>div'), function(index, action) { - breadcrumbsWidth += $(action).get(0).offsetWidth; - }); - - function resizeBreadcrumbs(firstRun) { - var width = $(this).width(); - if (width != lastWidth) { - if ((width < lastWidth || firstRun) && width < breadcrumbsWidth) { - if (hiddenBreadcrumbs == 0) { - breadcrumbsWidth -= $(breadcrumbs[1]).get(0).offsetWidth; - $(breadcrumbs[1]).find('a').hide(); - $(breadcrumbs[1]).append('<span>...</span>'); - breadcrumbsWidth += $(breadcrumbs[1]).get(0).offsetWidth; - hiddenBreadcrumbs = 2; - } - var i = hiddenBreadcrumbs; - while (width < breadcrumbsWidth && i > 1 && i < breadcrumbs.length - 1) { - breadcrumbsWidth -= $(breadcrumbs[i]).get(0).offsetWidth; - $(breadcrumbs[i]).hide(); - hiddenBreadcrumbs = i; - i++ - } - } else if (width > lastWidth && hiddenBreadcrumbs > 0) { - var i = hiddenBreadcrumbs; - while (width > breadcrumbsWidth && i > 0) { - if (hiddenBreadcrumbs == 1) { - breadcrumbsWidth -= $(breadcrumbs[1]).get(0).offsetWidth; - $(breadcrumbs[1]).find('span').remove(); - $(breadcrumbs[1]).find('a').show(); - breadcrumbsWidth += $(breadcrumbs[1]).get(0).offsetWidth; - } else { - $(breadcrumbs[i]).show(); - breadcrumbsWidth += $(breadcrumbs[i]).get(0).offsetWidth; - if (breadcrumbsWidth > width) { - breadcrumbsWidth -= $(breadcrumbs[i]).get(0).offsetWidth; - $(breadcrumbs[i]).hide(); - break; - } - } - i--; - hiddenBreadcrumbs = i; - } - } - lastWidth = width; - } - } + Files.initBreadCrumbs(); $(window).resize(function() { - resizeBreadcrumbs(false); + var width = $(this).width(); + Files.resizeBreadcrumbs(width, false); }); - resizeBreadcrumbs(true); + var width = $(this).width(); + Files.resizeBreadcrumbs(width, true); // display storage warnings setTimeout ( "Files.displayStorageWarnings()", 100 ); OC.Notification.setDefault(Files.displayStorageWarnings); - // file space size sync - function update_storage_statistics() { - $.getJSON(OC.filePath('files','ajax','getstoragestats.php'),function(response) { - Files.updateMaxUploadFilesize(response); - }); + // only possible at the moment if user is logged in + if (OC.currentUser) { + // start on load - we ask the server every 5 minutes + var updateStorageStatisticsInterval = 5*60*1000; + var updateStorageStatisticsIntervalId = setInterval(Files.updateStorageStatistics, updateStorageStatisticsInterval); + + // Use jquery-visibility to de-/re-activate file stats sync + if ($.support.pageVisibility) { + $(document).on({ + 'show.visibility': function() { + if (!updateStorageStatisticsIntervalId) { + updateStorageStatisticsIntervalId = setInterval(Files.updateStorageStatistics, updateStorageStatisticsInterval); + } + }, + 'hide.visibility': function() { + clearInterval(updateStorageStatisticsIntervalId); + updateStorageStatisticsIntervalId = 0; + } + }); + } } - // start on load - we ask the server every 5 minutes - var update_storage_statistics_interval = 5*60*1000; - var update_storage_statistics_interval_id = setInterval(update_storage_statistics, update_storage_statistics_interval); - - // Use jquery-visibility to de-/re-activate file stats sync - if ($.support.pageVisibility) { - $(document).on({ - 'show.visibility': function() { - if (!update_storage_statistics_interval_id) { - update_storage_statistics_interval_id = setInterval(update_storage_statistics, update_storage_statistics_interval); - } - }, - 'hide.visibility': function() { - clearInterval(update_storage_statistics_interval_id); - update_storage_statistics_interval_id = 0; - } - }); + //scroll to and highlight preselected file + if (getURLParameter('scrollto')) { + FileList.scrollTo(getURLParameter('scrollto')); } }); -function scanFiles(force, dir, users){ +function scanFiles(force, dir, users) { if (!OC.currentUser) { return; } - if(!dir){ + if (!dir) { dir = ''; } force = !!force; //cast to bool @@ -571,17 +438,18 @@ function scanFiles(force, dir, users){ scannerEventSource = new OC.EventSource(OC.filePath('files','ajax','scan.php'),{force: force,dir: dir}); } scanFiles.cancel = scannerEventSource.close.bind(scannerEventSource); - scannerEventSource.listen('count',function(count){ - console.log(count + ' files scanned') + scannerEventSource.listen('count',function(count) { + console.log(count + ' files scanned'); }); - scannerEventSource.listen('folder',function(path){ - console.log('now scanning ' + path) + scannerEventSource.listen('folder',function(path) { + console.log('now scanning ' + path); }); - scannerEventSource.listen('done',function(count){ + scannerEventSource.listen('done',function(count) { scanFiles.scanning=false; console.log('done after ' + count + ' files'); + Files.updateStorageStatistics(); }); - scannerEventSource.listen('user',function(user){ + scannerEventSource.listen('user',function(user) { console.log('scanning files for ' + user); }); } @@ -590,18 +458,14 @@ scanFiles.scanning=false; function boolOperationFinished(data, callback) { result = jQuery.parseJSON(data.responseText); Files.updateMaxUploadFilesize(result); - if(result.status == 'success'){ + if (result.status === 'success') { callback.call(); } else { alert(result.data.message); } } -function updateBreadcrumb(breadcrumbHtml) { - $('p.nav').empty().html(breadcrumbHtml); -} - -var createDragShadow = function(event){ +var createDragShadow = function(event) { //select dragged file var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked'); if (!isDragSelected) { @@ -609,9 +473,9 @@ var createDragShadow = function(event){ $(event.target).parents('tr').find('td input:first').prop('checked',true); } - var selectedFiles = getSelectedFiles(); + var selectedFiles = getSelectedFilesTrash(); - if (!isDragSelected && selectedFiles.length == 1) { + if (!isDragSelected && selectedFiles.length === 1) { //revert the selection $(event.target).parents('tr').find('td input:first').prop('checked',false); } @@ -628,7 +492,7 @@ var createDragShadow = function(event){ var dir=$('#dir').val(); - $(selectedFiles).each(function(i,elem){ + $(selectedFiles).each(function(i,elem) { var newtr = $('<tr/>').attr('data-dir', dir).attr('data-filename', elem.name); newtr.append($('<td/>').addClass('filename').text(elem.name)); newtr.append($('<td/>').addClass('size').text(humanFileSize(elem.size))); @@ -636,24 +500,25 @@ var createDragShadow = function(event){ if (elem.type === 'dir') { newtr.find('td.filename').attr('style','background-image:url('+OC.imagePath('core', 'filetypes/folder.png')+')'); } else { - getMimeIcon(elem.mime,function(path){ - newtr.find('td.filename').attr('style','background-image:url('+path+')'); - }); + var path = getPathForPreview(elem.name); + Files.lazyLoadPreview(path, elem.mime, function(previewpath) { + newtr.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }, null, null, elem.etag); } }); return dragshadow; -} +}; //options for file drag/drop var dragOptions={ revert: 'invalid', revertDuration: 300, - opacity: 0.7, zIndex: 100, appendTo: 'body', cursorAt: { left: -5, top: -5 }, + opacity: 0.7, zIndex: 100, appendTo: 'body', cursorAt: { left: 24, top: 18 }, helper: createDragShadow, cursor: 'move', stop: function(event, ui) { $('#fileList tr td.filename').addClass('ui-draggable'); } -} +}; // sane browsers support using the distance option if ( $('html.ie').length === 0) { dragOptions['distance'] = 20; @@ -666,20 +531,20 @@ var folderDropOptions={ return false; } - var target=$.trim($(this).find('.nametext').text()); + var target = $(this).closest('tr').data('file'); var files = ui.helper.find('tr'); - $(files).each(function(i,row){ + $(files).each(function(i,row) { var dir = $(row).data('dir'); var file = $(row).data('filename'); $.post(OC.filePath('files', 'ajax', 'move.php'), { dir: dir, file: file, target: dir+'/'+target }, function(result) { if (result) { if (result.status === 'success') { //recalculate folder size - var oldSize = $('#fileList tr').filterAttr('data-file',target).data('size'); - var newSize = oldSize + $('#fileList tr').filterAttr('data-file',file).data('size'); - $('#fileList tr').filterAttr('data-file',target).data('size', newSize); - $('#fileList tr').filterAttr('data-file',target).find('td.filesize').text(humanFileSize(newSize)); + var oldSize = $('#fileList tr[data-file="'+target+'"]').data('size'); + var newSize = oldSize + $('#fileList tr[data-file="'+file+'"]').data('size'); + $('#fileList tr[data-file="'+target+'"]').data('size', newSize); + $('#fileList tr[data-file="'+target+'"]').find('td.filesize').text(humanFileSize(newSize)); FileList.remove(file); procesSelection(); @@ -690,30 +555,30 @@ var folderDropOptions={ $('#notification').fadeIn(); } } else { - OC.dialogs.alert(t('Error moving file'), t('core', 'Error')); + OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); } }); }); }, tolerance: 'pointer' -} +}; var crumbDropOptions={ drop: function( event, ui ) { var target=$(this).data('dir'); - var dir=$('#dir').val(); - while(dir.substr(0,1)=='/'){//remove extra leading /'s + var dir = $('#dir').val(); + while(dir.substr(0,1) === '/') {//remove extra leading /'s dir=dir.substr(1); } - dir='/'+dir; - if(dir.substr(-1,1)!='/'){ - dir=dir+'/'; + dir = '/' + dir; + if (dir.substr(-1,1) !== '/') { + dir = dir + '/'; } - if(target==dir || target+'/'==dir){ + if (target === dir || target+'/' === dir) { return; } var files = ui.helper.find('tr'); - $(files).each(function(i,row){ + $(files).each(function(i,row) { var dir = $(row).data('dir'); var file = $(row).data('filename'); $.post(OC.filePath('files', 'ajax', 'move.php'), { dir: dir, file: file, target: target }, function(result) { @@ -728,19 +593,23 @@ var crumbDropOptions={ $('#notification').fadeIn(); } } else { - OC.dialogs.alert(t('Error moving file'), t('core', 'Error')); + OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); } }); }); }, tolerance: 'pointer' -} +}; -function procesSelection(){ - var selected=getSelectedFiles(); - var selectedFiles=selected.filter(function(el){return el.type=='file'}); - var selectedFolders=selected.filter(function(el){return el.type=='dir'}); - if(selectedFiles.length==0 && selectedFolders.length==0) { +function procesSelection() { + var selected = getSelectedFilesTrash(); + var selectedFiles = selected.filter(function(el) { + return el.type==='file'; + }); + var selectedFolders = selected.filter(function(el) { + return el.type==='dir'; + }); + if (selectedFiles.length === 0 && selectedFolders.length === 0) { $('#headerName>span.name').text(t('files','Name')); $('#headerSize').text(t('files','Size')); $('#modified').text(t('files','Modified')); @@ -749,33 +618,25 @@ function procesSelection(){ } else { $('.selectedActions').show(); - var totalSize=0; - for(var i=0;i<selectedFiles.length;i++){ + var totalSize = 0; + for(var i=0; i<selectedFiles.length; i++) { totalSize+=selectedFiles[i].size; }; - for(var i=0;i<selectedFolders.length;i++){ + for(var i=0; i<selectedFolders.length; i++) { totalSize+=selectedFolders[i].size; }; $('#headerSize').text(humanFileSize(totalSize)); - var selection=''; - if(selectedFolders.length>0){ - if(selectedFolders.length==1){ - selection+=t('files','1 folder'); - }else{ - selection+=t('files','{count} folders',{count: selectedFolders.length}); - } - if(selectedFiles.length>0){ - selection+=' & '; + var selection = ''; + if (selectedFolders.length > 0) { + selection += n('files', '%n folder', '%n folders', selectedFolders.length); + if (selectedFiles.length > 0) { + selection += ' & '; } } - if(selectedFiles.length>0){ - if(selectedFiles.length==1){ - selection+=t('files','1 file'); - }else{ - selection+=t('files','{count} files',{count: selectedFiles.length}); - } + if (selectedFiles.length>0) { + selection += n('files', '%n file', '%n files', selectedFiles.length); } - $('#headerName>span.name').text(selection); + $('#headerName span.name').text(selection); $('#modified').text(''); $('table').addClass('multiselect'); } @@ -783,46 +644,101 @@ function procesSelection(){ /** * @brief get a list of selected files - * @param string property (option) the property of the file requested - * @return array + * @param {string} property (option) the property of the file requested + * @return {array} * * possible values for property: name, mime, size and type * if property is set, an array with that property for each file is returnd * if it's ommited an array of objects with all properties is returned */ -function getSelectedFiles(property){ +function getSelectedFilesTrash(property) { var elements=$('td.filename input:checkbox:checked').parent().parent(); var files=[]; - elements.each(function(i,element){ + elements.each(function(i,element) { var file={ name:$(element).attr('data-file'), mime:$(element).data('mime'), type:$(element).data('type'), - size:$(element).data('size') + size:$(element).data('size'), + etag:$(element).data('etag') }; - if(property){ + if (property) { files.push(file[property]); - }else{ + } else { files.push(file); } }); return files; } -function getMimeIcon(mime, ready){ - if(getMimeIcon.cache[mime]){ - ready(getMimeIcon.cache[mime]); - }else{ - $.get( OC.filePath('files','ajax','mimeicon.php'), {mime: mime}, function(path){ - getMimeIcon.cache[mime]=path; - ready(getMimeIcon.cache[mime]); +Files.getMimeIcon = function(mime, ready) { + if (Files.getMimeIcon.cache[mime]) { + ready(Files.getMimeIcon.cache[mime]); + } else { + $.get( OC.filePath('files','ajax','mimeicon.php'), {mime: mime}, function(path) { + Files.getMimeIcon.cache[mime]=path; + ready(Files.getMimeIcon.cache[mime]); }); } } -getMimeIcon.cache={}; +Files.getMimeIcon.cache={}; + +function getPathForPreview(name) { + var path = $('#dir').val() + '/' + name; + return path; +} + +Files.lazyLoadPreview = function(path, mime, ready, width, height, etag) { + // get mime icon url + Files.getMimeIcon(mime, function(iconURL) { + var urlSpec = {}; + var previewURL; + ready(iconURL); // set mimeicon URL + + // now try getting a preview thumbnail URL + if ( ! width ) { + width = $('#filestable').data('preview-x'); + } + if ( ! height ) { + height = $('#filestable').data('preview-y'); + } + // note: the order of arguments must match the one + // from the server's template so that the browser + // knows it's the same file for caching + urlSpec.x = width; + urlSpec.y = height; + urlSpec.file = Files.fixPath(path); + + if (etag){ + // use etag as cache buster + urlSpec.c = etag; + } + else { + console.warn('Files.lazyLoadPreview(): missing etag argument'); + } + + if ( $('#publicUploadButtonMock').length ) { + urlSpec.t = $('#dirToken').val(); + previewURL = OC.Router.generate('core_ajax_public_preview', urlSpec); + } else { + previewURL = OC.Router.generate('core_ajax_preview', urlSpec); + } + previewURL = previewURL.replace('(', '%28'); + previewURL = previewURL.replace(')', '%29'); + + // preload image to prevent delay + // this will make the browser cache the image + var img = new Image(); + img.onload = function(){ + //set preview thumbnail URL + ready(previewURL); + } + img.src = previewURL; + }); +} -function getUniqueName(name){ - if($('tr').filterAttr('data-file',name).length>0){ +function getUniqueName(name) { + if ($('tr[data-file="'+name+'"]').exists()) { var parts=name.split('.'); var extension = ""; if (parts.length > 1) { @@ -831,9 +747,9 @@ function getUniqueName(name){ var base=parts.join('.'); numMatch=base.match(/\((\d+)\)/); var num=2; - if(numMatch && numMatch.length>0){ + if (numMatch && numMatch.length>0) { num=parseInt(numMatch[numMatch.length-1])+1; - base=base.split('(') + base=base.split('('); base.pop(); base=$.trim(base.join('(')); } @@ -847,9 +763,20 @@ function getUniqueName(name){ } function checkTrashStatus() { - $.post(OC.filePath('files_trashbin', 'ajax', 'isEmpty.php'), function(result){ + $.post(OC.filePath('files_trashbin', 'ajax', 'isEmpty.php'), function(result) { if (result.data.isEmpty === false) { $("input[type=button][id=trash]").removeAttr("disabled"); } }); -}
\ No newline at end of file +} + +function onClickBreadcrumb(e) { + var $el = $(e.target).closest('.crumb'), + $targetDir = $el.data('dir'); + isPublic = !!$('#isPublic').val(); + + if ($targetDir !== undefined && !isPublic) { + e.preventDefault(); + FileList.changeDirectory(decodeURIComponent($targetDir)); + } +} diff --git a/apps/files/js/jquery.fileupload.js b/apps/files/js/jquery.fileupload.js index a89e9dc2c44..f9f6cc3a382 100644 --- a/apps/files/js/jquery.fileupload.js +++ b/apps/files/js/jquery.fileupload.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload Plugin 5.9 + * jQuery File Upload Plugin 5.32.2 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -10,7 +10,7 @@ */ /*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, Blob, FormData, location */ +/*global define, window, document, location, File, Blob, FormData */ (function (factory) { 'use strict'; @@ -27,12 +27,28 @@ }(function ($) { 'use strict'; + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('<input type="file">').prop('disabled')); + // The FileReader API is not actually used, but works as feature detection, // as e.g. Safari supports XHR file uploads via the FormData API, // but not non-multipart XHR file uploads: $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); $.support.xhrFormDataFileUpload = !!window.FormData; + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + // The fileupload widget listens for change events on file input fields defined // via fileInput setting and paste or drop events of the given dropZone. // In addition to the default jQuery Widget methods, the fileupload widget @@ -44,17 +60,16 @@ $.widget('blueimp.fileupload', { options: { - // The namespace used for event handler binding on the dropZone and - // fileInput collections. - // If not set, the name of the widget ("fileupload") is used. - namespace: undefined, - // The drop target collection, by the default the complete document. - // Set to null or an empty collection to disable drag & drop support: + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: dropZone: $(document), - // The file input field collection, that is listened for change events. + // The paste target element(s), by the default the complete document. + // Set to null to disable paste support: + pasteZone: $(document), + // The file input field(s), that are listened to for change events. // If undefined, it is set to the file input fields inside // of the widget element on plugin initialization. - // Set to null or an empty collection to disable the change listener. + // Set to null to disable the change listener. fileInput: undefined, // By default, the file input field is replaced with a clone after // each input field change event. This is required for iframe transport @@ -63,7 +78,8 @@ replaceFileInput: true, // The parameter name for the file form data (the request argument name). // If undefined or empty, the name property of the file input field is - // used, or "files[]" if the file input name property is also empty: + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: paramName: undefined, // By default, each file of a selection is uploaded using an individual // request for XHR type uploads. Set to false to upload file @@ -108,6 +124,29 @@ // global progress calculation. Set the following option to false to // prevent recalculating the global progress data: recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, // Additional form data to be sent along with the file uploads can be set // using this option, which accepts an array of objects with name and @@ -121,48 +160,81 @@ // The add callback is invoked as soon as files are added to the fileupload // widget (via file input selection, drag & drop, paste or add API call). // If the singleFileUploads option is enabled, this callback will be - // called once for each file in the selection for XHR file uplaods, else + // called once for each file in the selection for XHR file uploads, else // once for each file selection. + // // The upload starts when the submit method is invoked on the data parameter. // The data object contains a files property holding the added files - // and allows to override plugin options as well as define ajax settings. + // and allows you to override plugin options as well as define ajax settings. + // // Listeners for this callback can also be bound the following way: // .bind('fileuploadadd', func); + // // data.submit() returns a Promise object and allows to attach additional // handlers using jQuery's Deferred callbacks: // data.submit().done(func).fail(func).always(func); add: function (e, data) { - data.submit(); + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } }, // Other callbacks: + // Callback for the submit event of each file upload: // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + // Callback for the start of each file upload request: // send: function (e, data) {}, // .bind('fileuploadsend', func); + // Callback for successful uploads: // done: function (e, data) {}, // .bind('fileuploaddone', func); + // Callback for failed (abort or error) uploads: // fail: function (e, data) {}, // .bind('fileuploadfail', func); + // Callback for completed (success, abort or error) requests: // always: function (e, data) {}, // .bind('fileuploadalways', func); + // Callback for upload progress events: // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + // Callback for global upload progress events: // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + // Callback for uploads start, equivalent to the global ajaxStart event: // start: function (e) {}, // .bind('fileuploadstart', func); + // Callback for uploads stop, equivalent to the global ajaxStop event: // stop: function (e) {}, // .bind('fileuploadstop', func); - // Callback for change events of the fileInput collection: + + // Callback for change events of the fileInput(s): // change: function (e, data) {}, // .bind('fileuploadchange', func); - // Callback for paste events to the dropZone collection: + + // Callback for paste events to the pasteZone(s): // paste: function (e, data) {}, // .bind('fileuploadpaste', func); - // Callback for drop events of the dropZone collection: + + // Callback for drop events of the dropZone(s): // drop: function (e, data) {}, // .bind('fileuploaddrop', func); - // Callback for dragover events of the dropZone collection: + + // Callback for dragover events of the dropZone(s): // dragover: function (e) {}, // .bind('fileuploaddragover', func); + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + // The plugin options are used as settings object for the ajax calls. // The following are jQuery ajax settings required for the file uploads: processData: false, @@ -170,15 +242,36 @@ cache: false }, - // A list of options that require a refresh after assigning a new value: - _refreshOptionsList: [ - 'namespace', - 'dropZone', + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ 'fileInput', + 'dropZone', + 'pasteZone', 'multipart', 'forceIframeTransport' ], + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + _isXHRUpload: function (options) { return !options.forceIframeTransport && ((!options.multipart && $.support.xhrFileUpload) || @@ -189,9 +282,11 @@ var formData; if (typeof options.formData === 'function') { return options.formData(options.form); - } else if ($.isArray(options.formData)) { + } + if ($.isArray(options.formData)) { return options.formData; - } else if (options.formData) { + } + if ($.type(options.formData) === 'object') { formData = []; $.each(options.formData, function (name, value) { formData.push({name: name, value: value}); @@ -209,28 +304,66 @@ return total; }, + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + _onProgress: function (e, data) { if (e.lengthComputable) { - var total = data.total || this._getTotal(data.files), - loaded = parseInt( - e.loaded / e.total * (data.chunkSize || total), - 10 - ) + (data.uploadedBytes || 0); - this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); - data.lengthComputable = true; - data.loaded = loaded; - data.total = total; + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); // Trigger a custom progress event with a total data property set // to the file size(s) of the current upload and a loaded data // property calculated accordingly: this._trigger('progress', e, data); // Trigger a global progress event for all current file uploads, // including ajax calls queued for sequential file uploads: - this._trigger('progressall', e, { - lengthComputable: true, - loaded: this._loaded, - total: this._total - }); + this._trigger('progressall', e, this._progress); } }, @@ -254,34 +387,30 @@ } }, + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + _initXHRData: function (options) { - var formData, + var that = this, + formData, file = options.files[0], // Ignore non-multipart setting if not supported: - multipart = options.multipart || !$.support.xhrFileUpload; - if (!multipart || options.blob) { - // For non-multipart uploads and chunked uploads, - // file meta data is not part of the request body, - // so we transmit this data as part of the HTTP headers. - // For cross domain requests, these headers must be allowed - // via Access-Control-Allow-Headers or removed using - // the beforeSend callback: - options.headers = $.extend(options.headers, { - 'X-File-Name': file.name, - 'X-File-Type': file.type, - 'X-File-Size': file.size - }); - if (!options.blob) { - // Non-chunked non-multipart upload: - options.contentType = file.type; - options.data = file; - } else if (!multipart) { - // Chunked non-multipart upload: - options.contentType = 'application/octet-stream'; - options.data = options.blob; - } + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = options.paramName[0]; + options.headers = options.headers || {}; + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; } - if (multipart && $.support.xhrFormDataFileUpload) { + if (!multipart) { + options.contentType = file.type; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { if (options.postMessage) { // window.postMessage does not allow sending FormData // objects, so we just add the File/Blob objects to @@ -290,19 +419,19 @@ formData = this._getFormData(options); if (options.blob) { formData.push({ - name: options.paramName, + name: paramName, value: options.blob }); } else { $.each(options.files, function (index, file) { formData.push({ - name: options.paramName, + name: options.paramName[index] || paramName, value: file }); }); } } else { - if (options.formData instanceof FormData) { + if (that._isInstanceOf('FormData', options.formData)) { formData = options.formData; } else { formData = new FormData(); @@ -311,14 +440,18 @@ }); } if (options.blob) { - formData.append(options.paramName, options.blob, file.name); + formData.append(paramName, options.blob, file.name); } else { $.each(options.files, function (index, file) { - // File objects are also Blob instances. // This check allows the tests to run with // dummy objects: - if (file instanceof Blob) { - formData.append(options.paramName, file, file.name); + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + options.paramName[index] || paramName, + file, + file.name + ); } }); } @@ -330,13 +463,13 @@ }, _initIframeSettings: function (options) { + var targetHost = $('<a></a>').prop('href', options.url).prop('host'); // Setting the dataType to iframe enables the iframe transport: options.dataType = 'iframe ' + (options.dataType || ''); // The iframe transport accepts a serialized array as form data: options.formData = this._getFormData(options); // Add redirect url to form data on cross-domain uploads: - if (options.redirect && $('<a></a>').prop('href', options.url) - .prop('host') !== location.host) { + if (options.redirect && targetHost && targetHost !== location.host) { options.formData.push({ name: options.redirectParamName || 'redirect', value: options.redirect @@ -358,29 +491,58 @@ options.dataType = 'postmessage ' + (options.dataType || ''); } } else { - this._initIframeSettings(options, 'iframe'); + this._initIframeSettings(options); } }, + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + _initFormSettings: function (options) { // Retrieve missing options from the input field and the // associated form, if available: if (!options.form || !options.form.length) { options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } } - if (!options.paramName) { - options.paramName = options.fileInput.prop('name') || - 'files[]'; - } + options.paramName = this._getParamName(options); if (!options.url) { options.url = options.form.prop('action') || location.href; } // The HTTP request method must be "POST" or "PUT": options.type = (options.type || options.form.prop('method') || '') .toUpperCase(); - if (options.type !== 'POST' && options.type !== 'PUT') { + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { options.type = 'POST'; } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } }, _getAJAXSettings: function (data) { @@ -390,6 +552,21 @@ return options; }, + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + // Maps jqXHR callbacks to the equivalent // methods of the given Promise object: _enhancePromise: function (promise) { @@ -414,24 +591,77 @@ return this._enhancePromise(promise); }, + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (data) { + return $.Deferred().resolveWith(that, [data]).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise(this)) + .pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise(this); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger('submit', e, this) !== false) && + that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + return that._getXHRPromise(); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + // Uploads a file in multiple, sequential requests // by splitting the file up in multiple blob chunks. // If the second parameter is true, only tests if the file // should be uploaded in chunks, but does not invoke any // upload requests: _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; var that = this, file = options.files[0], fs = file.size, - ub = options.uploadedBytes = options.uploadedBytes || 0, + ub = options.uploadedBytes, mcs = options.maxChunkSize || fs, - // Use the Blob methods with the slice implementation - // according to the W3C Blob API specification: - slice = file.webkitSlice || file.mozSlice || file.slice, - upload, - n, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), jqXHR, - pipe; + upload; if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || options.data) { return false; @@ -440,62 +670,84 @@ return true; } if (ub >= fs) { - file.error = 'uploadedBytes'; + file.error = options.i18n('uploadedBytes'); return this._getXHRPromise( false, options.context, [null, 'error', file.error] ); } - // n is the number of blobs to upload, - // calculated via filesize, uploaded bytes and max chunk size: - n = Math.ceil((fs - ub) / mcs); - // The chunk upload method accepting the chunk number as parameter: - upload = function (i) { - if (!i) { - return that._getXHRPromise(true, options.context); - } - // Upload the blobs in sequential order: - return upload(i -= 1).pipe(function () { - // Clone the options object for each chunk upload: - var o = $.extend({}, options); - o.blob = slice.call( - file, - ub + i * mcs, - ub + (i + 1) * mcs - ); - // Store the current chunk size, as the blob itself - // will be dereferenced after data processing: - o.chunkSize = o.blob.size; - // Process the upload data (the blob and potential form data): - that._initXHRData(o); - // Add progress listeners for this chunk upload: - that._initProgressListener(o); - jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) - .done(function () { - // Create a progress event if upload is done and - // no progress event has been invoked for this chunk: - if (!o.loaded) { - that._onProgress($.Event('progress', { - lengthComputable: true, - loaded: o.chunkSize, - total: o.chunkSize - }), o); - } - options.uploadedBytes = o.uploadedBytes += - o.chunkSize; - }); - return jqXHR; - }); + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); }; - // Return the piped Promise object, enhanced with an abort method, - // which is delegated to the jqXHR object of the current upload, - // and jqXHR callbacks mapped to the equivalent Promise methods: - pipe = upload(n); - pipe.abort = function () { + this._enhancePromise(promise); + promise.abort = function () { return jqXHR.abort(); }; - return this._enhancePromise(pipe); + upload(); + return promise; }, _beforeSend: function (e, data) { @@ -504,99 +756,113 @@ // and no other uploads are currently running, // equivalent to the global ajaxStart event: this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; this._active += 1; // Initialize the global progress values: - this._loaded += data.uploadedBytes || 0; - this._total += this._getTotal(data.files); + this._progress.loaded += data.loaded; + this._progress.total += data.total; }, _onDone: function (result, textStatus, jqXHR, options) { - if (!this._isXHRUpload(options)) { - // Create a progress event for each iframe load: + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: this._onProgress($.Event('progress', { lengthComputable: true, - loaded: 1, - total: 1 + loaded: total, + total: total }), options); } - options.result = result; - options.textStatus = textStatus; - options.jqXHR = jqXHR; + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; this._trigger('done', null, options); }, _onFail: function (jqXHR, textStatus, errorThrown, options) { - options.jqXHR = jqXHR; - options.textStatus = textStatus; - options.errorThrown = errorThrown; - this._trigger('fail', null, options); + var response = options._response; if (options.recalculateProgress) { // Remove the failed (error or abort) file upload from // the global progress calculation: - this._loaded -= options.loaded || options.uploadedBytes || 0; - this._total -= options.total || this._getTotal(options.files); + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); }, _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { - this._active -= 1; - options.textStatus = textStatus; - if (jqXHRorError && jqXHRorError.always) { - options.jqXHR = jqXHRorError; - options.result = jqXHRorResult; - } else { - options.jqXHR = jqXHRorResult; - options.errorThrown = jqXHRorError; - } + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks this._trigger('always', null, options); - if (this._active === 0) { - // The stop callback is triggered when all uploads have - // been completed, equivalent to the global ajaxStop event: - this._trigger('stop'); - // Reset the global progress values: - this._loaded = this._total = 0; - } }, _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } var that = this, jqXHR, + aborted, slot, pipe, options = that._getAJAXSettings(data), - send = function (resolve, args) { + send = function () { that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); jqXHR = jqXHR || ( - (resolve !== false && - that._trigger('send', e, options) !== false && - (that._chunkedUpload(options) || $.ajax(options))) || - that._getXHRPromise(false, options.context, args) + ((aborted || that._trigger('send', e, options) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) ).done(function (result, textStatus, jqXHR) { that._onDone(result, textStatus, jqXHR, options); }).fail(function (jqXHR, textStatus, errorThrown) { that._onFail(jqXHR, textStatus, errorThrown, options); }).always(function (jqXHRorResult, textStatus, jqXHRorError) { - that._sending -= 1; that._onAlways( jqXHRorResult, textStatus, jqXHRorError, options ); + that._sending -= 1; + that._active -= 1; if (options.limitConcurrentUploads && options.limitConcurrentUploads > that._sending) { // Start the next queued upload, // that has not been aborted: var nextSlot = that._slots.shift(); while (nextSlot) { - if (!nextSlot.isRejected()) { + if (that._getDeferredState(nextSlot) === 'pending') { nextSlot.resolve(); break; } nextSlot = that._slots.shift(); } } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } }); return jqXHR; }; @@ -609,18 +875,19 @@ this._slots.push(slot); pipe = slot.pipe(send); } else { - pipe = (this._sequence = this._sequence.pipe(send, send)); + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; } // Return the piped Promise object, enhanced with an abort method, // which is delegated to the jqXHR object of the current upload, // and jqXHR callbacks mapped to the equivalent Promise methods: pipe.abort = function () { - var args = [undefined, 'abort', 'abort']; + aborted = [undefined, 'abort', 'abort']; if (!jqXHR) { if (slot) { - slot.rejectWith(args); + slot.rejectWith(options.context, aborted); } - return send(false, args); + return send(); } return jqXHR.abort(); }; @@ -634,40 +901,43 @@ result = true, options = $.extend({}, this.options, data), limit = options.limitMultiFileUploads, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, fileSet, i; if (!(options.singleFileUploads || limit) || !this._isXHRUpload(options)) { fileSet = [data.files]; + paramNameSet = [paramName]; } else if (!options.singleFileUploads && limit) { fileSet = []; + paramNameSet = []; for (i = 0; i < data.files.length; i += limit) { fileSet.push(data.files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); } + } else { + paramNameSet = paramName; } data.originalFiles = data.files; $.each(fileSet || data.files, function (index, element) { - var files = fileSet ? element : [element], - newData = $.extend({}, data, {files: files}); - newData.submit = function () { - newData.jqXHR = this.jqXHR = - (that._trigger('submit', e, this) !== false) && - that._onSend(e, this); - return this.jqXHR; - }; - return (result = that._trigger('add', e, newData)); + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger('add', e, newData); + return result; }); return result; }, - // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: - _normalizeFile: function (index, file) { - if (file.name === undefined && file.size === undefined) { - file.name = file.fileName; - file.size = file.fileSize; - } - }, - _replaceFileInput: function (input) { var inputClone = input.clone(true); $('<form></form>').append(inputClone)[0].reset(); @@ -677,7 +947,7 @@ // Avoid memory leaks with the detached file input: $.cleanData(input.unbind('remove')); // Replace the original file input element in the fileInput - // collection with the clone, which has been copied including + // elements set with the clone, which has been copied including // event handlers: this.options.fileInput = this.options.fileInput.map(function (i, el) { if (el === input[0]) { @@ -692,102 +962,229 @@ } }, - _onChange: function (e) { - var that = e.data.fileupload, - data = { - files: $.each($.makeArray(e.target.files), that._normalizeFile), - fileInput: $(e.target), - form: $(e.target.form) - }; - if (!data.files.length) { + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + dirReader; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + dirReader.readEntries(function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, errorHandler); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } // If the files property is not available, the browser does not // support the File API and we add a pseudo File object with // the input value as name with path information removed: - data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; - } - if (that.options.replaceFileInput) { - that._replaceFileInput(data.fileInput); + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); } - if (that._trigger('change', e, data) === false || - that._onAdd(e, data) === false) { - return false; + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', e, data) !== false) { + that._onAdd(e, data); + } + }); }, _onPaste: function (e) { - var that = e.data.fileupload, - cbd = e.originalEvent.clipboardData, - items = (cbd && cbd.items) || [], + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, data = {files: []}; - $.each(items, function (index, item) { - var file = item.getAsFile && item.getAsFile(); - if (file) { - data.files.push(file); + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger('paste', e, data) === false || + this._onAdd(e, data) === false) { + return false; } - }); - if (that._trigger('paste', e, data) === false || - that._onAdd(e, data) === false) { - return false; } }, _onDrop: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, - data = { - files: $.each( - $.makeArray(dataTransfer && dataTransfer.files), - that._normalizeFile - ) - }; - if (that._trigger('drop', e, data) === false || - that._onAdd(e, data) === false) { - return false; + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger('drop', e, data) !== false) { + that._onAdd(e, data); + } + }); } - e.preventDefault(); }, _onDragOver: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; - if (that._trigger('dragover', e) === false) { - return false; - } + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; if (dataTransfer) { - dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; + if (this._trigger('dragover', e) === false) { + return false; + } + if ($.inArray('Files', dataTransfer.types) !== -1) { + dataTransfer.dropEffect = 'copy'; + e.preventDefault(); + } } - e.preventDefault(); }, _initEventHandlers: function () { - var ns = this.options.namespace; if (this._isXHRUpload(this.options)) { - this.options.dropZone - .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) - .bind('drop.' + ns, {fileupload: this}, this._onDrop) - .bind('paste.' + ns, {fileupload: this}, this._onPaste); + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); } - this.options.fileInput - .bind('change.' + ns, {fileupload: this}, this._onChange); }, _destroyEventHandlers: function () { - var ns = this.options.namespace; - this.options.dropZone - .unbind('dragover.' + ns, this._onDragOver) - .unbind('drop.' + ns, this._onDrop) - .unbind('paste.' + ns, this._onPaste); - this.options.fileInput - .unbind('change.' + ns, this._onChange); + this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); }, _setOption: function (key, value) { - var refresh = $.inArray(key, this._refreshOptionsList) !== -1; - if (refresh) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { this._destroyEventHandlers(); } - $.Widget.prototype._setOption.call(this, key, value); - if (refresh) { + this._super(key, value); + if (reinit) { this._initSpecialOptions(); this._initEventHandlers(); } @@ -796,42 +1193,68 @@ _initSpecialOptions: function () { var options = this.options; if (options.fileInput === undefined) { - options.fileInput = this.element.is('input:file') ? - this.element : this.element.find('input:file'); + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); } else if (!(options.fileInput instanceof $)) { options.fileInput = $(options.fileInput); } if (!(options.dropZone instanceof $)) { options.dropZone = $(options.dropZone); } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options; + // Initialize options set via HTML5 data-attributes: + $.each( + $(this.element[0].cloneNode(false)).data(), + function (key, value) { + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + ); }, _create: function () { - var options = this.options, - dataOpts = $.extend({}, this.element.data()); - dataOpts[this.widgetName] = undefined; - $.extend(options, dataOpts); - options.namespace = options.namespace || this.widgetName; + this._initDataAttributes(); this._initSpecialOptions(); this._slots = []; this._sequence = this._getXHRPromise(true); - this._sending = this._active = this._loaded = this._total = 0; + this._sending = this._active = 0; + this._initProgressObject(this); this._initEventHandlers(); }, - destroy: function () { - this._destroyEventHandlers(); - $.Widget.prototype.destroy.call(this); - }, - - enable: function () { - $.Widget.prototype.enable.call(this); - this._initEventHandlers(); + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; }, - disable: function () { - this._destroyEventHandlers(); - $.Widget.prototype.disable.call(this); + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; }, // This method is exposed to the widget API and allows adding files @@ -839,21 +1262,65 @@ // must have a files property and can contain additional options: // .fileupload('add', {files: filesList}); add: function (data) { + var that = this; if (!data || this.options.disabled) { return; } - data.files = $.each($.makeArray(data.files), this._normalizeFile); - this._onAdd(null, data); + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } }, // This method is exposed to the widget API and allows sending files // using the fileupload API. The data parameter accepts an object which - // must have a files property and can contain additional options: + // must have a files or fileInput property and can contain additional options: // .fileupload('send', {files: filesList}); // The method returns a Promise object for the file upload call. send: function (data) { if (data && !this.options.disabled) { - data.files = $.each($.makeArray(data.files), this._normalizeFile); + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data).then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); if (data.files.length) { return this._onSend(null, data); } @@ -863,4 +1330,4 @@ }); -})); +}));
\ No newline at end of file diff --git a/apps/files/js/jquery.iframe-transport.js b/apps/files/js/jquery.iframe-transport.js index d85c0c11297..5c9df77976b 100644 --- a/apps/files/js/jquery.iframe-transport.js +++ b/apps/files/js/jquery.iframe-transport.js @@ -1,5 +1,5 @@ /* - * jQuery Iframe Transport Plugin 1.3 + * jQuery Iframe Transport Plugin 1.7 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -30,27 +30,45 @@ // The iframe transport accepts three additional options: // options.fileInput: a jQuery collection of file input fields // options.paramName: the parameter name for the file form data, - // overrides the name property of the file input field(s) + // overrides the name property of the file input field(s), + // can be a string or an array of strings. // options.formData: an array of objects with name and value properties, // equivalent to the return data of .serializeArray(), e.g.: // [{name: 'a', value: 1}, {name: 'b', value: 2}] $.ajaxTransport('iframe', function (options) { - if (options.async && (options.type === 'POST' || options.type === 'GET')) { + if (options.async) { var form, - iframe; + iframe, + addParamChar; return { send: function (_, completeCallback) { form = $('<form style="display:none;"></form>'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } // javascript:false as initial iframe src // prevents warning popups on HTTPS in IE6. // IE versions below IE8 cannot set the name property of // elements that have already been added to the DOM, // so we set the name along with the iframe HTML markup: + counter += 1; iframe = $( '<iframe src="javascript:false;" name="iframe-transport-' + - (counter += 1) + '"></iframe>' + counter + '"></iframe>' ).bind('load', function () { - var fileInputClones; + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; iframe .unbind('load') .bind('load', function () { @@ -79,7 +97,12 @@ // (happens on form submits to iframe targets): $('<iframe src="javascript:false;"></iframe>') .appendTo(form); - form.remove(); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); }); form .prop('target', iframe.prop('name')) @@ -101,8 +124,11 @@ return fileInputClones[index]; }); if (options.paramName) { - options.fileInput.each(function () { - $(this).prop('name', options.paramName); + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); }); } // Appending the file input fields to the hidden form @@ -144,22 +170,36 @@ }); // The iframe transport returns the iframe content document as response. - // The following adds converters from iframe to text, json, html, and script: + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation $.ajaxSetup({ converters: { 'iframe text': function (iframe) { - return $(iframe[0].body).text(); + return iframe && $(iframe[0].body).text(); }, 'iframe json': function (iframe) { - return $.parseJSON($(iframe[0].body).text()); + return iframe && $.parseJSON($(iframe[0].body).text()); }, 'iframe html': function (iframe) { - return $(iframe[0].body).html(); + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); }, 'iframe script': function (iframe) { - return $.globalEval($(iframe[0].body).text()); + return iframe && $.globalEval($(iframe[0].body).text()); } } }); -})); +}));
\ No newline at end of file diff --git a/apps/files/js/keyboardshortcuts.js b/apps/files/js/keyboardshortcuts.js index cc2b5d42139..9d6c3ae8c33 100644 --- a/apps/files/js/keyboardshortcuts.js +++ b/apps/files/js/keyboardshortcuts.js @@ -131,7 +131,9 @@ var Files = Files || {}; return; } var preventDefault = false; - if ($.inArray(event.keyCode, keys) === -1) keys.push(event.keyCode); + if ($.inArray(event.keyCode, keys) === -1) { + keys.push(event.keyCode); + } if ( $.inArray(keyCodes.n, keys) !== -1 && ($.inArray(keyCodes.cmdFirefox, keys) !== -1 || $.inArray(keyCodes.cmdOpera, keys) !== -1 || $.inArray(keyCodes.leftCmdWebKit, keys) !== -1 || $.inArray(keyCodes.rightCmdWebKit, keys) !== -1 || $.inArray(keyCodes.ctrl, keys) !== -1 || event.ctrlKey)) { preventDefault = true; //new file/folder prevent browser from responding @@ -165,4 +167,4 @@ var Files = Files || {}; removeA(keys, event.keyCode); }); }; -})(Files);
\ No newline at end of file +})(Files); |