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 New features
- it's now possible to set a default reminder for calendar components - it's now possible to set a default reminder for calendar components
using SOGoCalendarDefaultReminder 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 Enhancements
- we now automatically convert <img src=data...> into file attachments - we now automatically convert <img src=data...> into file attachments
using CIDs. This prevents Outlook issues. using CIDs. This prevents Outlook issues.
- updated Finnish translation - updated Finnish translation
- XMLHttpRequest is now loaded conditionaly (< IE9) - XMLHttpRequest.js is now loaded conditionaly (< IE9)
Bug fixes Bug fixes
- -

View File

@ -15,13 +15,6 @@
cssClass = "tbicon_addressbook"; cssClass = "tbicon_addressbook";
label = "Contacts"; label = "Contacts";
tooltip = "Select a recipient from an Address Book"; }, 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 = "#"; { link = "#";
isSafe = NO; isSafe = NO;
onclick = "return clickedEditorSave(this);"; onclick = "return clickedEditorSave(this);";

View File

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

View File

@ -11,26 +11,18 @@
title="panelTitle" title="panelTitle"
const:popup="YES" const:popup="YES"
const:userDefaultsKeys="SOGoMailComposeMessageType,SOGoMailReplyPlacement,SOGoMailSignature" 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"> <script type="text/javascript">
var mailIsReply = <var:string value="isMailReply"/>; var mailIsReply = <var:string value="isMailReply"/>;
var sourceUID = <var:string value="sourceUID"/>; var sourceUID = <var:string value="sourceUID"/>;
var sourceFolder = '<var:string value="sourceFolder" const:escapeHTML="NO"/>'; var sourceFolder = '<var:string value="sourceFolder" const:escapeHTML="NO"/>';
var localeCode = '<var:string value="localeCode"/>'; var localeCode = '<var:string value="localeCode"/>';
</script> </script>
<div class="popupMenu" id="contactsMenu"> <div class="popupMenu" id="contactsMenu">
<ul><!-- space --></ul> <ul><!-- space --></ul>
</div> </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"> <div class="menu" id="optionsMenu">
<ul class="choiceMenu"> <ul class="choiceMenu">
@ -101,20 +93,11 @@
</div> </div>
<div id="rightPanel"> <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="priority" id="priority" var:value="priority"/>
<input type="hidden" name="receipt" id="receipt" var:value="receipt"/> <input type="hidden" name="receipt" id="receipt" var:value="receipt"/>
<input type="hidden" name="isHTML" id="isHTML" var:value="isHTML"/> <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"> <div id="headerArea">
<span class="headerField" const:id="fromField"><var:string label:value="From" />:</span> <span class="headerField" const:id="fromField"><var:string label:value="From" />:</span>
<var:popup const:name="from" const:id="fromSelect" <var:popup const:name="from" const:id="fromSelect"
@ -126,19 +109,23 @@
<var:component className="UIxMailToSelection" <var:component className="UIxMailToSelection"
to="to" cc="cc" bcc="bcc" /> to="to" cc="cc" bcc="bcc" />
</div> </div>
<div class="addressListElement" id="subjectRow" <div id="subjectRow">
><span class="headerField"><var:string label:value="Subject" <span class="headerField"><var:string label:value="Subject"/>:</span>
/>:</span <input name="subject" type="text" class="textField" var:value="subject"/>
> </div>
<input name="subject" <div id="fileupload">
type="text" <ul id="attachments">
class="textField" <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:value="subject" <var:foreach list="attachmentAttrs" item="attachment"
/></div> ><li class="progressDone" var:data-filename="attachment.filename">
<!-- separator line --><hr class="fieldSeparator"/> <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>
</div> </li></var:foreach>
</ul>
</div>
</div><!-- #headerArea -->
<textarea id="text" name="text" rows="30" var:value="text"></textarea> <textarea id="text" name="text" rows="30" var:value="text"></textarea>
<!-- img rsrc:src="tbird_073_compose.png" alt="screenshot" / -->
</form> </form>
</div> </div>
<div id="dropZone" style="display: none;"><!-- dropzone --></div>
</var:component> </var:component>

View File

@ -104,24 +104,6 @@ div#headerArea div.addressList
overflow: auto; overflow: auto;
overflow-x: hidden; } 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 input.currentAttachment
{ position: fixed; { position: fixed;
top: 1em; top: 1em;
@ -131,30 +113,42 @@ input.attachment
{ position: absolute; { position: absolute;
left: -1000px; } left: -1000px; }
div#compose_attachments_list #dropZone
{ background-color: #ffffff; { position: absolute;
margin-left: 0px; background: #000 url('upload_document.png') no-repeat center center;
padding: 2px; opacity: 0.6;
border-bottom: 1px solid #fff; border: 4px dashed #fff;
border-right: 1px solid #fff; left: 0px;
border-top: 2px solid #222; right: 0px;
border-left: 2px solid #222; top: 0px;
-moz-border-top-colors: #9c9a94 #000 transparent; bottom: 0px;
-moz-border-left-colors: #9c9a94 #000 transparent; } 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 UL#attachments
{ cursor: default; { cursor: default;
margin: 0px; margin: 0px;
padding: 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-type: none;
list-style-image: none; list-style-image: none;
overflow: auto; overflow: auto;
@ -163,11 +157,44 @@ UL#attachments
-khtml-user-select: none; } -khtml-user-select: none; }
UL#attachments LI UL#attachments LI
{ float: left; }
UL#attachments LI[data-filename]
{ white-space: nowrap; { 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 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 #pageContent TEXTAREA
{ width: 99%; } { width: 99%; }
@ -176,6 +203,9 @@ TEXTAREA#text
{ display: none; { display: none;
background: #fff; } background: #fff; }
#cke_text
{ clear: both; }
/* Contacts search pane */ /* Contacts search pane */
DIV#contactsSearch DIV#contactsSearch

View File

@ -134,13 +134,10 @@ function onValidateDone(onSuccess) {
var safetyNet = createElement("div", "javascriptSafetyNet"); var safetyNet = createElement("div", "javascriptSafetyNet");
$('pageContent').insert({top: safetyNet}); $('pageContent').insert({top: safetyNet});
var input = currentAttachmentInput(); if (!document.busyAnim) {
if (input) var toolbar = document.getElementById("toolbar");
input.parentNode.removeChild(input);
var toolbar = document.getElementById("toolbar");
if (!document.busyAnim)
document.busyAnim = startAnimation(toolbar); document.busyAnim = startAnimation(toolbar);
}
var lastRow = $("lastRow"); var lastRow = $("lastRow");
lastRow.down("select").name = "popup_last"; lastRow.down("select").name = "popup_last";
@ -149,8 +146,6 @@ function onValidateDone(onSuccess) {
document.pageform.action = "send"; document.pageform.action = "send";
AIM.submit($(document.pageform), {'onComplete' : onPostComplete});
if (typeof onSuccess == 'function') if (typeof onSuccess == 'function')
onSuccess(); onSuccess();
@ -159,7 +154,8 @@ function onValidateDone(onSuccess) {
return true; return true;
} }
function onPostComplete(response) { function onPostComplete(http) {
var response = http.responseText;
if (response && response.length > 0) { if (response && response.length > 0) {
var jsonResponse = response.evalJSON(); var jsonResponse = response.evalJSON();
if (jsonResponse["status"] == "success") { if (jsonResponse["status"] == "success") {
@ -192,93 +188,67 @@ function onPostComplete(response) {
function clickedEditorSend() { function clickedEditorSend() {
onValidate(function() { 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; return false;
} }
function currentAttachmentInput() { function formatBytes(bytes, si) {
var input = null; var thresh = si ? 1000 : 1024;
if (bytes < thresh) return bytes + ' B';
var inputs = $("attachmentsArea").getElementsByTagName("input"); var units = si ? ['KiB','MiB','GiB'] : ['KB','MB','GB'];
var i = 0; var u = -1;
while (!input && i < inputs.length) do {
if ($(inputs[i]).hasClassName("currentAttachment")) bytes /= thresh;
input = inputs[i]; ++u;
else } while (bytes >= thresh);
i++; return bytes.toFixed(1) + ' ' + units[u];
return input;
} }
function clickedEditorAttach() { function createAttachment(file) {
var input = currentAttachmentInput(); var list = $('attachments');
if (!input) { var attachment;
var area = $("attachmentsArea"); 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) { a.appendChild(document.createTextNode(file.name));
area.setStyle({ display: "block" }); if (file.size)
onWindowResize(null); attachment.appendChild(new Element('span', { 'class': 'muted' }).update('(' + formatBytes(file.size, true) + ')'));
}
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));
} }
return false; return attachment;
}
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]);
} }
function clickedEditorSave() { function clickedEditorSave() {
var input = currentAttachmentInput();
if (input)
input.parentNode.removeChild(input);
var lastRow = $("lastRow"); var lastRow = $("lastRow");
lastRow.down("select").name = "popup_last"; lastRow.down("select").name = "popup_last";
window.shouldPreserve = true; window.shouldPreserve = true;
document.pageform.action = "save"; document.pageform.action = "save";
document.pageform.submit();
if (window.opener && window.opener.open && !window.opener.closed) triggerAjaxRequest(document.pageform.action, function (http) {
window.opener.refreshFolderByType('draft'); 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; return false;
} }
@ -301,10 +271,6 @@ function onTextFocus(event) {
} }
MailEditor.textFirstFocus = false; MailEditor.textFirstFocus = false;
} }
var input = currentAttachmentInput();
if (input)
input.parentNode.removeChild(input);
} }
function onTextKeyDown(event) { function onTextKeyDown(event) {
@ -397,7 +363,6 @@ function onHTMLFocus(event) {
function initAddresses() { function initAddresses() {
var addressList = $("addressList"); var addressList = $("addressList");
var i = 1;
addressList.select("input.textField").each(function (input) { addressList.select("input.textField").each(function (input) {
if (!input.readAttribute("readonly")) { if (!input.readAttribute("readonly")) {
input.addInterface(SOGoAutoCompletionInterface); 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() { function initMailEditor() {
if (composeMode != "html" && $("text")) if (composeMode != "html" && $("text"))
$("text").style.display = "block"; $("text").style.display = "block";
var list = $("attachments"); configureAttachments();
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");
initAddresses(); initAddresses();
var textarea = $("text");
var focusField = textarea; var focusField = textarea;
if (!mailIsReply) { if (!mailIsReply) {
focusField = $("addr_0"); focusField = $("addr_0");
@ -546,10 +572,6 @@ function onMenuCheckReturnReceipt(event) {
function getMenus() { function getMenus() {
return { return {
"attachmentsMenu": [ null, onRemoveAttachments,
onSelectAllAttachments,
"-",
clickedEditorAttach, null],
"optionsMenu": [ onMenuCheckReturnReceipt, "optionsMenu": [ onMenuCheckReturnReceipt,
"-", "-",
"priorityMenu" ], "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) { function attachmentDeleteCallback(http) {
if (http.readyState == 4) { if (http.readyState == 4) {
if (isHttpStatus204(http.status)) { if (isHttpStatus204(http.status)) {
@ -623,10 +624,6 @@ function onMenuSetPriority(event) {
priorityInput.value = priority; priorityInput.value = priority;
} }
function onSelectAllAttachments() {
$("attachments").selectAll();
}
function onSelectOptions(event) { function onSelectOptions(event) {
if (event.button == 0 || (isWebKit() && event.button == 1)) { if (event.button == 0 || (isWebKit() && event.button == 1)) {
var node = getTarget(event); var node = getTarget(event);
@ -645,39 +642,21 @@ function onWindowResize(event) {
var headerarea = $("headerArea"); var headerarea = $("headerArea");
var totalwidth = $("rightPanel").getWidth(); var totalwidth = $("rightPanel").getWidth();
var attachmentsarea = $("attachmentsArea");
var attachmentswidth = 0;
var subjectfield = headerarea.down("div#subjectRow span.headerField"); var subjectfield = headerarea.down("div#subjectRow span.headerField");
var subjectinput = headerarea.down("div#subjectRow input.textField"); 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 // Resize subject field
subjectinput.setStyle({ width: (totalwidth subjectinput.setStyle({ width: (totalwidth
- $(subjectfield).getWidth() - $(subjectfield).getWidth()
- attachmentswidth
- 17) + 'px' }); - 17) + 'px' });
// Resize from field // Resize from field
$("fromSelect").setStyle({ width: (totalwidth $("fromSelect").setStyle({ width: (totalwidth
- $("fromField").getWidth() - $("fromField").getWidth()
- attachmentswidth
- 15) + 'px' }); - 15) + 'px' });
// Resize address fields // Resize address fields
var addresslist = $('addressList'); // var addresslist = $('addressList');
addresslist.setStyle({ width: (totalwidth - attachmentswidth - 10) + 'px' }); // addresslist.setStyle({ width: (totalwidth - 10) + 'px' });
// Set textarea position
var hr = headerarea.select("hr").first();
textarea.setStyle({ 'top': hr.offsetTop + 'px' });
// Resize the textarea (message content) // Resize the textarea (message content)
var offsetTop = $('rightPanel').offsetTop + headerarea.getHeight(); 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: 8pt;
font-size: inherit; } font-size: inherit; }
INPUT[type="text"], INPUT[type="password"], INPUT[type="file"], INPUT[type="text"], INPUT[type="password"],
TEXTAREA TEXTAREA
{ border-top: 1px solid #909090; { border-top: 1px solid #909090;
border-left: 1px solid #909090; border-left: 1px solid #909090;
@ -730,7 +730,7 @@ TEXTAREA[disabled], TEXTAREA[readonly]
border-color: #ccc; border-color: #ccc;
color: #9ABCD8; } color: #9ABCD8; }
INPUT[type="text"], INPUT[type="password"], INPUT[type="file"], TEXTAREA INPUT[type="text"], INPUT[type="password"], TEXTAREA
{ background: url("input_bg.gif"); } { background: url("input_bg.gif"); }
TEXTAREA TEXTAREA
@ -788,7 +788,7 @@ INPUT[name="search"]
* Avoid using DIVS as buttons, they're only helpful when they have multiple * Avoid using DIVS as buttons, they're only helpful when they have multiple
* listeners for "onclick" * listeners for "onclick"
*/ */
A.button { .button, a.button {
padding: 0px 0.5em; padding: 0px 0.5em;
background: transparent url('btn_a_bg.png') no-repeat scroll top right; background: transparent url('btn_a_bg.png') no-repeat scroll top right;
display: block; display: block;
@ -801,28 +801,30 @@ A.button {
cursor: pointer; cursor: pointer;
} }
A.button SPAN { .button SPAN {
background: transparent url('btn_span_bg.png') no-repeat; background: transparent url('btn_span_bg.png') no-repeat;
display: block; display: block;
line-height: 13px; line-height: 13px;
height: 13px;
padding: 5px 2px 5px 5px; padding: 5px 2px 5px 5px;
cursor: pointer; cursor: pointer;
min-width: 70px; min-width: 70px;
vertical-align: top;
} }
A.button.actionButton SPAN .button.actionButton SPAN
{ font-weight: bold; } { font-weight: bold; }
A.button:active SPAN .button:active SPAN
{ background-position: bottom left; { background-position: bottom left;
padding: 6px 2px 4px 5px; } padding: 6px 2px 4px 5px; }
A.disabled.button, .disabled.button,
A.disabled.button:active, .disabled.button:active,
A.disabled.button SPAN .disabled.button SPAN
{ color: #999; } { color: #999; }
A.disabled.button:active SPAN .disabled.button:active SPAN
{ background-position: top left; { background-position: top left;
padding: 5px 2px 5px 5px; } 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