From 7369a82bab05f5044432343de49c70a7600166e5 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Wed, 18 Dec 2013 16:36:49 -0500 Subject: [PATCH] Improve upload of attachments to messages --- NEWS | 4 +- UI/MailerUI/Toolbars/SOGoDraftObject.toolbar | 7 - UI/MailerUI/UIxMailEditor.m | 186 ++- UI/Templates/MailerUI/UIxMailEditor.wox | 53 +- UI/WebServerResources/UIxMailEditor.css | 108 +- UI/WebServerResources/UIxMailEditor.js | 261 ++- UI/WebServerResources/attachment.png | Bin 0 -> 1806 bytes UI/WebServerResources/generic.css | 22 +- UI/WebServerResources/jquery.fileupload.css | 36 + UI/WebServerResources/jquery.fileupload.js | 1417 +++++++++++++++++ .../jquery.iframe-transport.js | 210 +++ UI/WebServerResources/upload_document.png | Bin 0 -> 1322 bytes 12 files changed, 2002 insertions(+), 302 deletions(-) create mode 100644 UI/WebServerResources/attachment.png create mode 100755 UI/WebServerResources/jquery.fileupload.css create mode 100755 UI/WebServerResources/jquery.fileupload.js create mode 100755 UI/WebServerResources/jquery.iframe-transport.js create mode 100644 UI/WebServerResources/upload_document.png diff --git a/NEWS b/NEWS index 7a1224ab4..7ae477d46 100644 --- a/NEWS +++ b/NEWS @@ -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 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 - diff --git a/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar b/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar index 05e7a38ff..84fe4cea5 100644 --- a/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar +++ b/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar @@ -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);"; diff --git a/UI/MailerUI/UIxMailEditor.m b/UI/MailerUI/UIxMailEditor.m index fe60f39d3..e2b2c7ffd 100644 --- a/UI/MailerUI/UIxMailEditor.m +++ b/UI/MailerUI/UIxMailEditor.m @@ -56,6 +56,7 @@ #import #import #import +#import #import #import #import @@ -63,6 +64,8 @@ #import #import +#import + /* 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: @"%@", 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) diff --git a/UI/Templates/MailerUI/UIxMailEditor.wox b/UI/Templates/MailerUI/UIxMailEditor.wox index 2658ddfef..3dab61156 100644 --- a/UI/Templates/MailerUI/UIxMailEditor.wox +++ b/UI/Templates/MailerUI/UIxMailEditor.wox @@ -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"> +
-
-
+ -
- -
    -
  • -
-
:
-
: -
-
-
+
+ : + +
+
+
    +
  • +
  • + () +
  • +
+
+ - + + diff --git a/UI/WebServerResources/UIxMailEditor.css b/UI/WebServerResources/UIxMailEditor.css index 400c9ff9d..019401d08 100644 --- a/UI/WebServerResources/UIxMailEditor.css +++ b/UI/WebServerResources/UIxMailEditor.css @@ -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 diff --git a/UI/WebServerResources/UIxMailEditor.js b/UI/WebServerResources/UIxMailEditor.js index a41448fca..24fc7c568 100644 --- a/UI/WebServerResources/UIxMailEditor.js +++ b/UI/WebServerResources/UIxMailEditor.js @@ -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(); diff --git a/UI/WebServerResources/attachment.png b/UI/WebServerResources/attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..85f43eacd12b08fe2db1efc203a85e5c0f5ff1e7 GIT binary patch literal 1806 zcmV+p2l4ocP)yG!O|Wf+7hhN?2+X1q>015foAA5*MN-Z6N__F-H6Y zl!yx$qA?M|Vic4R5sECbv<8F%5nS6sTl!x8eXnz$^KNh7YbgXwnDk`c%sFS4^WAf1 z&NR-sTCOMy%$ql_Ph4Ew)6g&$*553yq`toX8)(^r1q%+^3y$+!J%T;xzZV)~_+L_Q zX3!JO*{!AjAoA7FSI`#pPySEzyRpup&?vLsb+OjJYURoLSbLi^xtp)OUA^4g+&S3p zP3+XJ30%&>dpSQpf04_N7!c*QFtVX3P&u^E2==A!4y}X=7^Jp|30(cj@Q*=9ApK4> zqHXSbq*{dlu41Y!}(1$@L4stVg>z!Jy*=1NEUgVCrMXe?|4Iv!|q!;|JpW z`SX*itE)f9F8W|<%~dr5qJ{PWCJURPH) zX5qqxC4Os|J9lpX#Kc5J!ydt4a8Y4l;fpqauflj38i{fL^cw&`ZlHey^aV1ndjNQi z_GoAnyuZXlUcmhXb@7asi%fw>U-Lg?=D!^JIn13O13Z`AX#E0k^q7tS255j6FttF$ zgPdzSq&RI7APt4b z@1RE*=!a;VK=w}fpFl(0GtKCKOWy?gcDwpf06c|ECbFvS`LpQTaT)Y==Iw&)ND<8p z5$l(r030zgH3Xg=J|n@bcXs_);RP_{eb{PgK|#T+tgNhd>FMc5X`cf45gMCNx(T~< zEiEnWwR-jHb2&LV#k6P23ta#wAveR^d}4ltx)CAtF7m4k0FP5oqOD6yxEKKU(9zjv zP=U3^=xct3)!{z+ejY3_ul{uOb@8(2*SvcnZv^n_PoN{TmKK!o{L&GCLx)}82s}58 zHk^m`&c3}>5dh}pjF0L2y=cHo(9-%#)CxZUFe0`WM=H9s|m7-$Pyib>8U%c+Tki&L72trS=y3IydCO z(mw|&4B8hn&HFXf$!A}-{tEb0o3nRICamuQIB4*74IUT{qsAMp^YHMYvk?Gt^VEed zsV&6~o1ArBIqNpjf18pdCu8l~!*2v&=70`> z7j*dZ0BPC)dGtv21p!!DSvkD8xcIaI@EY)|z+RPh2%Sd9{mt0~hTVwVa^ycW0O;m6 zj;XtPuJX?fY!y7y#NKv{fOHd0M5fH7kVn%}8nRKEh=fL~|2$+b2)d zL;%Qnc9?yqGsF<`yKPN{Q1~UBfddsn@mggBifrK!DlgF`WITiE%xSW)**jo<2Hk^f zD6ywcIn#P*EPZ7$^zVW%j~#?;D6uz}psoDWB}kWvP-(#>NSBbEP}gX4TRiV2s#W?L zR{-8}Yx~Gc3}6P_@M-Gb&(wMWbT1@DoPo2ObBS@&4$jRYJB?5pAP;x|BYH!TYtR&a z=v8GSGJa`lH^20rCdTzcb21fNq?A^DjeJFF(W4 zE-?L|n<0hHSo(U(dki{az@A3!KE*BN_hK)2?q&g8AHfUIOl{I*1U-daGJOG<+ObWf z2b!m}hBL9jD^U`2; zwu}(7_~Gh;1)`Q^phw9Z=').prop('disabled')); + + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default the complete document. + // Set to null to disable paste support: + pasteZone: $(document), + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = options.paramName[0]; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: options.paramName[index] || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(paramName, options.blob, file.name); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + options.paramName[index] || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).pipe( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.pipe(send); + } else { + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (limitSize && (!filesLength || files[0].size === undefined)) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options)) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + (batchSize + files[i + 1].size + overhead) > + limitSize) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; + }); + return result; + }, + + _replaceFileInput: function (input) { + var inputClone = input.clone(true); + $('
').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + dirReader; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + dirReader.readEntries(function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, errorHandler); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + 'dragover', + $.Event('dragover', {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + dataTransfer.dropEffect = 'copy'; + } + }, + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options; + // Initialize options set via HTML5 data-attributes: + $.each( + $(this.element[0].cloneNode(false)).data(), + function (key, value) { + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data).then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/UI/WebServerResources/jquery.iframe-transport.js b/UI/WebServerResources/jquery.iframe-transport.js new file mode 100755 index 000000000..6d476f2de --- /dev/null +++ b/UI/WebServerResources/jquery.iframe-transport.js @@ -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.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 = $( + '' + ).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): + $('') + .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) { + $('') + .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()); + } + } + }); + +})); diff --git a/UI/WebServerResources/upload_document.png b/UI/WebServerResources/upload_document.png new file mode 100644 index 0000000000000000000000000000000000000000..95bc045c23e3781bbef7cdbb14a46fb06f285221 GIT binary patch literal 1322 zcmeAS@N?(olHy`uVBq!ia0vp^DImx;bBqCEPEi zJ&1TElUs0Z=c?T4M|duYdCqPy4}Sh)(#9){Y=I{%7-MEzc~75l$4SMOQzrdLN`s?f zYme}O(v_Q+9GEg8a5`gKpW-C5px9&A54bM)^hVW)=^bmHzpM(!Br~sFOJ3PbvHJXf z;+Fb=8&-c+?x%WIHT~MXGIipkEs2>Oev`N~Q)*aZ<{P!EPU4)nrBUZG*CdXW8p&cm zR?c=QdlA#It4R$y*|P4>73u) z4x6X#yVxn<*>4zg%(_CdzHH)2t&6Q8Q$Ff___X45!wI1CyQUW=dv4ZAmb=&@V&oU7 z_tVI-!qVnU@+*ff*bjAbd3+`>K`OTMN^ zuH{r(vZzNnMbbG_*X7dT3Q6;+CZ~Q6dZ&pXc}G=LDPXaci#9IKvq4S!Gx{Z`+rsjwPB8gdd32sJ13gikp7r zcKiNnO_xhtp81cv_xhaqeLd)bX+l_#e*MAQ){2v^G0kVP_L{Yqp+@iJzPzB;#o%`3v0O1=vfgk8GJ_^YceQt{}bjN${!8t$%RKIdJfmoVSa z5a^W_iw64>_FZT?`#4S?$ z-&}sj*M7Ije%+m$4`(c$A)u5gWWjGUH{plppF-86H^L_X6RGZjNQI}{OH2!*f~H45 zZ(6d((Nr%}zCdKd2KjhCnF%J!n$JG2o)TiZBJAO6HJ{}C*rRI?NO~HkRXBSFHfPEH zt4RD7!w|t=qxo{>I`((F*si8MkV*KJvonGB!^R~7;hb8RLr*R?^sH{=e-ZxhA?Jey zOPnXoig91q*DYxa5vk}KXpZP(}kbS%yrp7 zkqz?yBZ5{NNpEejc>Zac0mJh7*L~V|A9vcTXP)uz9*@G@<9mQ*AcLo?pUXO@ GgeCyz8*Eho literal 0 HcmV?d00001