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.9KB

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