diff --git a/CHANGELOG b/CHANGELOG index cb132db0f..8b0f0675f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Add mail attachments using drag & drop on HTML5 enabled browsers - Add workaround for invalid BODYSTRUCTURE response - parse message with Mail_mimeDecode package (#1485585) - Decode header value in rcube_mime::get() by default (#1488511) - Fix errors with enabled PHP magic_quotes_sybase option (#1488506) diff --git a/program/js/app.js b/program/js/app.js index 9d6f7e808..b3d3aed5c 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -458,6 +458,14 @@ function rcube_webmail() if (this.gui_objects.folderlist) this.gui_containers.foldertray = $(this.gui_objects.folderlist); + // activate html5 file drop feature (if browser supports it and if configured) + if (this.gui_objects.filedrop && this.env.filedrop && ((XMLHttpRequest && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) { + $(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); }); + $(this.gui_objects.filedrop).addClass('droptarget') + .bind('dragover dragleave', function(e){ return ref.file_drag_hover(e, e.type == 'dragover'); }) + .get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false); + } + // trigger init event hook this.triggerEvent('init', { task:this.task, action:this.env.action }); @@ -3453,12 +3461,7 @@ function rcube_webmail() var content = '' + this.get_label('uploading' + (files > 1 ? 'many' : '')) + '', ts = frame_name.replace(/^rcmupload/, ''); - if (this.env.loadingicon) - content = ''+content; - content = '' - + (this.env.cancelicon ? '' : this.get_label('cancel')) + '' + content; - - this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }); + this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false }); // upload progress support if (this.env.upload_progress_time) { @@ -3478,6 +3481,13 @@ function rcube_webmail() if (!this.gui_objects.attachmentlist) return false; + if (!att.complete && ref.env.loadingicon) + att.html = '' + att.html; + + if (!att.complete && att.frame) + att.html = '' + + (this.env.cancelicon ? '' : this.get_label('cancel')) + '' + att.html; + var indicator, li = $('
  • ').attr('id', name).addClass(att.classname).html(att.html); // replace indicator's li @@ -6220,6 +6230,121 @@ function rcube_webmail() return frame_name; }; + // html5 file-drop API + this.document_drag_hover = function(e, over) + { + e.preventDefault(); + $(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active'); + }; + + this.file_drag_hover = function(e, over) + { + e.preventDefault(); + e.stopPropagation(); + $(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover'); + }; + + // handler when files are dropped to a designated area. + // compose a multipart form data and submit it to the server + this.file_dropped = function(e) + { + // abort event and reset UI + this.file_drag_hover(e, false); + + // prepare multipart form data composition + var files = e.target.files || e.dataTransfer.files, + formdata = window.FormData ? new FormData() : null, + fieldname = this.env.filedrop.fieldname || '_file', + boundary = '------multipartformboundary' + (new Date).getTime(), + dashdash = '--', crlf = '\r\n', + multipart = dashdash + boundary + crlf; + + if (!file || !files.length) + return; + + // inline function to submit the files to the server + var submit_data = function() { + var multiple = files.length > 1, + ts = new Date().getTime(), + content = '' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + ''; + + // add to attachments list + ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }); + + // complete multipart content and post request + multipart += dashdash + boundary + dashdash + crlf; + + $.ajax({ + type: 'POST', + dataType: 'json', + url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||'', _uploadid:ts, _remote:1 }), + contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary, + processData: false, + data: formdata || multipart, + beforeSend: function(xhr, s) { if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; }, + success: function(data){ ref.http_response(data); }, + error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); } + }); + }; + + // get contents of all dropped files + var last = this.env.filedrop.single ? 0 : files.length - 1; + for (var i=0, f; i <= last && (f = files[i]); i++) { + if (!f.name) f.name = f.fileName; + if (!f.size) f.size = f.fileSize; + if (!f.type) f.type = 'application/octet-stream'; + + // binary encode file name + if (!formdata && /[^\x20-\x7E]/.test(f.name)) + f.name_bin = unescape(encodeURIComponent(f.name)); + + if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) { + // TODO: show message to user + continue; + } + + // the easy way with FormData (FF4+, Chrome, Safari) + if (formdata) { + formdata.append(fieldname + '[]', f); + if (i == last) + return submit_data(); + } + // use FileReader supporetd by Firefox 3.6 + else if (window.FileReader) { + var reader = new FileReader(); + + // closure to pass file properties to async callback function + reader.onload = (function(file, i) { + return function(e) { + multipart += 'Content-Disposition: form-data; name="' + fieldname + '[]"'; + multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf; + multipart += 'Content-Length: ' + file.size + crlf; + multipart += 'Content-Type: ' + file.type + crlf + crlf; + multipart += e.target.result + crlf; + multipart += dashdash + boundary + crlf; + + if (i == last) // we're done, submit the data + return submit_data(); + } + })(f,i); + reader.readAsBinaryString(f); + } + // Firefox 3 + else if (f.getAsBinary) { + multipart += 'Content-Disposition: form-data; name="' + fieldname + '[]"'; + multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf; + multipart += 'Content-Length: ' + f.size + crlf; + multipart += 'Content-Type: ' + f.type + crlf + crlf; + multipart += f.getAsBinary() + crlf; + multipart += dashdash + boundary +crlf; + + if (i == last) + return submit_data(); + } + } + }; + + // starts interval for keep-alive/check-recent signal this.start_keepalive = function() { diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 306de3608..70f657d8d 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -1590,6 +1590,19 @@ function rcmail_contacts_list($attrib = array()) } +/** + * Register a certain container as active area to drop files onto + */ +function compose_file_drop_area($attrib) +{ + global $OUTPUT; + + if ($attrib['id']) { + $OUTPUT->add_gui_object('filedrop', $attrib['id']); + $OUTPUT->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments')); + } +} + // register UI objects $OUTPUT->add_handlers(array( @@ -1599,6 +1612,7 @@ $OUTPUT->add_handlers(array( 'composeattachmentlist' => 'rcmail_compose_attachment_list', 'composeattachmentform' => 'rcmail_compose_attachment_form', 'composeattachment' => 'rcmail_compose_attachment_field', + 'filedroparea' => 'compose_file_drop_area', 'priorityselector' => 'rcmail_priority_selector', 'editorselector' => 'rcmail_editor_selector', 'receiptcheckbox' => 'rcmail_receipt_checkbox', diff --git a/skins/larry/images/filedrop.png b/skins/larry/images/filedrop.png new file mode 100644 index 000000000..d4d455bdf Binary files /dev/null and b/skins/larry/images/filedrop.png differ diff --git a/skins/larry/mail.css b/skins/larry/mail.css index 762f59c8d..0889b3b6c 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -1168,11 +1168,36 @@ div.message-part blockquote blockquote blockquote { bottom: 0; width: 240px; background: #f0f0f0; - border-left: 1px solid #ddd; + border-style: solid; + border-color: #f0f0f0 #f0f0f0 #f0f0f0 #ddd; + border-width: 1px; padding: 8px; overflow: auto; } +#compose-attachments.droptarget { + background-image: url(images/filedrop.png); + background-position: center bottom; + background-repeat: no-repeat; +} + +#compose-attachments.droptarget.hover, +#compose-attachments.droptarget.active { + border-color: #019bc6; + box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5); + -moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5); + -webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5); + -o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5); +} + +#compose-attachments.droptarget.hover { + background-color: #d9ecf4; + box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9); + -moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9); + -webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9); + -o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9); +} + .defaultSkin table.mceLayout, .defaultSkin table.mceLayout tr.mceLast td { border: 0 !important; diff --git a/skins/larry/templates/compose.html b/skins/larry/templates/compose.html index a71e82043..93e9703a4 100644 --- a/skins/larry/templates/compose.html +++ b/skins/larry/templates/compose.html @@ -156,6 +156,7 @@ +