inline-attachment.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /*! inline-attachment - v2.0.3 - 2016-08-20 */
  2. /*jslint newcap: true */
  3. /*global XMLHttpRequest: false, FormData: false */
  4. /*
  5. * Inline Text Attachment
  6. *
  7. * Author: Roy van Kaathoven
  8. * Contact: ik@royvankaathoven.nl
  9. */
  10. (function(document, window) {
  11. 'use strict';
  12. var inlineAttachment = function(options, instance) {
  13. this.settings = inlineAttachment.util.merge(options, inlineAttachment.defaults);
  14. this.editor = instance;
  15. this.filenameTag = '{filename}';
  16. this.lastValue = null;
  17. };
  18. /**
  19. * Will holds the available editors
  20. *
  21. * @type {Object}
  22. */
  23. inlineAttachment.editors = {};
  24. /**
  25. * Utility functions
  26. */
  27. inlineAttachment.util = {
  28. /**
  29. * Simple function to merge the given objects
  30. *
  31. * @param {Object[]} object Multiple object parameters
  32. * @returns {Object}
  33. */
  34. merge: function() {
  35. var result = {};
  36. for (var i = arguments.length - 1; i >= 0; i--) {
  37. var obj = arguments[i];
  38. for (var k in obj) {
  39. if (obj.hasOwnProperty(k)) {
  40. result[k] = obj[k];
  41. }
  42. }
  43. }
  44. return result;
  45. },
  46. /**
  47. * Append a line of text at the bottom, ensuring there aren't unnecessary newlines
  48. *
  49. * @param {String} appended Current content
  50. * @param {String} previous Value which should be appended after the current content
  51. */
  52. appendInItsOwnLine: function(previous, appended) {
  53. return (previous + "\n\n[[D]]" + appended)
  54. .replace(/(\n{2,})\[\[D\]\]/, "\n\n")
  55. .replace(/^(\n*)/, "");
  56. },
  57. /**
  58. * Inserts the given value at the current cursor position of the textarea element
  59. *
  60. * @param {HtmlElement} el
  61. * @param {String} value Text which will be inserted at the cursor position
  62. */
  63. insertTextAtCursor: function(el, text) {
  64. var scrollPos = el.scrollTop,
  65. strPos = 0,
  66. browser = false,
  67. range;
  68. if ((el.selectionStart || el.selectionStart === '0')) {
  69. browser = "ff";
  70. } else if (document.selection) {
  71. browser = "ie";
  72. }
  73. if (browser === "ie") {
  74. el.focus();
  75. range = document.selection.createRange();
  76. range.moveStart('character', -el.value.length);
  77. strPos = range.text.length;
  78. } else if (browser === "ff") {
  79. strPos = el.selectionStart;
  80. }
  81. var front = (el.value).substring(0, strPos);
  82. var back = (el.value).substring(strPos, el.value.length);
  83. el.value = front + text + back;
  84. strPos = strPos + text.length;
  85. if (browser === "ie") {
  86. el.focus();
  87. range = document.selection.createRange();
  88. range.moveStart('character', -el.value.length);
  89. range.moveStart('character', strPos);
  90. range.moveEnd('character', 0);
  91. range.select();
  92. } else if (browser === "ff") {
  93. el.selectionStart = strPos;
  94. el.selectionEnd = strPos;
  95. el.focus();
  96. }
  97. el.scrollTop = scrollPos;
  98. }
  99. };
  100. /**
  101. * Default configuration options
  102. *
  103. * @type {Object}
  104. */
  105. inlineAttachment.defaults = {
  106. /**
  107. * URL where the file will be send
  108. */
  109. uploadUrl: 'upload_attachment.php',
  110. /**
  111. * Which method will be used to send the file to the upload URL
  112. */
  113. uploadMethod: 'POST',
  114. /**
  115. * Name in which the file will be placed
  116. */
  117. uploadFieldName: 'file',
  118. /**
  119. * Extension which will be used when a file extension could not
  120. * be detected
  121. */
  122. defaultExtension: 'png',
  123. /**
  124. * JSON field which refers to the uploaded file URL
  125. */
  126. jsonFieldName: 'filename',
  127. /**
  128. * Allowed MIME types
  129. */
  130. allowedTypes: [
  131. 'image/jpeg',
  132. 'image/png',
  133. 'image/jpg',
  134. 'image/gif'
  135. ],
  136. /**
  137. * Text which will be inserted when dropping or pasting a file.
  138. * Acts as a placeholder which will be replaced when the file is done with uploading
  139. */
  140. progressText: '![Uploading file...]()',
  141. /**
  142. * When a file has successfully been uploaded the progressText
  143. * will be replaced by the urlText, the {filename} tag will be replaced
  144. * by the filename that has been returned by the server
  145. */
  146. urlText: "![file]({filename})",
  147. /**
  148. * Text which will be used when uploading has failed
  149. */
  150. errorText: "Error uploading file",
  151. /**
  152. * Extra parameters which will be send when uploading a file
  153. */
  154. extraParams: {},
  155. /**
  156. * Extra headers which will be send when uploading a file
  157. */
  158. extraHeaders: {},
  159. /**
  160. * Before the file is send
  161. */
  162. beforeFileUpload: function() {
  163. return true;
  164. },
  165. /**
  166. * Triggers when a file is dropped or pasted
  167. */
  168. onFileReceived: function() {},
  169. /**
  170. * Custom upload handler
  171. *
  172. * @return {Boolean} when false is returned it will prevent default upload behavior
  173. */
  174. onFileUploadResponse: function() {
  175. return true;
  176. },
  177. /**
  178. * Custom error handler. Runs after removing the placeholder text and before the alert().
  179. * Return false from this function to prevent the alert dialog.
  180. *
  181. * @return {Boolean} when false is returned it will prevent default error behavior
  182. */
  183. onFileUploadError: function() {
  184. return true;
  185. },
  186. /**
  187. * When a file has succesfully been uploaded
  188. */
  189. onFileUploaded: function() {}
  190. };
  191. /**
  192. * Uploads the blob
  193. *
  194. * @param {Blob} file blob data received from event.dataTransfer object
  195. * @return {XMLHttpRequest} request object which sends the file
  196. */
  197. inlineAttachment.prototype.uploadFile = function(file) {
  198. var me = this,
  199. formData = new FormData(),
  200. xhr = new XMLHttpRequest(),
  201. settings = this.settings,
  202. extension = settings.defaultExtension || settings.defualtExtension;
  203. if (typeof settings.setupFormData === 'function') {
  204. settings.setupFormData(formData, file);
  205. }
  206. // Attach the file. If coming from clipboard, add a default filename (only works in Chrome for now)
  207. // http://stackoverflow.com/questions/6664967/how-to-give-a-blob-uploaded-as-formdata-a-file-name
  208. if (file.name) {
  209. var fileNameMatches = file.name.match(/\.(.+)$/);
  210. if (fileNameMatches) {
  211. extension = fileNameMatches[1];
  212. }
  213. }
  214. var remoteFilename = "image-" + Date.now() + "." + extension;
  215. if (typeof settings.remoteFilename === 'function') {
  216. remoteFilename = settings.remoteFilename(file);
  217. }
  218. formData.append(settings.uploadFieldName, file, remoteFilename);
  219. // Append the extra parameters to the formdata
  220. if (typeof settings.extraParams === "object") {
  221. for (var key in settings.extraParams) {
  222. if (settings.extraParams.hasOwnProperty(key)) {
  223. formData.append(key, settings.extraParams[key]);
  224. }
  225. }
  226. }
  227. xhr.open('POST', settings.uploadUrl);
  228. // Add any available extra headers
  229. if (typeof settings.extraHeaders === "object") {
  230. for (var header in settings.extraHeaders) {
  231. if (settings.extraHeaders.hasOwnProperty(header)) {
  232. xhr.setRequestHeader(header, settings.extraHeaders[header]);
  233. }
  234. }
  235. }
  236. xhr.onload = function() {
  237. // If HTTP status is OK or Created
  238. if (xhr.status === 200 || xhr.status === 201) {
  239. me.onFileUploadResponse(xhr);
  240. } else {
  241. me.onFileUploadError(xhr);
  242. }
  243. };
  244. if (settings.beforeFileUpload(xhr) !== false) {
  245. xhr.send(formData);
  246. }
  247. return xhr;
  248. };
  249. /**
  250. * Returns if the given file is allowed to handle
  251. *
  252. * @param {File} clipboard data file
  253. */
  254. inlineAttachment.prototype.isFileAllowed = function(file) {
  255. if (file.kind === 'string') { return false; }
  256. if (this.settings.allowedTypes.indexOf('*') === 0){
  257. return true;
  258. } else {
  259. return this.settings.allowedTypes.indexOf(file.type) >= 0;
  260. }
  261. };
  262. /**
  263. * Handles upload response
  264. *
  265. * @param {XMLHttpRequest} xhr
  266. * @return {Void}
  267. */
  268. inlineAttachment.prototype.onFileUploadResponse = function(xhr) {
  269. if (this.settings.onFileUploadResponse.call(this, xhr) !== false) {
  270. var result = JSON.parse(xhr.responseText),
  271. filename = result[this.settings.jsonFieldName];
  272. if (result && filename) {
  273. var newValue;
  274. if (typeof this.settings.urlText === 'function') {
  275. newValue = this.settings.urlText.call(this, filename, result);
  276. } else {
  277. newValue = this.settings.urlText.replace(this.filenameTag, filename);
  278. }
  279. var text = this.editor.getValue().replace(this.lastValue, newValue);
  280. this.editor.setValue(text);
  281. this.settings.onFileUploaded.call(this, filename);
  282. }
  283. }
  284. };
  285. /**
  286. * Called when a file has failed to upload
  287. *
  288. * @param {XMLHttpRequest} xhr
  289. * @return {Void}
  290. */
  291. inlineAttachment.prototype.onFileUploadError = function(xhr) {
  292. if (this.settings.onFileUploadError.call(this, xhr) !== false) {
  293. var text = this.editor.getValue().replace(this.lastValue, "");
  294. this.editor.setValue(text);
  295. }
  296. };
  297. /**
  298. * Called when a file has been inserted, either by drop or paste
  299. *
  300. * @param {File} file
  301. * @return {Void}
  302. */
  303. inlineAttachment.prototype.onFileInserted = function(file) {
  304. if (this.settings.onFileReceived.call(this, file) !== false) {
  305. this.lastValue = this.settings.progressText;
  306. this.editor.insertValue(this.lastValue);
  307. }
  308. };
  309. /**
  310. * Called when a paste event occured
  311. * @param {Event} e
  312. * @return {Boolean} if the event was handled
  313. */
  314. inlineAttachment.prototype.onPaste = function(e) {
  315. var result = false,
  316. clipboardData = e.clipboardData,
  317. items;
  318. if (typeof clipboardData === "object") {
  319. items = clipboardData.items || clipboardData.files || [];
  320. for (var i = 0; i < items.length; i++) {
  321. var item = items[i];
  322. if (this.isFileAllowed(item)) {
  323. result = true;
  324. this.onFileInserted(item.getAsFile());
  325. this.uploadFile(item.getAsFile());
  326. }
  327. }
  328. }
  329. if (result) { e.preventDefault(); }
  330. return result;
  331. };
  332. /**
  333. * Called when a drop event occures
  334. * @param {Event} e
  335. * @return {Boolean} if the event was handled
  336. */
  337. inlineAttachment.prototype.onDrop = function(e) {
  338. var result = false;
  339. for (var i = 0; i < e.dataTransfer.files.length; i++) {
  340. var file = e.dataTransfer.files[i];
  341. if (this.isFileAllowed(file)) {
  342. result = true;
  343. this.onFileInserted(file);
  344. this.uploadFile(file);
  345. }
  346. }
  347. return result;
  348. };
  349. window.inlineAttachment = inlineAttachment;
  350. })(document, window);
  351. /*jslint newcap: true */
  352. /*global inlineAttachment: false */
  353. (function() {
  354. 'use strict';
  355. inlineAttachment.editors.input = {
  356. Editor: function(instance) {
  357. var input = instance;
  358. return {
  359. getValue: function() {
  360. return input.value;
  361. },
  362. insertValue: function(val) {
  363. inlineAttachment.util.insertTextAtCursor(input, val);
  364. },
  365. setValue: function(val) {
  366. input.value = val;
  367. }
  368. };
  369. },
  370. attachToInput: function(input, options) {
  371. options = options || {};
  372. var editor = new inlineAttachment.editors.input.Editor(input),
  373. inlineattach = new inlineAttachment(options, editor);
  374. input.addEventListener('paste', function(e) {
  375. inlineattach.onPaste(e);
  376. }, false);
  377. input.addEventListener('drop', function(e) {
  378. e.stopPropagation();
  379. e.preventDefault();
  380. inlineattach.onDrop(e);
  381. }, false);
  382. input.addEventListener('dragenter', function(e) {
  383. e.stopPropagation();
  384. e.preventDefault();
  385. }, false);
  386. input.addEventListener('dragover', function(e) {
  387. e.stopPropagation();
  388. e.preventDefault();
  389. }, false);
  390. }
  391. };
  392. })();