/*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 */ defualtExtension: '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: "![]({filename})", /** * 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.defualtExtension; 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(); this.onFileInserted(item.getAsFile(), id); this.uploadFile(item.getAsFile(), 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 // . // // 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); };