435 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			435 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/*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: [
 | 
						|
      'image/jpeg',
 | 
						|
      'image/png',
 | 
						|
      'image/jpg',
 | 
						|
      'image/gif'
 | 
						|
    ],
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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.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
 | 
						|
// <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);
 | 
						|
}; |