define('jira/attachment/inline-attach', ['jira/util/urls', 'jira/util/formatter', 'jira/lib/class', 'jira/xsrf', 'jira/util/navigator', 'jquery', 'wrm/context-path'], function (urls, formatter, Class, XSRF, Navigator, jQuery, wrmContextPath) { 'use strict'; var contextPath = wrmContextPath(); /** * Convert a file input element into an inline file upload control. * * If possible it will use the FileApi to perform the uploads. This allows * the user to see progress and cancel individual uploads mid way through. * * If the browser does not have the FileApi it will submit uploads in the background using a form. In this mode * the user will not see progress. * *
*
*
*
*
*
* new InlineAttach("input:file");
*
* or
*
* new InlineAttach(jQuery("#id"));
*
*
*
* @class InlineAttach
* @extends Class
*/
var InlineAttach = Class.extend({
/**
* Creates an inline file attach control
*
* @constructs
* @param {String | jQuery} element the file input to use for uploading.
*/
init: function init(element) {
var $element = jQuery(element);
if (InlineAttach.AjaxPresenter.isSupported($element)) {
new InlineAttach.AjaxPresenter($element);
} else {
new InlineAttach.FormPresenter($element);
}
}
});
jQuery.extend(InlineAttach, /** @lends InlineAttach */{
/**
* The maxium number of uploads that can occur concurrently.
* @static
*/
MAX_UPLOADS: 2,
/**
* The amount of time to wait to see if the upload finishes before displaying the progress. We do this so
* because the progress bar is not very useful for small files and infact may introduce a flicker as it is
* displayed and quickly removed.
* @static
*/
DISPLAY_WAIT: 600,
/**
* Wraps the passed function in such a way that ensures it always runs in the passed scope. If no scope is passed
* the the function is returned unmodified. If no function is passed, a no-operation function is returned.
*
* @param fn the function to wrap.
* @param scope the scope to run the function under.
* @return the wrapped function.
*/
rescope: function rescope(fn, scope) {
if (fn) {
if (scope) {
return jQuery.proxy(fn, scope);
} else {
return fn;
}
} else {
return jQuery.noop;
}
},
/**
* Copy the passed array-like object.
*
* @param array the array like object to copy.
*/
copyArrayLike: function copyArrayLike(array) {
return jQuery.makeArray(array);
},
/**
* Some global render helpers.
*/
Renderers: {
container: function container() {
return jQuery("");
}
}
});
/**
* A class to helper with the impleation of upload logic. It manages the queing of uploads to ensure that only a
* certain number of them are active at one time.
* @class InlineAttach.Presenter
* @extends Class
*/
InlineAttach.Presenter = Class.extend({
/**
* @constructs
*/
init: function init() {
/**
* Has the user cancelled the attach?
*/
this.cancelled = false;
/**
* The upload that are currently running.
*/
this.running = [];
/**
* The uploads that are currently waiting to run..
*/
this.waiting = [];
},
/**
* Add an upload. The upload will be started if there are not too many currently running uploads or otherwise
* it will be queued waiting for a call to _finishUpload.
*
* @param upload the upload to start or queue.
* @return true if there are currently running uploads, false otherwise.
*/
_addUpload: function _addUpload(upload) {
//Only start the current upload if we have not reached the concurrent upload limit.
if (!this.cancelled) {
if (this.running.length >= InlineAttach.MAX_UPLOADS) {
this.waiting.push(upload);
} else {
this.running.push(upload);
upload.upload();
}
}
return this.running.length > 0;
},
/**
* Call to indicate that the passed upload has finished running. If there are queued uploads one will be
* selected and started to replaced the one just finished.
*
* @param upload that has finished.
* @return true if there are currently running uploads, false otherwise.
*/
_finishUpload: function _finishUpload(upload) {
if (!this.cancelled) {
InlineAttach.Presenter.removeFromArray(this.waiting, upload);
if (InlineAttach.Presenter.removeFromArray(this.running, upload)) {
if (this.waiting.length > 0) {
var next = this.waiting.shift();
this.running.push(next);
next.upload();
}
}
}
return this.running.length > 0;
},
/**
* Called when the user 'cancels' all uploading. It aborts all running and queued uploads.
*/
_cancel: function _cancel() {
this.cancelled = true;
var i;
//Make a copy of waiting (in case an upload finishes while we are aborting) and abort them.
var wait = InlineAttach.copyArrayLike(this.waiting);
for (i = 0; i < wait.length; i++) {
wait[i].abort();
}
//Make a copy of running (in case the upload finishes while we are aborting) and abort them.
var run = InlineAttach.copyArrayLike(this.running);
for (i = 0; i < run.length; i++) {
run[i].abort();
}
this.waiting = [];
this.running = [];
}
});
jQuery.extend(InlineAttach.Presenter, /** @lends InlineAttach.Presenter */{
/**
* Removes the first occurance of the passed element from the passed array.
*
* @param array the array to process.
* @param element the element to remove.
* @return the element removed or null if no such element is found.
*/
removeFromArray: function removeFromArray(array, element) {
var index = jQuery.inArray(element, array);
if (index >= 0) {
return array.splice(index, 1);
} else {
return null;
}
}
});
/**
* The overall control logic when file uploads are to occur using form submission in the background.
* @class InlineAttach.FormPresenter
* @extends InlineAttach.Presenter
*/
InlineAttach.FormPresenter = InlineAttach.Presenter.extend({
/**
* @constructs
* @param $element the file input to used for uploading.
*/
init: function init($element) {
this._super();
/**
* The UI form where the user is requesting the upload.
*/
this.form = new InlineAttach.Form(new InlineAttach.FileInput($element, false));
this.form.fileSelector.onChange(jQuery.proxy(this._attach, this));
this.form.onCancel(jQuery.proxy(this._cancel, this));
},
/**
* Called when the file input changes to start uploading to the server.
*
* @param fileName in the file input.
*/
_attach: function _attach(fileName) {
this.form.clearErrors();
if (this.cancelled) {
return;
}
var form = this.form;
var data = this._createSubmitData();
//Add a new "File Input" to the form. We use the old input as part of a hidden form that we can submit to the
//server in the background.
var $oldInput = form.cloneFileInput();
form.fileSelector.clear();
var progress = form.addStaticProgress(fileName);
//We only show progress after we are sure the upload will take longer than InlineAttach.DISPLAY_WAIT.
var timer = new InlineAttach.Timer(function () {
!this.cancelled && progress.show();
}, this);
var upload = new InlineAttach.FormUpload({
$input: $oldInput,
url: InlineAttach.FormPresenter.DEFAULT_URL,
params: data,
scope: this,
before: function before() {
!this.cancelled && progress.start();
},
success: function success(val) {
if (this.cancelled) {
return;
}
if (val.id && val.name) {
form.addTemporaryFileCheckbox(val.id, val.name, progress);
} else if (val.errorMsg) {
form.addErrorWithFileName(val.errorMsg, fileName, progress);
} else {
form.addError(InlineAttach.Text.tr("upload.error.bad.response", fileName), progress);
}
},
error: function error(text) {
if (this.cancelled) {
return;
}
if (text.indexOf("SecurityTokenMissing") >= 0) {
form.addError(InlineAttach.Text.tr("upload.xsrf.timeout", fileName), progress);
} else {
form.addError(InlineAttach.Text.tr("upload.error.unknown", fileName), progress);
}
},
after: function after() {
timer.cancel();
progress.remove();
if (!this.cancelled && !this._finishUpload(upload)) {
form.enable();
}
}
});
progress.onCancel(function () {
upload.abort();
});
if (this._addUpload(upload)) {
timer.schedule(InlineAttach.DISPLAY_WAIT);
form.disable();
}
form.fileSelector.focus();
},
/**
* Called when the user 'cancels' the upload form, that is, when the user stops the upload.
*/
_cancel: function _cancel() {
this._super();
this.form.enable();
},
_createSubmitData: function _createSubmitData() {
var data = { atl_token: this.form.getAtlToken(), formToken: this.form.getFormToken() };
if (this.form.issueId) {
data.id = this.form.issueId;
} else if (this.form.projectId) {
data.create = true;
data.projectId = this.form.projectId;
} else {
throw "Unable to find either an issueId or projectId to submit the attachment to.";
}
return data;
}
});
/**
* Default location to add temporary attachments using multi-part request and forms.
* @static
* @memberof InlineAttach.FormPresenter
*/
InlineAttach.FormPresenter.DEFAULT_URL = contextPath + "/secure/AttachTemporaryFile.jspa?decorator=none";
/**
* The overall control logic when file uploads are to occur using direct AJAX and XHR.
* @class InlineAttach.AjaxPresenter
* @extends InlineAttach.Presenter
*/
InlineAttach.AjaxPresenter = InlineAttach.Presenter.extend({
/**
* @constructs
* @param $element the file input used for uploading.
*/
init: function init($element) {
this._super();
this.form = new InlineAttach.Form(new InlineAttach.FileInput($element, true));
this.form.fileSelector.onChange(jQuery.proxy(this._attach, this));
this.form.onCancel(jQuery.proxy(this._cancel, this));
},
/**
* Called to attach the passed File objects to the current issue.
*
* @param files the files to attach to the issue.
*/
_attach: function _attach(files) {
this.form.clearErrors();
if (this.cancelled) {
return;
}
if (files && files.length > 0) {
files = this._checkAndFilterFiles(files);
if (files) {
this._uploadFiles(files);
}
}
this.form.fileSelector.clear().focus();
},
/**
* Called to check the passed files to ensure they can be uploaded. Returns an array of files that can be
* uploaded. Null will be returned if no files can be uploaded.
*
* @param files the files that we want to filter.
* @return Returns an array of files that can be uploaded. Null will be returned if no files can be uploaded.
*/
_checkAndFilterFiles: function _checkAndFilterFiles(files) {
if (files.length > InlineAttach.AjaxPresenter.MAX_SELECTED_FILES) {
this.form.addError(InlineAttach.Text.tr("upload.error.too.many.files", files.length, InlineAttach.AjaxPresenter.MAX_SELECTED_FILES));
return null;
}
var maxSize = this.form.maxSize;
var newFiles = [];
for (var i = 0; i < files.length; i++) {
try {
var file = files[i];
if (file.size === 0) {
this.form.addError(InlineAttach.Text.tr("upload.empty.file", file.name));
} else if (maxSize > 0 && file.size > maxSize) {
//Note the order of this call is important. We want the size of the file to be based on max file
//(i.e. if maxSize is in MB than file.size should be in MB).
var sizes = InlineAttach.Text.fileSize(maxSize, file.size);
this.form.addError(InlineAttach.Text.tr("upload.too.big", file.name, sizes[1], sizes[0]));
} else {
//JRADEV-5679:
//Firefox throws exceptions on some I/O edge cases with its implementation of the FileAPI.
// For example, reading the File.size can throw an exception if the file no longer exists.
// So we don't have to add try...catch statements around everything we copy the attributes we
// need.
newFiles.push({ name: file.name, size: file.size, file: file });
}
} catch (e) {
this.form.addError(InlineAttach.AjaxUpload.getClientErrorMessage(e, file));
}
}
return newFiles.length === 0 ? null : newFiles;
},
/**
* Create and return the data to be submitted with the upload.
*
* @return the data to be submitted with the upload.
*/
_createSubmitData: function _createSubmitData() {
var data = { atl_token: this.form.getAtlToken(), formToken: this.form.getFormToken() };
if (this.form.issueId) {
data.issueId = this.form.issueId;
} else if (this.form.projectId) {
data.projectId = this.form.projectId;
} else {
throw "Unable to find either an issueId or projectId to submit the attachment to.";
}
return data;
},
/**
* Actually start uploading the files. The passed files have been checked and are valid.
*
* @param files the files to upload.
*/
_uploadFiles: function _uploadFiles(files) {
var form = this.form;
var data = this._createSubmitData();
var that = this;
var running = false;
jQuery.each(files, function () {
var _progress = form.addProgress(this);
var file = this;
//We only show progress after we are sure the upload will take longer than InlineAttach.DISPLAY_WAIT.
var timer = new InlineAttach.Timer(function () {
if (!that.cancelled) {
_progress.show();
}
});
var upload = new InlineAttach.AjaxUpload({
file: file.file,
params: jQuery.extend({ filename: file.name, size: file.size }, data),
scope: that,
url: InlineAttach.AjaxPresenter.DEFAULT_URL,
before: function before() {
!this.cancelled && _progress.start();
},
progress: function progress(val) {
!this.cancelled && _progress.update(val);
},
success: function success(val, status) {
if (this.cancelled) {
return;
}
if (status === 201) {
if (val.id !== undefined && val.name !== undefined) {
form.addTemporaryFileCheckbox(val.id, val.name, _progress, file.file);
} else {
form.addError(InlineAttach.Text.tr("upload.error.bad.response", file.name), _progress);
}
} else {
if (val.token) {
form.setAtlToken(val.token);
}
if (val.errorMessage) {
form.addErrorWithFileName(val.errorMessage, file.name, _progress);
} else {
form.addError(this._getErrorFromStatus(status, file), _progress);
}
}
},
error: function error(text, status) {
if (this.cancelled) {
return;
}
if (status < 0) {
//This is a client error so just render it.
form.addError(text, _progress);
} else {
var statusError = this._getErrorFromStatus(status, file);
if (statusError) {
form.addError(statusError, _progress);
} else {
form.addError(InlineAttach.Text.tr("upload.error.unknown", file.name), _progress);
}
}
},
after: function after() {
timer.cancel();
_progress.finish().remove();
if (!this.cancelled && !this._finishUpload(upload)) {
form.enable();
}
}
});
_progress.onCancel(function () {
upload.abort();
});
if (that._addUpload(upload)) {
running = true;
timer.schedule(InlineAttach.DISPLAY_WAIT);
}
});
//Disable the form if there are any running uploads. The last running upload will enable the form.
if (running) {
this.form.disable();
}
},
_getErrorFromStatus: function _getErrorFromStatus(status, file) {
var error;
if (status === 0) {
error = InlineAttach.Text.tr("upload.error.server.no.reply", file.name);
} else if (status === 400) {
error = InlineAttach.Text.tr("upload.error.badrequest", file.name);
} else if (status === 401) {
error = InlineAttach.Text.tr("upload.error.auth", file.name);
} else {
error = InlineAttach.Text.tr("upload.error.unknown.status", file.name, status);
}
return error;
},
/**
* Called when the user clicks cancel on the from they are using to attach files (i.e. the user does not want
* to attach any files).
*/
_cancel: function _cancel() {
this._super();
this.form.enable();
}
});
jQuery.extend(InlineAttach.AjaxPresenter, /** @lends InlineAttach.AjaxPresenter */{
/**
* The default location to attach temporary files using AJAX.
* @static
*/
DEFAULT_URL: contextPath + "/rest/internal/2/AttachTemporaryFile",
/**
* The number of files that can be attached at one time.
* @static
*/
MAX_SELECTED_FILES: 100,
/**
* Check to see if AJAX uploads are supported.
*
* @param $element the input element that will be used for attachments.
*/
isSupported: function isSupported($element) {
if (!$element || !$element[0] || !$element[0].files) {
return false;
} else {
return InlineAttach.AjaxUpload.isSupported();
}
}
});
/**
* Simple wrapper around a HTML file input.
* @class InlineAttach.FileInput
* @extends Class
*/
InlineAttach.FileInput = Class.extend({
/**
* @constructs
* @param $fileInput the file input to wrap.
* @param testMultiple tries to make the wrapped file input accept multiple files when set to true.
*/
init: function init($fileInput, testMultiple) {
this.$element = $fileInput;
this.$container = $fileInput.parent();
if (testMultiple && this.$element[0].files !== undefined) {
this.$element.attr("multiple", "multiple");
this.multiple = true;
} else {
this.multiple = false;
}
},
clear: function clear() {
this.$element.val('');
return this;
},
getFiles: function getFiles() {
return this.$element[0].files;
},
hasFiles: function hasFiles() {
return this.getFiles().length > 0;
},
/**
* Call the passed function when the wrapped file input changes. The callback will have "this" assigned to the
* FileInput and not the wrapped HTML element.
*
* @param callback the function to call when the file input changes. It will be run with the FileInput assigned
* to "this". If the FileApi is supported, the first argument will be the FileApi files from the HTML input.
* If the FileApi is *not* supported it will be the string value from the HTML input.
*/
onChange: function onChange(callback) {
var that = this;
this.$element.change(function () {
if (that.multiple) {
callback.call(that, this.files);
} else {
callback.call(that, that.getFileName());
}
});
return this;
},
focus: function focus() {
if (this._isIE()) {
var $e = this.$element;
//IE being the usual pain that it is wont focus unless there's this timeout.
setTimeout(function () {
$e.focus();
}, 0);
} else {
this.$element.focus();
}
return this;
},
/**
* Clone the current HTML input and replace it with a new one.
*
* @return the old file input.
*/
cloneInput: function cloneInput() {
var oldElement = this.$element;
oldElement.replaceWith(this.$element = oldElement.clone(true));
oldElement.unbind();
return oldElement;
},
/**
* Return the filename from the wrapped HTML element. We strip out some common garbage that some
* browsers add to the name.
*
* See: http://dev.w3.org/html5/spec/number-state.html#concept-input-type-file-selected
*
* @function
* @return the filename currently in the wrapped input element.
*/
getFileName: function () {
//Match the "c:\fakepath\" from the start of the string provided its not the entire string.
var fakepath = /^c:\\fakepath\\(?!$)/i;
return function () {
var fileName = this.$element.val();
//Remove "c:\fakepath\" from the string if there is stuff after it.
// SEE: http://dev.w3.org/html5/spec/number-state.html#concept-input-type-file-selected
fileName = fileName.replace(fakepath, "");
if (this._isIE() && fileName.indexOf("\\") >= 0) {
//IE returns an absolute path for the selected file, we however only want to display the
//filename.
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
}
return fileName;
};
}(),
_isIE: function _isIE() {
return Navigator.isIE() && Navigator.majorVersion() < 11;
},
before: function before(el) {
if (el) {
if (el.$element) {
el = el.$element;
}
this.$container.before(el);
}
}
});
(function () {
var options = { showPercentage: false, height: "2px" };
var count = 0;
/**
* Represents a simple progress bar.
* @class InlineAttach.ProgressBar
* @extends Class
*/
InlineAttach.ProgressBar = Class.extend({
/** @constructs */
init: function init() {
var $container = this.$element = this._renderers.container();
this.$progress = this._renderers.progress().appendTo($container);
this.$progress.progressBar(0, options);
this.hidden = true;
this.old = 0;
},
value: function value(_value) {
if (_value > 100) {
_value = 100;
} else if (_value < 0) {
_value = 0;
}
if (this.hidden) {
this.$progress.show();
this.hidden = false;
}
if (this.old !== _value) {
this.$progress.progressBar(_value, options);
if (_value >= 100) {
this.$progress.fadeOut();
}
this.old = _value;
}
},
_renderers: {
container: function container() {
return jQuery("