Improve upload of attachments to messages

pull/17/head
Francis Lachapelle 2013-12-18 16:36:49 -05:00
parent 1a900b05d9
commit 7369a82bab
12 changed files with 2002 additions and 302 deletions

4
NEWS
View File

@ -4,12 +4,14 @@
New features
- it's now possible to set a default reminder for calendar components
using SOGoCalendarDefaultReminder
- select multiple files to attach to a message or drag'n'drop files onto the
mail editor; will also now display progress of uploads
Enhancements
- we now automatically convert <img src=data...> into file attachments
using CIDs. This prevents Outlook issues.
- updated Finnish translation
- XMLHttpRequest is now loaded conditionaly (< IE9)
- XMLHttpRequest.js is now loaded conditionaly (< IE9)
Bug fixes
-

View File

@ -15,13 +15,6 @@
cssClass = "tbicon_addressbook";
label = "Contacts";
tooltip = "Select a recipient from an Address Book"; },
{ link = "#";
isSafe = NO;
onclick = "return clickedEditorAttach(this)";
image = "tb-compose-attach-flat-24x24.png";
cssClass = "tbicon_attach single-window-not-conditional";
label = "Attach";
tooltip = "Include an attachment"; },
{ link = "#";
isSafe = NO;
onclick = "return clickedEditorSave(this);";

View File

@ -56,6 +56,7 @@
#import <SOGo/WOResourceManager+SOGo.h>
#import <SOGoUI/UIxComponent.h>
#import <Mailer/SOGoDraftObject.h>
#import <Mailer/SOGoMailObject+Draft.h>
#import <Mailer/SOGoMailFolder.h>
#import <Mailer/SOGoMailAccount.h>
#import <Mailer/SOGoMailAccounts.h>
@ -63,6 +64,8 @@
#import <Contacts/SOGoContactFolder.h>
#import <Contacts/SOGoContactSourceFolder.h>
#import <UI/MailPartViewers/UIxMailSizeFormatter.h>
/*
UIxMailEditor
@ -89,8 +92,9 @@
id currentFolder;
/* these are for the inline attachment list */
NSString *attachmentName;
NSArray *attachmentNames;
NSDictionary *attachment;
NSArray *attachmentAttrs;
NSString *currentAttachment;
NSMutableArray *attachedFiles;
}
@ -117,6 +121,9 @@ static NSArray *infoKeys = nil;
priority = @"NORMAL";
receipt = nil;
currentFolder = nil;
currentAttachment = nil;
attachmentAttrs = nil;
attachedFiles = nil;
}
return self;
@ -137,8 +144,9 @@ static NSArray *infoKeys = nil;
[bcc release];
[sourceUID release];
[sourceFolder release];
[attachmentName release];
[attachmentNames release];
[attachment release];
[currentAttachment release];
[attachmentAttrs release];
[attachedFiles release];
[currentFolder release];
[super dealloc];
@ -369,14 +377,19 @@ static NSArray *infoKeys = nil;
return (([to count] + [cc count] + [bcc count]) > 0);
}
- (void) setAttachmentName: (NSString *) newAttachmentName
- (void) setAttachment: (NSDictionary *) newAttachment
{
ASSIGN (attachmentName, newAttachmentName);
ASSIGN (attachment, newAttachment);
}
- (NSString *) attachmentName
- (NSFormatter *) sizeFormatter
{
return attachmentName;
return [UIxMailSizeFormatter sharedMailSizeFormatter];
}
- (NSDictionary *) attachment
{
return attachment;
}
/* from addresses */
@ -417,7 +430,7 @@ static NSArray *infoKeys = nil;
- (NSDictionary *) storeInfo
{
[self debugWithFormat:@"storing info ..."];
return [self valuesForKeys:infoKeys];
return [self valuesForKeys: infoKeys];
}
/* contacts search */
@ -517,8 +530,8 @@ static NSArray *infoKeys = nil;
- (NSDictionary *) _scanAttachmentFilenamesInRequest: (id) httpBody
{
NSMutableDictionary *filenames;
NSDictionary *attachment;
NSMutableDictionary *files;
NSDictionary *file;
NSArray *parts;
unsigned int count, max;
NGMimeBodyPart *part;
@ -527,112 +540,136 @@ static NSArray *infoKeys = nil;
parts = [httpBody parts];
max = [parts count];
filenames = [NSMutableDictionary dictionaryWithCapacity: max];
files = [NSMutableDictionary dictionaryWithCapacity: max];
for (count = 0; count < max; count++)
{
part = [parts objectAtIndex: count];
header = (NGMimeContentDispositionHeaderField *)
[part headerForKey: @"content-disposition"];
mimeType = [(NGMimeType *)
[part headerForKey: @"content-type"] stringValue];
filename = [self _fixedFilename: [header filename]];
attachment = [NSDictionary dictionaryWithObjectsAndKeys:
filename, @"filename",
mimeType, @"mimetype", nil];
[filenames setObject: attachment forKey: [header name]];
header = (NGMimeContentDispositionHeaderField *)[part headerForKey: @"content-disposition"];
if ([[header name] hasPrefix: @"attachments"])
{
mimeType = [(NGMimeType *)[part headerForKey: @"content-type"] stringValue];
filename = [self _fixedFilename: [header filename]];
file = [NSDictionary dictionaryWithObjectsAndKeys:
filename, @"filename",
mimeType, @"mimetype",
[part body], @"body",
nil];
[files setObject: file forKey: [NSString stringWithFormat: @"%@_%@", [header name], filename]];
}
}
return filenames;
return files;
}
- (BOOL) _saveAttachments
- (NSException *) _saveAttachments
{
NSException *error;
WORequest *request;
NSEnumerator *allKeys;
NSString *key;
BOOL success;
NSDictionary *filenames;
NSEnumerator *allAttachments;
NSDictionary *attrs, *filenames;
NGMimeType *mimeType;
id httpBody;
SOGoDraftObject *co;
success = YES;
error = nil;
request = [context request];
httpBody = [[request httpRequest] body];
filenames = [self _scanAttachmentFilenamesInRequest: httpBody];
mimeType = [[request httpRequest] contentType];
if ([[mimeType type] isEqualToString: @"multipart"])
{
httpBody = [[request httpRequest] body];
filenames = [self _scanAttachmentFilenamesInRequest: httpBody];
co = [self clientObject];
allKeys = [[request formValueKeys] objectEnumerator];
while ((key = [allKeys nextObject]) && success)
if ([key hasPrefix: @"attachment"])
success
= (![co saveAttachment: (NSData *) [request formValueForKey: key]
withMetadata: [filenames objectForKey: key]]);
co = [self clientObject];
allAttachments = [filenames objectEnumerator];
while ((attrs = [allAttachments nextObject]) && !error)
{
error = [co saveAttachment: (NSData *) [attrs objectForKey: @"body"]
withMetadata: attrs];
// Keep the name of the last attachment saved
ASSIGN(currentAttachment, [attrs objectForKey: @"filename"]);
}
}
return success;
return error;
}
- (BOOL) _saveFormInfo
- (NSException *) _saveFormInfo
{
NSDictionary *info;
NSException *error;
BOOL success;
SOGoDraftObject *co;
co = [self clientObject];
[co fetchInfo];
success = YES;
if ([self _saveAttachments])
error = [self _saveAttachments];
if (!error)
{
info = [self storeInfo];
[co setHeaders: info];
[co setIsHTML: isHTML];
[co setText: (isHTML ? [NSString stringWithFormat: @"<html>%@</html>", text] : text)];;
error = [co storeInfo];
if (error)
{
[self errorWithFormat: @"failed to store draft: %@", error];
// TODO: improve error handling
success = NO;
}
}
else
success = NO;
// TODO: wrap content
return success;
return error;
}
- (id) failedToSaveFormResponse
- (id) failedToSaveFormResponse: (NSString *) msg
{
// TODO: improve error handling
return [NSException exceptionWithHTTPStatus:500 /* server error */
reason:@"failed to store draft object on server!"];
NSDictionary *d;
d = [NSDictionary dictionaryWithObjectsAndKeys: msg, @"textStatus", nil];
return [self responseWithStatus: 500
andString: [d jsonRepresentation]];
}
/* attachment helper */
- (NSArray *) attachmentNames
- (NSArray *) attachmentAttrs
{
NSArray *a;
SOGoDraftObject *co;
SOGoMailObject *mail;
if (!attachmentNames)
co = [self clientObject];
if (!attachmentAttrs || ![co imap4URL])
{
[co fetchInfo];
if ([co IMAP4ID] > -1)
{
mail = [[[SOGoMailObject alloc] initWithImap4URL: [co imap4URL] inContainer: [co container]] autorelease];
a = [mail fetchFileAttachmentKeys];
ASSIGN (attachmentAttrs, a);
}
}
if (currentAttachment)
{
a = [[self clientObject] fetchAttachmentNames];
ASSIGN (attachmentNames,
[a sortedArrayUsingSelector: @selector (compare:)]);
// When currentAttachment is defined, only return the attributes of the last
// attachment saved
NSEnumerator *allAttachments;
NSDictionary* attrs;
allAttachments = [attachmentAttrs objectEnumerator];
while ((attrs = [allAttachments nextObject]))
{
if ([[attrs objectForKey: @"filename"] isEqualToString: currentAttachment])
{
return [NSArray arrayWithObject: attrs];
}
}
}
return attachmentNames;
return attachmentAttrs;
}
- (BOOL) hasAttachments
{
return [[self attachmentNames] count] > 0 ? YES : NO;
return [[self attachmentAttrs] count] > 0 ? YES : NO;
}
- (NSString *) uid
@ -658,14 +695,20 @@ static NSArray *infoKeys = nil;
{
id result;
if ([self _saveFormInfo])
result = [self _saveFormInfo];
if (!result)
{
result = [[self clientObject] save];
if (!result)
result = [self responseWith204];
}
if (!result)
{
attachmentAttrs = nil;
NSArray *attrs = [self attachmentAttrs];
result = [self responseWithStatus: 200
andString: [attrs jsonRepresentation]];
}
else
result = [self failedToSaveFormResponse];
result = [self failedToSaveFormResponse: [result reason]];
return result;
}
@ -740,10 +783,11 @@ static NSArray *infoKeys = nil;
error = [self validateForSend];
if (!error)
{
if ([self _saveFormInfo])
error = [self _saveFormInfo];
if (!error)
error = [co sendMail];
else
error = [self failedToSaveFormResponse];
error = [self failedToSaveFormResponse: [error reason]];
}
if (error)

View File

@ -11,26 +11,18 @@
title="panelTitle"
const:popup="YES"
const:userDefaultsKeys="SOGoMailComposeMessageType,SOGoMailReplyPlacement,SOGoMailSignature"
const:jsFiles="UIxMailToSelection.js,ckeditor/ckeditor.js,SOGoAutoCompletion.js,ContactsUI.js">
const:jsFiles="UIxMailToSelection.js,ckeditor/ckeditor.js,SOGoAutoCompletion.js,ContactsUI.js,jquery-ui.js,jquery.fileupload.js,jquery.iframe-transport.js"
const:cssFiles="jquery.fileupload.css">
<script type="text/javascript">
var mailIsReply = <var:string value="isMailReply"/>;
var sourceUID = <var:string value="sourceUID"/>;
var sourceFolder = '<var:string value="sourceFolder" const:escapeHTML="NO"/>';
var localeCode = '<var:string value="localeCode"/>';
</script>
<div class="popupMenu" id="contactsMenu">
<ul><!-- space --></ul>
</div>
<div class="menu" id="attachmentsMenu">
<ul>
<li><var:string label:value="Open"/></li>
<li><var:string label:value="Delete" /></li>
<li><var:string label:value="Select All" /></li>
<li><!-- separator --></li>
<li><var:string label:value="Attach File(s)..." /></li>
<li><var:string label:value="Attach Web Page..." /></li>
</ul>
</div>
<div class="menu" id="optionsMenu">
<ul class="choiceMenu">
@ -101,20 +93,11 @@
</div>
<div id="rightPanel">
<form const:href="" name="pageform" enctype="multipart/form-data" autocomplete="off">
<form href="save" name="pageform" enctype="multipart/form-data" autocomplete="off">
<input type="hidden" name="priority" id="priority" var:value="priority"/>
<input type="hidden" name="receipt" id="receipt" var:value="receipt"/>
<input type="hidden" name="isHTML" id="isHTML" var:value="isHTML"/>
<div id="attachmentsArea">
<var:string label:value="Attachments:" />
<ul id="attachments">
<var:foreach list="attachmentNames" item="attachmentName"
><li var:title="attachmentName"><img rsrc:src="attachment.gif"
/><var:string value="attachmentName"
/></li></var:foreach>
</ul>
</div>
<div id="headerArea">
<span class="headerField" const:id="fromField"><var:string label:value="From" />:</span>
<var:popup const:name="from" const:id="fromSelect"
@ -126,19 +109,23 @@
<var:component className="UIxMailToSelection"
to="to" cc="cc" bcc="bcc" />
</div>
<div class="addressListElement" id="subjectRow"
><span class="headerField"><var:string label:value="Subject"
/>:</span
>
<input name="subject"
type="text"
class="textField"
var:value="subject"
/></div>
<!-- separator line --><hr class="fieldSeparator"/>
</div>
<div id="subjectRow">
<span class="headerField"><var:string label:value="Subject"/>:</span>
<input name="subject" type="text" class="textField" var:value="subject"/>
</div>
<div id="fileupload">
<ul id="attachments">
<li class="attachButton"><span class="button fileinput-button"><span><img rsrc:src="title_attachment_14x14.png" /> <var:string label:value="Attach"/></span><input id="fileUpload" type="file" name="attachments" const:multiple="multiple"/></span></li>
<var:foreach list="attachmentAttrs" item="attachment"
><li class="progressDone" var:data-filename="attachment.filename">
<i class="icon-attachment"><!-- icon --></i><a var:href="attachment.url" target="_new"><var:string value="attachment.filename"/></a><span class="muted">(<var:string value="attachment.size" formatter="sizeFormatter" />)</span>
</li></var:foreach>
</ul>
</div>
</div><!-- #headerArea -->
<textarea id="text" name="text" rows="30" var:value="text"></textarea>
<!-- img rsrc:src="tbird_073_compose.png" alt="screenshot" / -->
</form>
</div>
<div id="dropZone" style="display: none;"><!-- dropzone --></div>
</var:component>

View File

@ -104,24 +104,6 @@ div#headerArea div.addressList
overflow: auto;
overflow-x: hidden; }
div#attachmentsArea
{ display: none;
float: right;
width: 200px;
padding: 2px 5px 0;
margin: auto;
border-left: 1px solid #888; }
hr.fieldSeparator
{ background-color: #848284;
border: 0;
clear: both;
color: #848284;
height: 1px;
margin: 0px;
padding: 0px;
width: 100%; }
input.currentAttachment
{ position: fixed;
top: 1em;
@ -131,30 +113,42 @@ input.attachment
{ position: absolute;
left: -1000px; }
div#compose_attachments_list
{ background-color: #ffffff;
margin-left: 0px;
padding: 2px;
border-bottom: 1px solid #fff;
border-right: 1px solid #fff;
border-top: 2px solid #222;
border-left: 2px solid #222;
-moz-border-top-colors: #9c9a94 #000 transparent;
-moz-border-left-colors: #9c9a94 #000 transparent; }
#dropZone
{ position: absolute;
background: #000 url('upload_document.png') no-repeat center center;
opacity: 0.6;
border: 4px dashed #fff;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
z-index: 999; }
#dropZone div
{ position: absolute;
color: #fff;
font-size: 18px;
height: 100px;
width: 300px;
margin: 60px 0 0 -150px;
left: 50%;
top: 50%;
text-align: center;
}
#fileupload {
margin-top: 5px;
clear: both;
}
.button.fileinput-button
{ display: inline-block;
float: none; }
UL#attachments
{ cursor: default;
margin: 0px;
padding: 0px;
height: 100%;
border-bottom: 1px solid #eee;
border-right: 1px solid #eee;
border-top: 1px solid #222;
border-left: 1px solid #222;
background-color: #CCDDEC;
background-image: url("input_bg.gif");
-moz-border-top-colors: #9c9a94 #000 transparent;
-moz-border-left-colors: #9c9a94 #000 transparent;
list-style-type: none;
list-style-image: none;
overflow: auto;
@ -163,11 +157,44 @@ UL#attachments
-khtml-user-select: none; }
UL#attachments LI
{ float: left; }
UL#attachments LI[data-filename]
{ white-space: nowrap;
padding-bottom: 1px; }
line-height: 18px;
margin: 3px 6px; }
UL#attachments LI[data-filename] SPAN
{ margin-left: 5px; }
UL#attachments LI[data-filename] A,
UL#attachments LI[data-filename] SPAN
{ padding-left: 2px;
vertical-align: top; }
UL#attachments LI IMG
{ vertical-align: bottom; }
{ vertical-align: top; }
UL#attachments .icon-attachment
{ background: url('attachment.png') no-repeat top left;
display: inline-block;
width: 16px;
height: 16px; }
UL#attachments .progress0 .icon-attachment
{ background-position: 0px 0px; }
UL#attachments .progress1 .icon-attachment
{ background-position: -16px 0px; }
UL#attachments .progress2 .icon-attachment
{ background-position: -32px 0px; }
UL#attachments .progress3 .icon-attachment
{ background-position: -48px 0px; }
UL#attachments .progress4 .icon-attachment
{ background-position: -64px 0px; }
UL#attachments .progressDone .icon-attachment
{ background-position: -80px 0px; }
UL#attachments .progressDone .icon-attachment:hover
{ background-position: -96px 0px;
cursor: pointer; }
#pageContent TEXTAREA
{ width: 99%; }
@ -176,6 +203,9 @@ TEXTAREA#text
{ display: none;
background: #fff; }
#cke_text
{ clear: both; }
/* Contacts search pane */
DIV#contactsSearch

View File

@ -134,13 +134,10 @@ function onValidateDone(onSuccess) {
var safetyNet = createElement("div", "javascriptSafetyNet");
$('pageContent').insert({top: safetyNet});
var input = currentAttachmentInput();
if (input)
input.parentNode.removeChild(input);
var toolbar = document.getElementById("toolbar");
if (!document.busyAnim)
if (!document.busyAnim) {
var toolbar = document.getElementById("toolbar");
document.busyAnim = startAnimation(toolbar);
}
var lastRow = $("lastRow");
lastRow.down("select").name = "popup_last";
@ -149,8 +146,6 @@ function onValidateDone(onSuccess) {
document.pageform.action = "send";
AIM.submit($(document.pageform), {'onComplete' : onPostComplete});
if (typeof onSuccess == 'function')
onSuccess();
@ -159,7 +154,8 @@ function onValidateDone(onSuccess) {
return true;
}
function onPostComplete(response) {
function onPostComplete(http) {
var response = http.responseText;
if (response && response.length > 0) {
var jsonResponse = response.evalJSON();
if (jsonResponse["status"] == "success") {
@ -192,93 +188,67 @@ function onPostComplete(response) {
function clickedEditorSend() {
onValidate(function() {
document.pageform.submit();
triggerAjaxRequest(document.pageform.action,
onPostComplete,
null,
Form.serialize(document.pageform), // excludes the file input
{ "Content-type": "application/x-www-form-urlencoded" });
});
return false;
}
function currentAttachmentInput() {
var input = null;
var inputs = $("attachmentsArea").getElementsByTagName("input");
var i = 0;
while (!input && i < inputs.length)
if ($(inputs[i]).hasClassName("currentAttachment"))
input = inputs[i];
else
i++;
return input;
function formatBytes(bytes, si) {
var thresh = si ? 1000 : 1024;
if (bytes < thresh) return bytes + ' B';
var units = si ? ['KiB','MiB','GiB'] : ['KB','MB','GB'];
var u = -1;
do {
bytes /= thresh;
++u;
} while (bytes >= thresh);
return bytes.toFixed(1) + ' ' + units[u];
}
function clickedEditorAttach() {
var input = currentAttachmentInput();
if (!input) {
var area = $("attachmentsArea");
function createAttachment(file) {
var list = $('attachments');
var attachment;
if (list.select('[data-filename="'+file.name+'"]').length == 0) {
// File is not already uploaded
var attachment = createElement('li', null, ['muted progress0'], null, { 'data-filename': file.name }, list);
attachment.appendChild(new Element('i', { 'class': 'icon-attachment' }));
var a = createElement('a', null, null, null, {'href': '#', 'target': '_new' }, attachment);
if (!area.style.display) {
area.setStyle({ display: "block" });
onWindowResize(null);
}
var inputs = area.getElementsByTagName("input");
var attachmentName = "attachment" + attachmentCount;
var newAttachment = createElement("input", attachmentName,
"currentAttachment", null,
{ type: "file",
name: attachmentName },
area);
attachmentCount++;
newAttachment.observe("change",
onAttachmentChange.bindAsEventListener(newAttachment));
a.appendChild(document.createTextNode(file.name));
if (file.size)
attachment.appendChild(new Element('span', { 'class': 'muted' }).update('(' + formatBytes(file.size, true) + ')'));
}
return false;
}
function onAttachmentChange(event) {
if (this.value == "")
this.parentNode.removeChild(this);
else {
this.addClassName("attachment");
this.removeClassName("currentAttachment");
var list = $("attachments");
createAttachment(this, list);
clickedEditorAttach(null);
}
}
function createAttachment(node, list) {
var attachment = createElement("li", null, null, { node: node }, null, list);
createElement("img", null, null, { src: ResourcesURL + "/attachment.gif" },
null, attachment);
var filename = node.value;
var separator;
if (navigator.appVersion.indexOf("Windows") > -1)
separator = "\\";
else
separator = "/";
var fileArray = filename.split(separator);
var attachmentName = document.createTextNode(fileArray[fileArray.length-1]);
attachment.appendChild(attachmentName);
attachment.writeAttribute("title", fileArray[fileArray.length-1]);
return attachment;
}
function clickedEditorSave() {
var input = currentAttachmentInput();
if (input)
input.parentNode.removeChild(input);
var lastRow = $("lastRow");
lastRow.down("select").name = "popup_last";
window.shouldPreserve = true;
document.pageform.action = "save";
document.pageform.submit();
if (window.opener && window.opener.open && !window.opener.closed)
window.opener.refreshFolderByType('draft');
triggerAjaxRequest(document.pageform.action, function (http) {
if (http.readyState == 4) {
if (http.status == 200) {
if (window.opener && window.opener.open && !window.opener.closed)
window.opener.refreshFolderByType('draft');
}
else {
var response = http.responseText.evalJSON(true);
showAlertDialog("Error while saving the draft: " + response.textStatus);
}
}
},
null,
Form.serialize(document.pageform), // excludes the file input
{ "Content-type": "application/x-www-form-urlencoded" });
return false;
}
@ -301,10 +271,6 @@ function onTextFocus(event) {
}
MailEditor.textFirstFocus = false;
}
var input = currentAttachmentInput();
if (input)
input.parentNode.removeChild(input);
}
function onTextKeyDown(event) {
@ -397,7 +363,6 @@ function onHTMLFocus(event) {
function initAddresses() {
var addressList = $("addressList");
var i = 1;
addressList.select("input.textField").each(function (input) {
if (!input.readAttribute("readonly")) {
input.addInterface(SOGoAutoCompletionInterface);
@ -424,23 +389,84 @@ function configureDragHandle() {
}
}
function configureAttachments() {
var list = $("attachments");
if (!list) return;
list.on('click', 'a', function (event, element) {
if (!element.up('li').hasClassName('progressDone'))
return false;
});
list.on('click', 'i.icon-attachment', function (event, element) {
var item = element.up('li');
if (item.hasClassName('progressDone')) {
var filename = item.readAttribute('data-filename');
var url = "" + window.location;
var parts = url.split("/");
parts[parts.length-1] = "deleteAttachment?filename=" + encodeURIComponent(filename);
url = parts.join("/");
triggerAjaxRequest(url, attachmentDeleteCallback, item);
}
});
var dropzone = jQuery('#dropZone');
jQuery('#fileUpload').fileupload({
// With singleFileUploads option enabled, the 'add' and 'done' (or 'fail') callbacks
// are called once for each file in the selection for XHR file uploads
singleFileUploads: true,
dataType: 'json',
add: function (e, data) {
var file = data.files[0];
var attachment = createAttachment(file);
if (attachment) {
file.attachment = attachment;
data.submit();
}
if (dropzone.is(":visible"))
dropzone.fadeOut('fast');
},
done: function (e, data) {
var attachment = data.files[0].attachment;
var attrs = data.result[data.result.length-1];
attachment.className = 'progressDone';
attachment.down('a').setAttribute('href', attrs.url);
if (window.opener && window.opener.open && !window.opener.closed)
window.opener.refreshFolderByType('draft');
},
fail: function (e, data) {
var attachment = data.files[0].attachment;
var filename = data.files[0].name;
var response = data.xhr().response.evalJSON();
showAlertDialog("Error while uploading the file " + filename + ": " + response.textStatus);
attachment.remove();
},
dragover: function (e, data) {
if (!dropzone.is(":visible"))
dropzone.show();
},
progress: function (e, data) {
var progress = parseInt(data.loaded / data.total * 4, 10);
var attachment = data.files[0].attachment;
attachment.className = 'muted progress' + progress;
}
});
dropzone.on('dragleave', function (e) {
dropzone.fadeOut('fast');
});
}
function initMailEditor() {
if (composeMode != "html" && $("text"))
$("text").style.display = "block";
var list = $("attachments");
if (!list) return;
list.multiselect = true;
list.on("click", onRowClick);
list.attachMenu("attachmentsMenu");
var elements = $(list).childNodesWithTag("li");
if (elements.length > 0)
$("attachmentsArea").setStyle({ display: "block" });
var textarea = $("text");
configureAttachments();
initAddresses();
var textarea = $("text");
var focusField = textarea;
if (!mailIsReply) {
focusField = $("addr_0");
@ -546,10 +572,6 @@ function onMenuCheckReturnReceipt(event) {
function getMenus() {
return {
"attachmentsMenu": [ null, onRemoveAttachments,
onSelectAllAttachments,
"-",
clickedEditorAttach, null],
"optionsMenu": [ onMenuCheckReturnReceipt,
"-",
"priorityMenu" ],
@ -561,27 +583,6 @@ function getMenus() {
};
}
function onRemoveAttachments() {
var list = $("attachments");
var nodes = list.getSelectedNodes();
for (var i = nodes.length-1; i > -1; i--) {
var input = $(nodes[i]).node;
if (input) {
input.parentNode.removeChild(input);
list.removeChild(nodes[i]);
}
else {
var filename = nodes[i].title;
var url = "" + window.location;
var parts = url.split("/");
parts[parts.length-1] = "deleteAttachment?filename=" + encodeURIComponent(filename);
url = parts.join("/");
triggerAjaxRequest(url, attachmentDeleteCallback,
nodes[i]);
}
}
}
function attachmentDeleteCallback(http) {
if (http.readyState == 4) {
if (isHttpStatus204(http.status)) {
@ -623,10 +624,6 @@ function onMenuSetPriority(event) {
priorityInput.value = priority;
}
function onSelectAllAttachments() {
$("attachments").selectAll();
}
function onSelectOptions(event) {
if (event.button == 0 || (isWebKit() && event.button == 1)) {
var node = getTarget(event);
@ -645,39 +642,21 @@ function onWindowResize(event) {
var headerarea = $("headerArea");
var totalwidth = $("rightPanel").getWidth();
var attachmentsarea = $("attachmentsArea");
var attachmentswidth = 0;
var subjectfield = headerarea.down("div#subjectRow span.headerField");
var subjectinput = headerarea.down("div#subjectRow input.textField");
if (attachmentsarea.style.display) {
// Resize attachments list
attachmentswidth = attachmentsarea.getWidth();
fromfield = $(document).getElementsByClassName('headerField', headerarea)[0];
var height = headerarea.getHeight() - fromfield.getHeight() - subjectfield.getHeight() - 10;
if (Prototype.Browser.IE)
$("attachments").setStyle({ height: (height - 13) + 'px' });
else
$("attachments").setStyle({ height: height + 'px' });
}
// Resize subject field
subjectinput.setStyle({ width: (totalwidth
- $(subjectfield).getWidth()
- attachmentswidth
- 17) + 'px' });
// Resize from field
$("fromSelect").setStyle({ width: (totalwidth
- $("fromField").getWidth()
- attachmentswidth
- 15) + 'px' });
// Resize address fields
var addresslist = $('addressList');
addresslist.setStyle({ width: (totalwidth - attachmentswidth - 10) + 'px' });
// Set textarea position
var hr = headerarea.select("hr").first();
textarea.setStyle({ 'top': hr.offsetTop + 'px' });
// var addresslist = $('addressList');
// addresslist.setStyle({ width: (totalwidth - 10) + 'px' });
// Resize the textarea (message content)
var offsetTop = $('rightPanel').offsetTop + headerarea.getHeight();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -714,7 +714,7 @@ DIV, TEXTAREA, INPUT, SELECT
font-size: 8pt;
font-size: inherit; }
INPUT[type="text"], INPUT[type="password"], INPUT[type="file"],
INPUT[type="text"], INPUT[type="password"],
TEXTAREA
{ border-top: 1px solid #909090;
border-left: 1px solid #909090;
@ -730,7 +730,7 @@ TEXTAREA[disabled], TEXTAREA[readonly]
border-color: #ccc;
color: #9ABCD8; }
INPUT[type="text"], INPUT[type="password"], INPUT[type="file"], TEXTAREA
INPUT[type="text"], INPUT[type="password"], TEXTAREA
{ background: url("input_bg.gif"); }
TEXTAREA
@ -788,7 +788,7 @@ INPUT[name="search"]
* Avoid using DIVS as buttons, they're only helpful when they have multiple
* listeners for "onclick"
*/
A.button {
.button, a.button {
padding: 0px 0.5em;
background: transparent url('btn_a_bg.png') no-repeat scroll top right;
display: block;
@ -801,28 +801,30 @@ A.button {
cursor: pointer;
}
A.button SPAN {
.button SPAN {
background: transparent url('btn_span_bg.png') no-repeat;
display: block;
line-height: 13px;
height: 13px;
padding: 5px 2px 5px 5px;
cursor: pointer;
min-width: 70px;
vertical-align: top;
}
A.button.actionButton SPAN
.button.actionButton SPAN
{ font-weight: bold; }
A.button:active SPAN
.button:active SPAN
{ background-position: bottom left;
padding: 6px 2px 4px 5px; }
A.disabled.button,
A.disabled.button:active,
A.disabled.button SPAN
.disabled.button,
.disabled.button:active,
.disabled.button SPAN
{ color: #999; }
A.disabled.button:active SPAN
.disabled.button:active SPAN
{ background-position: top left;
padding: 5px 2px 5px 5px; }

View File

@ -0,0 +1,36 @@
@charset "UTF-8";
/*
* jQuery File Upload Plugin CSS 1.3.0
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
.fileinput-button {
position: relative;
overflow: hidden;
}
.fileinput-button input {
position: absolute;
top: 0;
right: 0;
margin: 0;
opacity: 0;
-ms-filter: 'alpha(opacity=0)';
font-size: 200px;
direction: ltr;
cursor: pointer;
}
/* Fixes for IE < 8 */
@media screen\9 {
.fileinput-button input {
filter: alpha(opacity=0);
font-size: 100%;
height: 100%;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,210 @@
/*
* jQuery Iframe Transport Plugin 1.8.1
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/*jslint unparam: true, nomen: true */
/*global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['jquery'], factory);
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0;
// The iframe transport accepts four 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),
// 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}]
// options.initialIframeSrc: the URL of the initial iframe src,
// by default set to "javascript:false;"
$.ajaxTransport('iframe', function (options) {
if (options.async) {
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6:
/*jshint scripturl: true */
var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
/*jshint scripturl: false */
form,
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';
}
// 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="' + initialIframeSrc +
'" name="iframe-transport-' + counter + '"></iframe>'
).bind('load', function () {
var fileInputClones,
paramNames = $.isArray(options.paramName) ?
options.paramName : [options.paramName];
iframe
.unbind('load')
.bind('load', function () {
var response;
// Wrap in a try/catch block to catch exceptions thrown
// when trying to access cross-domain iframe contents:
try {
response = iframe.contents();
// Google Chrome and Firefox do not throw an
// exception when calling iframe.contents() on
// cross-domain requests, so we unify the response:
if (!response.length || !response[0].firstChild) {
throw new Error();
}
} catch (e) {
response = undefined;
}
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': response}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="' + initialIframeSrc + '"></iframe>')
.appendTo(form);
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'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
if (options.paramName) {
options.fileInput.each(function (index) {
$(this).prop(
'name',
paramNames[index] || options.paramName
);
});
}
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]);
$(input).prop('name', clone.prop('name'));
clone.replaceWith(input);
});
}
});
form.append(iframe).appendTo(document.body);
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', initialIframeSrc);
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// 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 && $(iframe[0].body).text();
},
'iframe json': function (iframe) {
return iframe && $.parseJSON($(iframe[0].body).text());
},
'iframe html': function (iframe) {
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 iframe && $.globalEval($(iframe[0].body).text());
}
}
});
}));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB