You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

attachments.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /**
  2. * Redmine - project management software
  3. * Copyright (C) 2006- Jean-Philippe Lang
  4. * This code is released under the GNU General Public License.
  5. */
  6. function addFile(inputEl, file, eagerUpload) {
  7. var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
  8. var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
  9. var maxFiles = ($(inputEl).attr('multiple') == 'multiple' ? 10 : 1);
  10. if (attachmentsFields.children().length < maxFiles) {
  11. var attachmentId = addFile.nextAttachmentId++;
  12. var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
  13. var param = $(inputEl).data('param');
  14. if (!param) {param = 'attachments'};
  15. fileSpan.append(
  16. $('<input>', { type: 'text', 'class': 'icon icon-attachment filename readonly', name: param +'[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
  17. $('<input>', { type: 'text', 'class': 'description', name: param + '[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
  18. $('<input>', { type: 'hidden', 'class': 'token', name: param + '[' + attachmentId + '][token]'} ),
  19. $('<a>&nbsp</a>').attr({ href: "#", 'class': 'icon-only icon-del remove-upload' }).click(removeFile).toggle(!eagerUpload)
  20. ).appendTo(attachmentsFields);
  21. if ($(inputEl).data('description') == 0) {
  22. fileSpan.find('input.description').remove();
  23. }
  24. if(eagerUpload) {
  25. ajaxUpload(file, attachmentId, fileSpan, inputEl);
  26. }
  27. addAttachment.toggle(attachmentsFields.children().length < maxFiles);
  28. return attachmentId;
  29. }
  30. return null;
  31. }
  32. addFile.nextAttachmentId = 1;
  33. function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
  34. function onLoadstart(e) {
  35. fileSpan.removeClass('ajax-waiting');
  36. fileSpan.addClass('ajax-loading');
  37. $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
  38. }
  39. function onProgress(e) {
  40. if(e.lengthComputable) {
  41. this.progressbar( 'value', e.loaded * 100 / e.total );
  42. }
  43. }
  44. function actualUpload(file, attachmentId, fileSpan, inputEl) {
  45. ajaxUpload.uploading++;
  46. uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
  47. loadstartEventHandler: onLoadstart.bind(progressSpan),
  48. progressEventHandler: onProgress.bind(progressSpan)
  49. })
  50. .done(function(result) {
  51. addInlineAttachmentMarkup(file);
  52. progressSpan.progressbar( 'value', 100 ).remove();
  53. fileSpan.find('input.description, a').css('display', 'inline-block');
  54. })
  55. .fail(function(result) {
  56. progressSpan.text(result.statusText);
  57. }).always(function() {
  58. ajaxUpload.uploading--;
  59. fileSpan.removeClass('ajax-loading');
  60. var form = fileSpan.parents('form');
  61. if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
  62. $('input:submit', form).removeAttr('disabled');
  63. }
  64. form.dequeue('upload');
  65. });
  66. }
  67. var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
  68. progressSpan.progressbar();
  69. fileSpan.addClass('ajax-waiting');
  70. var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
  71. if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
  72. actualUpload(file, attachmentId, fileSpan, inputEl);
  73. else
  74. $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
  75. }
  76. ajaxUpload.uploading = 0;
  77. function removeFile() {
  78. $(this).closest('.attachments_form').find('.add_attachment').show();
  79. $(this).parent('span').remove();
  80. return false;
  81. }
  82. function uploadBlob(blob, uploadUrl, attachmentId, options) {
  83. var actualOptions = $.extend({
  84. loadstartEventHandler: $.noop,
  85. progressEventHandler: $.noop
  86. }, options);
  87. uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
  88. if (blob instanceof window.Blob) {
  89. uploadUrl += '&filename=' + encodeURIComponent(blob.name);
  90. uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
  91. }
  92. return $.ajax(uploadUrl, {
  93. type: 'POST',
  94. contentType: 'application/octet-stream',
  95. beforeSend: function(jqXhr, settings) {
  96. jqXhr.setRequestHeader('Accept', 'application/js');
  97. // attach proper File object
  98. settings.data = blob;
  99. },
  100. xhr: function() {
  101. var xhr = $.ajaxSettings.xhr();
  102. xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
  103. xhr.upload.onprogress = actualOptions.progressEventHandler;
  104. return xhr;
  105. },
  106. data: blob,
  107. cache: false,
  108. processData: false
  109. });
  110. }
  111. function addInputFiles(inputEl) {
  112. var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
  113. var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
  114. var clearedFileInput = $(inputEl).clone().val('');
  115. var sizeExceeded = false;
  116. var param = $(inputEl).data('param');
  117. if (!param) {param = 'attachments'};
  118. if ($.ajaxSettings.xhr().upload && inputEl.files) {
  119. // upload files using ajax
  120. sizeExceeded = uploadAndAttachFiles(inputEl.files, inputEl);
  121. $(inputEl).remove();
  122. } else {
  123. // browser not supporting the file API, upload on form submission
  124. var attachmentId;
  125. var aFilename = inputEl.value.split(/\/|\\/);
  126. attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
  127. if (attachmentId) {
  128. $(inputEl).attr({ name: param + '[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
  129. }
  130. }
  131. clearedFileInput.prependTo(addAttachment);
  132. }
  133. function uploadAndAttachFiles(files, inputEl) {
  134. var maxFileSize = $(inputEl).data('max-file-size');
  135. var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
  136. var sizeExceeded = false;
  137. var filesLength = $(inputEl).closest('.attachments_form').find('.attachments_fields').children().length + files.length
  138. $.each(files, function() {
  139. if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
  140. });
  141. if (sizeExceeded) {
  142. window.alert(maxFileSizeExceeded);
  143. } else {
  144. $.each(files, function() {addFile(inputEl, this, true);});
  145. }
  146. if (filesLength > ($(inputEl).attr('multiple') == 'multiple' ? 10 : 1)) {
  147. window.alert($(inputEl).data('max-number-of-files-message'));
  148. }
  149. return sizeExceeded;
  150. }
  151. function handleFileDropEvent(e) {
  152. $(this).removeClass('fileover');
  153. blockEventPropagation(e);
  154. if ($.inArray('Files', e.dataTransfer.types) > -1) {
  155. handleFileDropEvent.target = e.target;
  156. if ($(this).hasClass('custom-field-filedroplistner')){
  157. uploadAndAttachFiles(e.dataTransfer.files, $(this).find('input:file.custom-field-filedrop').first());
  158. } else {
  159. uploadAndAttachFiles(e.dataTransfer.files, $(this).find('input:file.filedrop').first());
  160. }
  161. }
  162. }
  163. handleFileDropEvent.target = '';
  164. function dragOverHandler(e) {
  165. $(this).addClass('fileover');
  166. blockEventPropagation(e);
  167. e.dataTransfer.dropEffect = 'copy';
  168. }
  169. function dragOutHandler(e) {
  170. $(this).removeClass('fileover');
  171. blockEventPropagation(e);
  172. }
  173. function setupFileDrop() {
  174. if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
  175. $.event.addProp('dataTransfer');
  176. $('form div.box:not(.filedroplistner)').has('input:file.filedrop').each(function() {
  177. $(this).on({
  178. dragover: dragOverHandler,
  179. dragleave: dragOutHandler,
  180. drop: handleFileDropEvent,
  181. paste: copyImageFromClipboard
  182. }).addClass('filedroplistner');
  183. });
  184. $('form div.box input:file.custom-field-filedrop').closest('p').not('.custom-field-filedroplistner').each(function() {
  185. $(this).on({
  186. dragover: dragOverHandler,
  187. dragleave: dragOutHandler,
  188. drop: handleFileDropEvent
  189. }).addClass('custom-field-filedroplistner');
  190. });
  191. }
  192. }
  193. function addInlineAttachmentMarkup(file) {
  194. // insert uploaded image inline if dropped area is currently focused textarea
  195. if($(handleFileDropEvent.target).hasClass('wiki-edit') && $.inArray(file.type, window.wikiImageMimeTypes) > -1) {
  196. var $textarea = $(handleFileDropEvent.target);
  197. var cursorPosition = $textarea.prop('selectionStart');
  198. var description = $textarea.val();
  199. var sanitizedFilename = file.name.replace(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_');
  200. var inlineFilename = encodeURIComponent(sanitizedFilename)
  201. .replace(/[!()]/g, function(match) { return "%" + match.charCodeAt(0).toString(16) });
  202. var newLineBefore = true;
  203. var newLineAfter = true;
  204. if(cursorPosition === 0 || description.substr(cursorPosition-1,1).match(/\r|\n/)) {
  205. newLineBefore = false;
  206. }
  207. if(description.substr(cursorPosition,1).match(/\r|\n/)) {
  208. newLineAfter = false;
  209. }
  210. $textarea.val(
  211. description.substring(0, cursorPosition)
  212. + (newLineBefore ? '\n' : '')
  213. + inlineFilename
  214. + (newLineAfter ? '\n' : '')
  215. + description.substring(cursorPosition, description.length)
  216. );
  217. $textarea.prop({
  218. 'selectionStart': cursorPosition + newLineBefore,
  219. 'selectionEnd': cursorPosition + inlineFilename.length + newLineBefore
  220. });
  221. $textarea.parents('.jstBlock')
  222. .find('.jstb_img').click();
  223. // move cursor into next line
  224. cursorPosition = $textarea.prop('selectionStart');
  225. $textarea.prop({
  226. 'selectionStart': cursorPosition + 1,
  227. 'selectionEnd': cursorPosition + 1
  228. });
  229. }
  230. }
  231. function copyImageFromClipboard(e) {
  232. if (!$(e.target).hasClass('wiki-edit')) { return; }
  233. var clipboardData = e.clipboardData || e.originalEvent.clipboardData
  234. if (!clipboardData) { return; }
  235. if (clipboardData.types.some(function(t){ return /^text\/plain$/.test(t); })) { return; }
  236. var files = clipboardData.files
  237. for (var i = 0 ; i < files.length ; i++) {
  238. var file = files[i];
  239. if (file.type.indexOf("image") != -1) {
  240. var date = new Date();
  241. var filename = 'clipboard-'
  242. + date.getFullYear()
  243. + ('0'+(date.getMonth()+1)).slice(-2)
  244. + ('0'+date.getDate()).slice(-2)
  245. + ('0'+date.getHours()).slice(-2)
  246. + ('0'+date.getMinutes()).slice(-2)
  247. + '-' + randomKey(5).toLocaleLowerCase()
  248. + '.' + file.name.split('.').pop();
  249. // get input file in the closest form
  250. var inputEl = $(this).closest("form").find('input:file.filedrop');
  251. handleFileDropEvent.target = e.target;
  252. addFile(inputEl, new File([file], filename, { type: file.type }), true);
  253. }
  254. }
  255. }
  256. $(document).ready(setupFileDrop);
  257. $(document).ready(function(){
  258. $("input.deleted_attachment").change(function(){
  259. $(this).parents('.existing-attachment').toggleClass('deleted', $(this).is(":checked"));
  260. }).change();
  261. });