file.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import $ from 'jquery';
  2. import Dropzone from 'dropzone';
  3. import EXIF from 'exif-js';
  4. import { config, translations } from 'grav-form';
  5. // translations
  6. const Dictionary = {
  7. dictCancelUpload: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD,
  8. dictCancelUploadConfirmation: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
  9. dictDefaultMessage: translations.PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE,
  10. dictFallbackMessage: translations.PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE,
  11. dictFallbackText: translations.PLUGIN_FORM.DROPZONE_FALLBACK_TEXT,
  12. dictFileTooBig: translations.PLUGIN_FORM.DROPZONE_FILE_TOO_BIG,
  13. dictInvalidFileType: translations.PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE,
  14. dictMaxFilesExceeded: translations.PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED,
  15. dictRemoveFile: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE,
  16. dictRemoveFileConfirmation: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION,
  17. dictResponseError: translations.PLUGIN_FORM.DROPZONE_RESPONSE_ERROR
  18. };
  19. Dropzone.autoDiscover = false;
  20. const DropzoneMediaConfig = {
  21. createImageThumbnails: { thumbnailWidth: 150 },
  22. addRemoveLinks: false,
  23. dictDefaultMessage: Dictionary.dictDefaultMessage,
  24. dictRemoveFileConfirmation: Dictionary.dictRemoveFileConfirmation,
  25. previewTemplate: ''
  26. };
  27. window.EXIF = EXIF;
  28. export default class FilesField {
  29. constructor({ container = '.dropzone.files-upload', options = {} } = {}) {
  30. this.container = $(container);
  31. if (!this.container.length) { return; }
  32. this.urls = {};
  33. DropzoneMediaConfig.previewTemplate = $('#dropzone-template').html();
  34. this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
  35. klass: this,
  36. url: this.container.data('file-url-add') || config.current_url,
  37. acceptedFiles: this.container.data('media-types'),
  38. init: this.initDropzone
  39. }, this.container.data('dropzone-options'), options);
  40. this.dropzone = new Dropzone(container, this.options);
  41. this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
  42. this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
  43. this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
  44. this.dropzone.on('sending', this.onDropzoneSending.bind(this));
  45. this.dropzone.on('error', this.onDropzoneError.bind(this));
  46. }
  47. initDropzone() {
  48. let files = this.options.klass.container.find('[data-file]');
  49. let dropzone = this;
  50. if (!files.length) { return; }
  51. files.each((index, file) => {
  52. file = $(file);
  53. let data = file.data('file');
  54. let mock = {
  55. name: data.name,
  56. size: data.size,
  57. type: data.type,
  58. status: Dropzone.ADDED,
  59. accepted: true,
  60. url: this.options.url,
  61. removeUrl: data.remove
  62. };
  63. dropzone.files.push(mock);
  64. dropzone.options.addedfile.call(dropzone, mock);
  65. if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path);
  66. file.remove();
  67. });
  68. }
  69. getURI() {
  70. return this.container.data('mediaUri') || '';
  71. }
  72. onDropzoneSending(file, xhr, formData) {
  73. formData.append('__form-name__', this.container.closest('form').find('[name="__form-name__"]').val());
  74. formData.append('__form-file-uploader__', 1);
  75. formData.append('name', this.options.dotNotation);
  76. formData.append('form-nonce', config.form_nonce);
  77. formData.append('task', 'filesupload');
  78. formData.append('uri', this.getURI());
  79. }
  80. onDropzoneSuccess(file, response, xhr) {
  81. if (this.options.reloadPage) {
  82. global.location.reload();
  83. }
  84. // store params for removing file from session before it gets saved
  85. if (response.session) {
  86. file.sessionParams = response.session;
  87. file.removeUrl = this.options.url;
  88. // Touch field value to force a mutation detection
  89. const input = this.container.find('[name][type="hidden"]');
  90. const value = input.val();
  91. input.val(value + ' ');
  92. }
  93. return this.handleError({
  94. file,
  95. data: response,
  96. mode: 'removeFile',
  97. msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
  98. <pre>${response.message}</pre>`
  99. });
  100. }
  101. onDropzoneComplete(file) {
  102. if (!file.accepted && !file.rejected) {
  103. let data = {
  104. status: 'error',
  105. message: `${translations.PLUGIN_FORM.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
  106. };
  107. return this.handleError({
  108. file,
  109. data,
  110. mode: 'removeFile',
  111. msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
  112. <pre>${data.message}</pre>`
  113. });
  114. }
  115. if (this.options.reloadPage) {
  116. global.location.reload();
  117. }
  118. }
  119. onDropzoneRemovedFile(file, ...extra) {
  120. if (!file.accepted || file.rejected) { return; }
  121. let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
  122. let path = (url || '').match(/path:(.*)\//);
  123. let data = new FormData();
  124. data.append('filename', file.name);
  125. data.append('__form-name__', this.container.closest('form').find('[name="__form-name__"]').val());
  126. data.append('name', this.options.dotNotation);
  127. data.append('form-nonce', config.form_nonce);
  128. data.append('uri', this.getURI());
  129. if (file.sessionParams) {
  130. data.append('__form-file-remover__', '1');
  131. data.append('session', file.sessionParams);
  132. }
  133. $.ajax({
  134. url,
  135. data,
  136. method: 'POST',
  137. contentType: false,
  138. processData: false,
  139. success: () => {
  140. if (!path) { return; }
  141. path = global.atob(path[1]);
  142. let input = this.container.find('[name][type="hidden"]');
  143. let data = JSON.parse(input.val() || '{}');
  144. delete data[path];
  145. input.val(JSON.stringify(data));
  146. }
  147. });
  148. }
  149. onDropzoneError(file, response, xhr) {
  150. let message = xhr && response.error ? response.error.message : response;
  151. $(file.previewElement).find('[data-dz-errormessage]').html(message);
  152. return this.handleError({
  153. file,
  154. data: { status: 'error' },
  155. msg: `<pre>${message}</pre>`
  156. });
  157. }
  158. handleError(options) {
  159. return true;
  160. /* let { file, data, mode, msg } = options;
  161. if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
  162. switch (mode) {
  163. case 'addBack':
  164. if (file instanceof File) {
  165. this.dropzone.addFile.call(this.dropzone, file);
  166. } else {
  167. this.dropzone.files.push(file);
  168. this.dropzone.options.addedfile.call(this.dropzone, file);
  169. this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
  170. }
  171. break;
  172. case 'removeFile':
  173. default:
  174. if (~this.dropzone.files.indexOf(file)) {
  175. file.rejected = true;
  176. this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
  177. }
  178. break;
  179. }
  180. let modal = $('[data-remodal-id="generic"]');
  181. modal.find('.error-content').html(msg);
  182. $.remodal.lookup[modal.data('remodal')].open(); */
  183. }
  184. }
  185. export function UriToMarkdown(uri) {
  186. uri = uri.replace(/@3x|@2x|@1x/, '');
  187. uri = uri.replace(/\(/g, '%28');
  188. uri = uri.replace(/\)/g, '%29');
  189. return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
  190. }
  191. let instances = [];
  192. let cache = $();
  193. const onAddedNodes = (event, target/* , record, instance */) => {
  194. let files = $(target).find('.dropzone.files-upload');
  195. if (!files.length) { return; }
  196. files.each((index, file) => {
  197. file = $(file);
  198. if (!~cache.index(file)) {
  199. addNode(file);
  200. }
  201. });
  202. };
  203. const addNode = (container) => {
  204. container = $(container);
  205. let input = container.find('input[type="file"]');
  206. let settings = container.data('grav-file-settings') || {};
  207. if (settings.accept && ~settings.accept.indexOf('*')) {
  208. settings.accept = [''];
  209. }
  210. let options = {
  211. url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
  212. paramName: settings.paramName || 'file',
  213. dotNotation: settings.name || 'file',
  214. acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
  215. maxFilesize: settings.filesize || 256,
  216. maxFiles: settings.limit || null,
  217. resizeWidth: settings.resizeWidth || null,
  218. resizeHeight: settings.resizeHeight || null,
  219. resizeQuality: settings.resizeQuality || null,
  220. accept: function(file, done) {
  221. const resolution = settings.resolution;
  222. if (!resolution) return done();
  223. setTimeout(() => {
  224. let error = '';
  225. if (resolution.min) {
  226. Object.keys(resolution.min).forEach((attr) => {
  227. if (file[attr] < resolution.min[attr]) {
  228. error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
  229. }
  230. });
  231. }
  232. if (!(settings.resizeWidth || settings.resizeHeight)) {
  233. if (resolution.max) {
  234. Object.keys(resolution.max).forEach((attr) => {
  235. if (file[attr] > resolution.max[attr]) {
  236. error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
  237. }
  238. });
  239. }
  240. }
  241. return done(error);
  242. }, 50);
  243. }
  244. };
  245. cache = cache.add(container);
  246. container = container[0];
  247. instances.push(new FilesField({ container, options }));
  248. };
  249. export let Instances = (() => {
  250. $(document).ready(() => {
  251. $('.dropzone.files-upload').each((i, container) => addNode(container));
  252. $('body').on('mutation._grav', onAddedNodes);
  253. });
  254. return instances;
  255. })();