434 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			434 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*jslint newcap: true */
 | |
| /*global XMLHttpRequest: false, FormData: false */
 | |
| /*
 | |
|  * Inline Text Attachment
 | |
|  *
 | |
|  * Author: Roy van Kaathoven
 | |
|  * Contact: ik@royvankaathoven.nl
 | |
|  */
 | |
| (function(document, window) {
 | |
|   'use strict';
 | |
| 
 | |
|   var inlineAttachment = function(options, instance) {
 | |
|     this.settings = inlineAttachment.util.merge(options, inlineAttachment.defaults);
 | |
|     this.editor = instance;
 | |
|     this.filenameTag = '{filename}';
 | |
|     this.lastValue = null;
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Will holds the available editors
 | |
|    *
 | |
|    * @type {Object}
 | |
|    */
 | |
|   inlineAttachment.editors = {};
 | |
| 
 | |
|   /**
 | |
|    * Utility functions
 | |
|    */
 | |
|   inlineAttachment.util = {
 | |
| 
 | |
|     /**
 | |
|      * Simple function to merge the given objects
 | |
|      *
 | |
|      * @param {Object[]} object Multiple object parameters
 | |
|      * @returns {Object}
 | |
|      */
 | |
|     merge: function() {
 | |
|       var result = {};
 | |
|       for (var i = arguments.length - 1; i >= 0; i--) {
 | |
|         var obj = arguments[i];
 | |
|         for (var k in obj) {
 | |
|           if (obj.hasOwnProperty(k)) {
 | |
|             result[k] = obj[k];
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return result;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Append a line of text at the bottom, ensuring there aren't unnecessary newlines
 | |
|      *
 | |
|      * @param {String} appended Current content
 | |
|      * @param {String} previous Value which should be appended after the current content
 | |
|      */
 | |
|     appendInItsOwnLine: function(previous, appended) {
 | |
|       return (previous + "\n\n[[D]]" + appended)
 | |
|         .replace(/(\n{2,})\[\[D\]\]/, "\n\n")
 | |
|         .replace(/^(\n*)/, "");
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Inserts the given value at the current cursor position of the textarea element
 | |
|      *
 | |
|      * @param  {HtmlElement} el
 | |
|      * @param  {String} value Text which will be inserted at the cursor position
 | |
|      */
 | |
|     insertTextAtCursor: function(el, text) {
 | |
|       var scrollPos = el.scrollTop,
 | |
|         strPos = 0,
 | |
|         browser = false,
 | |
|         range;
 | |
| 
 | |
|       if ((el.selectionStart || el.selectionStart === '0')) {
 | |
|         browser = "ff";
 | |
|       } else if (document.selection) {
 | |
|         browser = "ie";
 | |
|       }
 | |
| 
 | |
|       if (browser === "ie") {
 | |
|         el.focus();
 | |
|         range = document.selection.createRange();
 | |
|         range.moveStart('character', -el.value.length);
 | |
|         strPos = range.text.length;
 | |
|       } else if (browser === "ff") {
 | |
|         strPos = el.selectionStart;
 | |
|       }
 | |
| 
 | |
|       var front = (el.value).substring(0, strPos);
 | |
|       var back = (el.value).substring(strPos, el.value.length);
 | |
|       el.value = front + text + back;
 | |
|       strPos = strPos + text.length;
 | |
|       if (browser === "ie") {
 | |
|         el.focus();
 | |
|         range = document.selection.createRange();
 | |
|         range.moveStart('character', -el.value.length);
 | |
|         range.moveStart('character', strPos);
 | |
|         range.moveEnd('character', 0);
 | |
|         range.select();
 | |
|       } else if (browser === "ff") {
 | |
|         el.selectionStart = strPos;
 | |
|         el.selectionEnd = strPos;
 | |
|         el.focus();
 | |
|       }
 | |
|       el.scrollTop = scrollPos;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Default configuration options
 | |
|    *
 | |
|    * @type {Object}
 | |
|    */
 | |
|   inlineAttachment.defaults = {
 | |
|     /**
 | |
|      * URL where the file will be send
 | |
|      */
 | |
|     uploadUrl: 'uploadimage',
 | |
| 
 | |
|     /**
 | |
|      * Which method will be used to send the file to the upload URL
 | |
|      */
 | |
|     uploadMethod: 'POST',
 | |
| 
 | |
|     /**
 | |
|      * Name in which the file will be placed
 | |
|      */
 | |
|     uploadFieldName: 'image',
 | |
| 
 | |
|     /**
 | |
|      * Extension which will be used when a file extension could not
 | |
|      * be detected
 | |
|      */
 | |
|     defaultExtension: 'png',
 | |
| 
 | |
|     /**
 | |
|      * JSON field which refers to the uploaded file URL
 | |
|      */
 | |
|     jsonFieldName: 'link',
 | |
| 
 | |
|     /**
 | |
|      * Allowed MIME types
 | |
|      */
 | |
|     allowedTypes: window.allowedUploadMimeTypes,
 | |
| 
 | |
|     /**
 | |
|      * Text which will be inserted when dropping or pasting a file.
 | |
|      * Acts as a placeholder which will be replaced when the file is done with uploading
 | |
|      */
 | |
|     progressText: '![Uploading file...{filename}]()',
 | |
| 
 | |
|     /**
 | |
|      * When a file has successfully been uploaded the progressText
 | |
|      * will be replaced by the urlText, the {filename} tag will be replaced
 | |
|      * by the filename that has been returned by the server
 | |
|      */
 | |
|     urlText: "",
 | |
| 
 | |
|     /**
 | |
|      * Text which will be used when uploading has failed
 | |
|      */
 | |
|     errorText: "Error uploading file",
 | |
| 
 | |
|     /**
 | |
|      * Extra parameters which will be send when uploading a file
 | |
|      */
 | |
|     extraParams: {},
 | |
| 
 | |
|     /**
 | |
|      * Extra headers which will be send when uploading a file
 | |
|      */
 | |
|     extraHeaders: {},
 | |
| 
 | |
|     /**
 | |
|      * Before the file is send
 | |
|      */
 | |
|     beforeFileUpload: function() {
 | |
|       return true;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Triggers when a file is dropped or pasted
 | |
|      */
 | |
|     onFileReceived: function() {},
 | |
| 
 | |
|     /**
 | |
|      * Custom upload handler
 | |
|      *
 | |
|      * @return {Boolean} when false is returned it will prevent default upload behavior
 | |
|      */
 | |
|     onFileUploadResponse: function() {
 | |
|       return true;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Custom error handler. Runs after removing the placeholder text and before the alert().
 | |
|      * Return false from this function to prevent the alert dialog.
 | |
|      *
 | |
|      * @return {Boolean} when false is returned it will prevent default error behavior
 | |
|      */
 | |
|     onFileUploadError: function() {
 | |
|       return true;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * When a file has succesfully been uploaded
 | |
|      */
 | |
|     onFileUploaded: function() {}
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Uploads the blob
 | |
|    *
 | |
|    * @param  {Blob} file blob data received from event.dataTransfer object
 | |
|    * @return {XMLHttpRequest} request object which sends the file
 | |
|    */
 | |
|   inlineAttachment.prototype.uploadFile = function(file, id) {
 | |
|     var me = this,
 | |
|       formData = new FormData(),
 | |
|       xhr = new XMLHttpRequest(),
 | |
|       id = id,
 | |
|       settings = this.settings,
 | |
|       extension = settings.defaultExtension;
 | |
| 
 | |
|     if (typeof settings.setupFormData === 'function') {
 | |
|       settings.setupFormData(formData, file);
 | |
|     }
 | |
| 
 | |
|     // Attach the file. If coming from clipboard, add a default filename (only works in Chrome for now)
 | |
|     // http://stackoverflow.com/questions/6664967/how-to-give-a-blob-uploaded-as-formdata-a-file-name
 | |
|     if (file.name) {
 | |
|       var fileNameMatches = file.name.match(/\.(.+)$/);
 | |
|       if (fileNameMatches) {
 | |
|         extension = fileNameMatches[1];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     var remoteFilename = "image-" + Date.now() + "." + extension;
 | |
|     if (typeof settings.remoteFilename === 'function') {
 | |
|       remoteFilename = settings.remoteFilename(file);
 | |
|     }
 | |
| 
 | |
|     formData.append(settings.uploadFieldName, file, remoteFilename);
 | |
| 
 | |
|     // Append the extra parameters to the formdata
 | |
|     if (typeof settings.extraParams === "object") {
 | |
|       for (var key in settings.extraParams) {
 | |
|         if (settings.extraParams.hasOwnProperty(key)) {
 | |
|           formData.append(key, settings.extraParams[key]);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     xhr.open('POST', settings.uploadUrl);
 | |
| 
 | |
|     // Add any available extra headers
 | |
|     if (typeof settings.extraHeaders === "object") {
 | |
|         for (var header in settings.extraHeaders) {
 | |
|             if (settings.extraHeaders.hasOwnProperty(header)) {
 | |
|                 xhr.setRequestHeader(header, settings.extraHeaders[header]);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     xhr.onload = function() {
 | |
|       // If HTTP status is OK or Created
 | |
|       if (xhr.status === 200 || xhr.status === 201) {
 | |
|         me.onFileUploadResponse(xhr, id);
 | |
|       } else {
 | |
|         me.onFileUploadError(xhr, id);
 | |
|       }
 | |
|     };
 | |
|     if (settings.beforeFileUpload(xhr) !== false) {
 | |
|       xhr.send(formData);
 | |
|     }
 | |
|     return xhr;
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Returns if the given file is allowed to handle
 | |
|    *
 | |
|    * @param {File} clipboard data file
 | |
|    */
 | |
|   inlineAttachment.prototype.isFileAllowed = function(file) {
 | |
|     if (this.settings.allowedTypes.indexOf('*') === 0){
 | |
|       return true;
 | |
|     } else {
 | |
|       return this.settings.allowedTypes.indexOf(file.type) >= 0;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Handles upload response
 | |
|    *
 | |
|    * @param  {XMLHttpRequest} xhr
 | |
|    * @return {Void}
 | |
|    */
 | |
|   inlineAttachment.prototype.onFileUploadResponse = function(xhr, id) {
 | |
|     if (this.settings.onFileUploadResponse.call(this, xhr) !== false) {
 | |
|       var result = JSON.parse(xhr.responseText),
 | |
|         filename = result[this.settings.jsonFieldName];
 | |
| 
 | |
|       if (result && filename) {
 | |
|           var replacements = [];
 | |
|           var string = this.settings.progressText.replace(this.filenameTag, id);
 | |
|           var lines = this.editor.getValue().split('\n');
 | |
|         var newValue = this.settings.urlText.replace(this.filenameTag, filename);
 | |
|           for(var i = 0; i < lines.length; i++) {
 | |
|             var ch = lines[i].indexOf(string);
 | |
|             if(ch != -1)
 | |
|                 replacements.push({replacement:newValue, from:{line:i, ch:ch}, to:{line:i, ch:ch + string.length}});
 | |
|         }
 | |
|           for(var i = 0; i < replacements.length; i++)
 | |
|             this.editor.replaceRange(replacements[i]);
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Called when a file has failed to upload
 | |
|    *
 | |
|    * @param  {XMLHttpRequest} xhr
 | |
|    * @return {Void}
 | |
|    */
 | |
|   inlineAttachment.prototype.onFileUploadError = function(xhr, id) {
 | |
|     if (this.settings.onFileUploadError.call(this, xhr) !== false) {
 | |
|       var replacements = [];
 | |
|       var string = this.settings.progressText.replace(this.filenameTag, id);
 | |
|       var lines = this.editor.getValue().split('\n');
 | |
|         for(var i = 0; i < lines.length; i++) {
 | |
|             var ch = lines[i].indexOf(this.lastValue);
 | |
|             if(ch != -1)
 | |
|                 replacements.push({replacement:"", from:{line:i, ch:ch}, to:{line:i, ch:ch + string.length}});
 | |
|         }
 | |
|         for(var i = 0; i < replacements.length; i++)
 | |
|             this.editor.replaceRange(replacements[i]);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Called when a file has been inserted, either by drop or paste
 | |
|    *
 | |
|    * @param  {File} file
 | |
|    * @return {Void}
 | |
|    */
 | |
|   inlineAttachment.prototype.onFileInserted = function(file, id) {
 | |
|     if (this.settings.onFileReceived.call(this, file) !== false) {
 | |
|       this.lastValue = this.settings.progressText.replace(this.filenameTag, id);
 | |
|       this.editor.insertValue(this.lastValue + "\n");
 | |
|     }
 | |
|   };
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Called when a paste event occured
 | |
|    * @param  {Event} e
 | |
|    * @return {Boolean} if the event was handled
 | |
|    */
 | |
|   inlineAttachment.prototype.onPaste = function(e) {
 | |
|     var result = false,
 | |
|       clipboardData = e.clipboardData,
 | |
|       items;
 | |
| 
 | |
|     if (typeof clipboardData === "object") {
 | |
|       items = clipboardData.items || clipboardData.files || [];
 | |
| 
 | |
|       for (var i = 0; i < items.length; i++) {
 | |
|         var item = items[i];
 | |
|         if (this.isFileAllowed(item)) {
 | |
|           result = true;
 | |
|           var id = ID();
 | |
|           var file = item.getAsFile();
 | |
|           if (file !== null) {
 | |
|             this.onFileInserted(file, id);
 | |
|             this.uploadFile(file, id);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (result) { e.preventDefault(); }
 | |
| 
 | |
|     return result;
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * Called when a drop event occures
 | |
|    * @param  {Event} e
 | |
|    * @return {Boolean} if the event was handled
 | |
|    */
 | |
|   inlineAttachment.prototype.onDrop = function(e) {
 | |
|     var result = false;
 | |
|     for (var i = 0; i < e.dataTransfer.files.length; i++) {
 | |
|       var file = e.dataTransfer.files[i];
 | |
|       if (this.isFileAllowed(file)) {
 | |
|         result = true;
 | |
|         var id = ID();
 | |
|         this.onFileInserted(file, id);
 | |
|         this.uploadFile(file, id);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
|   };
 | |
| 
 | |
|   window.inlineAttachment = inlineAttachment;
 | |
| 
 | |
| })(document, window);
 | |
| 
 | |
| // Generate unique IDs for use as pseudo-private/protected names.
 | |
| // Similar in concept to
 | |
| // <http://wiki.ecmascript.org/doku.php?id=strawman:names>.
 | |
| //
 | |
| // The goals of this function are twofold:
 | |
| //
 | |
| // * Provide a way to generate a string guaranteed to be unique when compared
 | |
| //   to other strings generated by this function.
 | |
| // * Make the string complex enough that it is highly unlikely to be
 | |
| //   accidentally duplicated by hand (this is key if you're using `ID`
 | |
| //   as a private/protected name on an object).
 | |
| //
 | |
| // Use:
 | |
| //
 | |
| //     var privateName = ID();
 | |
| //     var o = { 'public': 'foo' };
 | |
| //     o[privateName] = 'bar';
 | |
| var ID = function () {
 | |
|   // Math.random should be unique because of its seeding algorithm.
 | |
|   // Convert it to base 36 (numbers + letters), and grab the first 9 characters
 | |
|   // after the decimal.
 | |
|   return '_' + Math.random().toString(36).substr(2, 9);
 | |
| };
 |