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 9.6KB

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