files.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import $ from 'jquery';
  2. import Dropzone from 'dropzone';
  3. import EXIF from 'exif-js';
  4. import request from '../../utils/request';
  5. import { config, translations } from 'grav-config';
  6. // translations
  7. const Dictionary = {
  8. dictCancelUpload: translations.PLUGIN_ADMIN.DROPZONE_CANCEL_UPLOAD,
  9. dictCancelUploadConfirmation: translations.PLUGIN_ADMIN.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
  10. dictDefaultMessage: translations.PLUGIN_ADMIN.DROPZONE_DEFAULT_MESSAGE,
  11. dictFallbackMessage: translations.PLUGIN_ADMIN.DROPZONE_FALLBACK_MESSAGE,
  12. dictFallbackText: translations.PLUGIN_ADMIN.DROPZONE_FALLBACK_TEXT,
  13. dictFileTooBig: translations.PLUGIN_ADMIN.DROPZONE_FILE_TOO_BIG,
  14. dictInvalidFileType: translations.PLUGIN_ADMIN.DROPZONE_INVALID_FILE_TYPE,
  15. dictMaxFilesExceeded: translations.PLUGIN_ADMIN.DROPZONE_MAX_FILES_EXCEEDED,
  16. dictRemoveFile: translations.PLUGIN_ADMIN.DROPZONE_REMOVE_FILE,
  17. dictResponseError: translations.PLUGIN_ADMIN.DROPZONE_RESPONSE_ERROR
  18. };
  19. Dropzone.autoDiscover = false;
  20. Dropzone.options.gravPageDropzone = {};
  21. Dropzone.confirm = (question, accepted, rejected) => {
  22. let doc = $(document);
  23. let modalSelector = '[data-remodal-id="delete-media"]';
  24. let removeEvents = () => {
  25. doc.off('confirmation', modalSelector, accept);
  26. doc.off('cancellation', modalSelector, reject);
  27. $(modalSelector).find('.remodal-confirm').removeClass('pointer-events-disabled');
  28. };
  29. let accept = () => {
  30. accepted && accepted();
  31. removeEvents();
  32. };
  33. let reject = () => {
  34. rejected && rejected();
  35. removeEvents();
  36. };
  37. $.remodal.lookup[$(modalSelector).data('remodal')].open();
  38. doc.on('confirmation', modalSelector, accept);
  39. doc.on('cancellation', modalSelector, reject);
  40. };
  41. const DropzoneMediaConfig = {
  42. timeout: 0,
  43. thumbnailWidth: 200,
  44. thumbnailHeight: 150,
  45. addRemoveLinks: false,
  46. dictDefaultMessage: translations.PLUGIN_ADMIN.DROP_FILES_HERE_TO_UPLOAD.replace(/&lt;/g, '<').replace(/&gt;/g, '>'),
  47. dictRemoveFileConfirmation: '[placeholder]',
  48. previewTemplate: `
  49. <div class="dz-preview dz-file-preview dz-no-editor">
  50. <div class="dz-details">
  51. <div class="dz-filename"><span data-dz-name></span></div>
  52. <div class="dz-size" data-dz-size></div>
  53. <img data-dz-thumbnail />
  54. </div>
  55. <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
  56. <div class="dz-success-mark"><span>✔</span></div>
  57. <div class="dz-error-mark"><span>✘</span></div>
  58. <div class="dz-error-message"><span data-dz-errormessage></span></div>
  59. <a class="dz-unset" title="${translations.PLUGIN_ADMIN.UNSET}" href="#" data-dz-unset>${translations.PLUGIN_ADMIN.UNSET}</a>
  60. <a class="dz-remove" title="${translations.PLUGIN_ADMIN.DELETE}" href="javascript:undefined;" data-dz-remove>${translations.PLUGIN_ADMIN.DELETE}</a>
  61. <a class="dz-metadata" title="${translations.PLUGIN_ADMIN.METADATA}" href="#" target="_blank" data-dz-metadata>${translations.PLUGIN_ADMIN.METADATA}</a>
  62. <a class="dz-view" title="${translations.PLUGIN_ADMIN.VIEW}" href="#" target="_blank" data-dz-view>${translations.PLUGIN_ADMIN.VIEW}</a>
  63. </div>`.trim()
  64. };
  65. global.EXIF = EXIF;
  66. const ACCEPT_FUNC = function(file, done, settings) {
  67. const resolution = settings.resolution;
  68. if (!resolution) return done();
  69. const reader = new FileReader();
  70. let error = '';
  71. const hasMin = (resolution.min && (resolution.min.width || resolution.min.height));
  72. const hasMax = (resolution.max && (resolution.max.width || resolution.max.height));
  73. if (hasMin || (!(settings.resizeWidth || settings.resizeHeight) && hasMax)) {
  74. reader.onload = function(event) {
  75. const image = new Image();
  76. image.src = event.target.result;
  77. image.onload = function() {
  78. if (resolution.min) {
  79. Object.keys(resolution.min).forEach((attr) => {
  80. if (resolution.min[attr] && this[attr] < resolution.min[attr]) {
  81. error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
  82. }
  83. });
  84. }
  85. if (!(settings.resizeWidth || settings.resizeHeight)) {
  86. if (resolution.max) {
  87. Object.keys(resolution.max).forEach((attr) => {
  88. if (resolution.max[attr] && this[attr] > resolution.max[attr]) {
  89. error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
  90. }
  91. });
  92. }
  93. }
  94. return error ? done(error) : done();
  95. };
  96. };
  97. reader.readAsDataURL(file);
  98. } else {
  99. return error ? done(error) : done();
  100. }
  101. };
  102. export default class FilesField {
  103. constructor({ container = '.dropzone.files-upload', options = {} } = {}) {
  104. this.container = $(container);
  105. if (!this.container.length) { return; }
  106. this.urls = {};
  107. this.customPost = this.container.data('filePostAdd') || {};
  108. this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
  109. klass: this,
  110. url: this.container.data('file-url-add') || config.current_url,
  111. acceptedFiles: this.container.data('media-types'),
  112. init: this.initDropzone
  113. }, this.container.data('dropzone-options'), options);
  114. this.options = Object.assign({}, this.options, {
  115. accept: function(file, done) { ACCEPT_FUNC(file, done, this.options); }
  116. });
  117. this.dropzone = new Dropzone(container, this.options);
  118. this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
  119. this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
  120. this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
  121. this.dropzone.on('sending', this.onDropzoneSending.bind(this));
  122. this.dropzone.on('error', this.onDropzoneError.bind(this));
  123. this.container.on('mouseenter', '[data-dz-view]', (e) => {
  124. const value = JSON.parse(this.container.find('[name][type="hidden"]').val() || '{}');
  125. const target = $(e.currentTarget);
  126. const file = target.parent('.dz-preview').find('.dz-filename');
  127. const filename = encodeURI(file.text());
  128. const URL = Object.keys(value).filter((key) => value[key].name === filename).shift();
  129. target.attr('href', `${config.base_url_simple}/${URL}`);
  130. });
  131. }
  132. initDropzone() {
  133. let files = this.options.klass.container.find('[data-file]');
  134. let dropzone = this;
  135. if (!files.length) { return; }
  136. files.each((index, file) => {
  137. file = $(file);
  138. let data = file.data('file');
  139. let mock = {
  140. name: data.name,
  141. size: data.size,
  142. type: data.type,
  143. status: Dropzone.ADDED,
  144. accepted: true,
  145. url: this.options.url,
  146. removeUrl: data.remove
  147. };
  148. dropzone.files.push(mock);
  149. dropzone.options.addedfile.call(dropzone, mock);
  150. if (mock.type.match(/^image\//)) {
  151. dropzone.options.thumbnail.call(dropzone, mock, data.path);
  152. dropzone.createThumbnailFromUrl(mock, data.path);
  153. }
  154. file.remove();
  155. });
  156. }
  157. getURI() {
  158. return this.container.data('mediaUri') || '';
  159. }
  160. onDropzoneSending(file, xhr, formData) {
  161. if (Object.keys(this.customPost).length) {
  162. Object.keys(this.customPost).forEach((key) => {
  163. formData.append(key, this.customPost[key]);
  164. });
  165. } else {
  166. formData.append('name', this.options.dotNotation);
  167. formData.append('task', 'filesupload');
  168. formData.append('uri', this.getURI());
  169. }
  170. formData.append('admin-nonce', config.admin_nonce);
  171. }
  172. onDropzoneSuccess(file, response, xhr) {
  173. response = typeof response === 'string' ? JSON.parse(response) : response;
  174. if (this.options.reloadPage) {
  175. global.location.reload();
  176. }
  177. // store params for removing file from session before it gets saved
  178. if (response.session) {
  179. file.sessionParams = response.session;
  180. file.removeUrl = this.options.url;
  181. // Touch field value to force a mutation detection
  182. const input = this.container.find('[name][type="hidden"]');
  183. const value = input.val();
  184. input.val(value + ' ');
  185. }
  186. return this.handleError({
  187. file,
  188. data: response,
  189. mode: 'removeFile',
  190. msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
  191. <pre>${response.message}</pre>`
  192. });
  193. }
  194. onDropzoneComplete(file) {
  195. if (!file.accepted && !file.rejected) {
  196. let data = {
  197. status: 'error',
  198. message: `${translations.PLUGIN_ADMIN.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
  199. };
  200. return this.handleError({
  201. file,
  202. data,
  203. mode: 'removeFile',
  204. msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
  205. <pre>${data.message}</pre>`
  206. });
  207. }
  208. if (this.options.reloadPage) {
  209. global.location.reload();
  210. }
  211. }
  212. b64_to_utf8(str) {
  213. str = str.replace(/\s/g, '');
  214. return decodeURIComponent(escape(window.atob(str)));
  215. }
  216. onDropzoneRemovedFile(file, ...extra) {
  217. if (!file.accepted || file.rejected) { return; }
  218. let url = file.removeUrl || this.urls.delete || this.options.url;
  219. let path = (url || '').match(/path:(.*)\//);
  220. let body = { filename: file.name, uri: this.getURI() };
  221. if (file.sessionParams) {
  222. body.task = 'filessessionremove';
  223. body.session = file.sessionParams;
  224. }
  225. const customPost = this.container.data('filePostRemove') || {};
  226. if (Object.keys(customPost).length) {
  227. body = {};
  228. Object.keys(customPost).forEach((key) => {
  229. body[key] = customPost[key];
  230. });
  231. }
  232. body['filename'] = file.name;
  233. body['admin-nonce'] = config.admin_nonce;
  234. request(url, { method: 'post', body }, () => {
  235. if (!path) { return; }
  236. path = this.b64_to_utf8(path[1]);
  237. let input = this.container.find('[name][type="hidden"]');
  238. let data = JSON.parse(input.val() || '{}');
  239. delete data[path];
  240. input.val(JSON.stringify(data));
  241. });
  242. }
  243. onDropzoneError(file, response, xhr) {
  244. let message = xhr ? response.error.message : response;
  245. $(file.previewElement).find('[data-dz-errormessage]').html(message);
  246. return this.handleError({
  247. file,
  248. data: { status: 'error' },
  249. msg: `<pre>${message}</pre>`
  250. });
  251. }
  252. handleError(options) {
  253. let { file, data, mode, msg } = options;
  254. if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
  255. switch (mode) {
  256. case 'addBack':
  257. if (file instanceof File) {
  258. this.dropzone.addFile.call(this.dropzone, file);
  259. } else {
  260. this.dropzone.files.push(file);
  261. this.dropzone.options.addedfile.call(this.dropzone, file);
  262. this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
  263. }
  264. break;
  265. case 'removeFile':
  266. default:
  267. if (~this.dropzone.files.indexOf(file)) {
  268. file.rejected = true;
  269. this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
  270. }
  271. break;
  272. }
  273. let modal = $('[data-remodal-id="generic"]');
  274. modal.find('.error-content').html(msg);
  275. $.remodal.lookup[modal.data('remodal')].open();
  276. }
  277. }
  278. export function UriToMarkdown(uri) {
  279. uri = uri.replace(/@3x|@2x|@1x/, '');
  280. uri = uri.replace(/\(/g, '%28');
  281. uri = uri.replace(/\)/g, '%29');
  282. return uri.match(/\.(jpe?g|png|gif|svg|mp4|webm|ogv|mov)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
  283. }
  284. let instances = [];
  285. let cache = $();
  286. const onAddedNodes = (event, target/* , record, instance */) => {
  287. let files = $(target).find('.dropzone.files-upload');
  288. if (!files.length) { return; }
  289. files.each((index, file) => {
  290. file = $(file);
  291. if (!~cache.index(file)) {
  292. addNode(file);
  293. }
  294. });
  295. };
  296. const addNode = (container) => {
  297. container = $(container);
  298. let input = container.find('input[type="file"]');
  299. let settings = container.data('grav-file-settings') || {};
  300. if (settings.accept && ~settings.accept.indexOf('*')) {
  301. settings.accept = [''];
  302. }
  303. let options = {
  304. url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
  305. paramName: settings.paramName || 'file',
  306. dotNotation: settings.name || 'file',
  307. acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
  308. maxFilesize: typeof settings.filesize !== 'undefined' ? settings.filesize : 256,
  309. maxFiles: settings.limit || null,
  310. resizeWidth: settings.resizeWidth || null,
  311. resizeHeight: settings.resizeHeight || null,
  312. resizeQuality: settings.resizeQuality || null,
  313. accept: function(file, done) { ACCEPT_FUNC(file, done, settings); }
  314. };
  315. cache = cache.add(container);
  316. container = container[0];
  317. instances.push(new FilesField({ container, options }));
  318. };
  319. export let Instances = (() => {
  320. $('.dropzone.files-upload').each((i, container) => addNode(container));
  321. $('body').on('mutation._grav', onAddedNodes);
  322. return instances;
  323. })();