summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bruederli <thomas@roundcube.net>2012-06-06 17:35:21 +0200
committerThomas Bruederli <thomas@roundcube.net>2012-06-06 17:35:21 +0200
commitae6d2de17f740915e47c64d210680eb5e9850335 (patch)
treed385b6a56aaca33cadeda6ece5bf7e2ae2b4c8e7
parente29515a504e862065684f6f41ab632ab38087c8d (diff)
New feature to add mail attachments using drag & drop on HTML5 enabled browsers
-rw-r--r--CHANGELOG1
-rw-r--r--program/js/app.js137
-rw-r--r--program/steps/mail/compose.inc14
-rw-r--r--skins/larry/images/filedrop.pngbin0 -> 605 bytes
-rw-r--r--skins/larry/mail.css27
-rw-r--r--skins/larry/templates/compose.html1
6 files changed, 173 insertions, 7 deletions
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 = '<span>' + this.get_label('uploading' + (files > 1 ? 'many' : '')) + '</span>',
ts = frame_name.replace(/^rcmupload/, '');
- if (this.env.loadingicon)
- content = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />'+content;
- content = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+ts+'\', \''+frame_name+'\');" href="#cancelupload" class="cancelupload">'
- + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + 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 = '<img src="'+ref.env.loadingicon+'" alt="" class="uploading" />' + att.html;
+
+ if (!att.complete && att.frame)
+ att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
+ + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + att.html;
+
var indicator, li = $('<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 = '<span>' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + '</span>';
+
+ // 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
--- /dev/null
+++ b/skins/larry/images/filedrop.png
Binary files 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 @@
<roundcube:button name="addattachment" type="input" class="button" classSel="button pressed" label="addattachment" onclick="UI.show_uploadform();return false" tabindex="10" />
</div>
<roundcube:object name="composeAttachmentList" id="attachment-list" class="attachmentslist" />
+ <roundcube:object name="fileDropArea" id="compose-attachments" />
</div>
</div>