Thomas Bruederli
2012-06-06 ae6d2de17f740915e47c64d210680eb5e9850335
New feature to add mail attachments using drag & drop on HTML5 enabled browsers
1 files added
5 files modified
180 ■■■■■ changed files
CHANGELOG 1 ●●●● patch | view | raw | blame | history
program/js/app.js 137 ●●●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 14 ●●●●● patch | view | raw | blame | history
skins/larry/images/filedrop.png patch | view | raw | blame | history
skins/larry/mail.css 27 ●●●●● patch | view | raw | blame | history
skins/larry/templates/compose.html 1 ●●●● patch | view | raw | blame | history
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)
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()
  {
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',
skins/larry/images/filedrop.png
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;
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>