From ae6d2de17f740915e47c64d210680eb5e9850335 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Wed, 06 Jun 2012 11:35:21 -0400
Subject: [PATCH] New feature to add mail attachments using drag & drop on HTML5 enabled browsers

---
 CHANGELOG                          |    1 
 program/steps/mail/compose.inc     |   14 ++++
 program/js/app.js                  |  137 +++++++++++++++++++++++++++++++++++++++++++--
 skins/larry/mail.css               |   27 ++++++++
 skins/larry/images/filedrop.png    |    0 
 skins/larry/templates/compose.html |    1 
 6 files changed, 173 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index cb132db..8b0f067 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 9d6f7e8..b3d3aed 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -458,6 +458,14 @@
     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 @@
       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) {
@@ -3477,6 +3480,13 @@
   {
     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);
 
@@ -6220,6 +6230,121 @@
     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 306de36..70f657d 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -1590,6 +1590,19 @@
 }
 
 
+/**
+ * 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 @@
   '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 0000000..d4d455b
--- /dev/null
+++ b/skins/larry/images/filedrop.png
Binary files differ
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index 762f59c..0889b3b 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1168,11 +1168,36 @@
 	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 a71e820..93e9703 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>
 

--
Gitblit v1.9.1