file.js 12 KB

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